From 4271da651a8f49dc8997b99f747771b0b6a5d55e Mon Sep 17 00:00:00 2001 From: aidenbrown Date: Mon, 1 Jun 2026 11:03:38 -0400 Subject: [PATCH 1/4] Add BrowserStack Playwright demo --- .github/workflows/build.yml | 28 +++ .../browserstack-playwright.spec.js | 180 ++++++++++++++++++ browserstack-demo/playwright.config.js | 14 ++ package-lock.json | 180 ++++++++++++++++++ package.json | 5 + 5 files changed, 407 insertions(+) create mode 100644 browserstack-demo/browserstack-playwright.spec.js create mode 100644 browserstack-demo/playwright.config.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cd3f96..358fe54 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,3 +43,31 @@ jobs: - name: Run tests run: npm run test -- --ci --coverage --maxWorkers=2 --reporters=default --reporters=github-actions + + browserstack-playwright: + name: BrowserStack Playwright Test + runs-on: ubuntu-latest + permissions: + contents: read + if: github.event_name == 'workflow_dispatch' + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build packages + run: npm run build + + - name: Run BrowserStack Playwright test + run: npm run test:browserstack diff --git a/browserstack-demo/browserstack-playwright.spec.js b/browserstack-demo/browserstack-playwright.spec.js new file mode 100644 index 0000000..cfd1f74 --- /dev/null +++ b/browserstack-demo/browserstack-playwright.spec.js @@ -0,0 +1,180 @@ +import { test, expect, chromium } from '@playwright/test'; +import { execSync } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import http from 'http'; +import handler from 'serve-handler'; +import BrowserStackLocal from 'browserstack-local'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.join(__dirname, '..'); + +let server; +let bsLocal; + +// Helper to start a local HTTP server +function startHttpServer(port, rootDir) { + return new Promise((resolve, reject) => { + server = http.createServer((req, res) => { + handler(req, res, { + public: rootDir, + cleanUrls: false, + }); + }); + + server.listen(port, 'localhost', () => { + console.log(`Server running on http://localhost:${port}`); + resolve(); + }); + + server.on('error', reject); + }); +} + +// Helper to stop the HTTP server +function stopHttpServer() { + return new Promise((resolve, reject) => { + if (server) { + server.close((err) => { + if (err) reject(err); + else resolve(); + }); + } else { + resolve(); + } + }); +} + +// Helper to start BrowserStack Local +function startBrowserStackLocal(username, accessKey, localIdentifier) { + return new Promise((resolve, reject) => { + bsLocal = new BrowserStackLocal.Local(); + const bsLocalArgs = { + key: accessKey, + localIdentifier: localIdentifier, + forceLocal: true, + }; + + bsLocal.start(bsLocalArgs, (err) => { + if (err) { + reject(new Error(`Failed to start BrowserStack Local: ${err}`)); + } else { + console.log('BrowserStack Local started'); + resolve(); + } + }); + }); +} + +// Helper to stop BrowserStack Local +function stopBrowserStackLocal() { + return new Promise((resolve) => { + if (bsLocal && bsLocal.isRunning()) { + bsLocal.stop(() => { + console.log('BrowserStack Local stopped'); + resolve(); + }); + } else { + resolve(); + } + }); +} + +test.describe('BrowserStack Playwright Demo', () => { + const username = process.env.BROWSERSTACK_USERNAME; + const accessKey = process.env.BROWSERSTACK_ACCESS_KEY; + const localIdentifier = `local-${Date.now()}`; + const localPort = 8080; + const exampleDir = path.join(repoRoot, 'packages', 'plugin-button-click-counter'); + + test.beforeAll(async () => { + if (!username || !accessKey) { + throw new Error('BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables are required'); + } + + // Start local HTTP server + await startHttpServer(localPort, exampleDir); + + // Start BrowserStack Local + await startBrowserStackLocal(username, accessKey, localIdentifier); + }); + + test.afterAll(async () => { + // Stop BrowserStack Local + await stopBrowserStackLocal(); + + // Stop HTTP server + await stopHttpServer(); + }); + + test('should load example page and interact with button', async () => { + const clientPlaywrightVersion = execSync('npx playwright --version') + .toString() + .trim() + .split(' ')[1]; + + const capabilities = { + browser: 'playwright-chromium', + os: 'osx', + os_version: 'sonoma', + name: 'Plugin Button Click Counter Example', + build: 'Training Repo Test', + project: 'BrowserStack Playwright Demo', + 'browserstack.username': username, + 'browserstack.accessKey': accessKey, + 'browserstack.local': 'true', + 'browserstack.localIdentifier': localIdentifier, + 'client.playwrightVersion': clientPlaywrightVersion, + }; + + const wsEndpoint = `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent( + JSON.stringify(capabilities) + )}`; + + console.log('BrowserStack capabilities:', { + browser: capabilities.browser, + os: capabilities.os, + os_version: capabilities.os_version, + name: capabilities.name, + build: capabilities.build, + project: capabilities.project, + local: capabilities['browserstack.local'], + localIdentifier: capabilities['browserstack.localIdentifier'], + clientPlaywrightVersion: capabilities['client.playwrightVersion'], + hasUsername: Boolean(capabilities['browserstack.username']), + hasAccessKey: Boolean(capabilities['browserstack.accessKey']), + }); + + const browser = await chromium.connect({ + wsEndpoint, + }); + + const context = await browser.newContext(); + const page = await context.newPage(); + + try { + await page.goto(`http://localhost:${localPort}/examples/index.html`, { + waitUntil: 'networkidle', + timeout: 30000, + }); + + const body = page.locator('body'); + await expect(body).toBeVisible(); + + const buttons = page.locator('button'); + const buttonCount = await buttons.count(); + + if (buttonCount > 0) { + const firstButton = buttons.first(); + await firstButton.click(); + console.log('Clicked the first button'); + } + + await expect(body).toBeVisible(); + console.log('Test passed: page loaded and interacted successfully'); + } finally { + await context.close(); + await browser.close(); + } + }); +}); diff --git a/browserstack-demo/playwright.config.js b/browserstack-demo/playwright.config.js new file mode 100644 index 0000000..14b36aa --- /dev/null +++ b/browserstack-demo/playwright.config.js @@ -0,0 +1,14 @@ +export default { + testDir: '.', + testMatch: '**/browserstack-*.spec.js', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: 'html', + use: { + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + timeout: 60000, +}; diff --git a/package-lock.json b/package-lock.json index 17bfbec..49b7129 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,10 @@ "devDependencies": { "@changesets/changelog-github": "^0.4.7", "@changesets/cli": "^2.25.2", + "@playwright/test": "^1.40.0", + "browserstack-local": "^1.2.8", + "playwright": "^1.40.0", + "serve-handler": "^6.1.5", "turbo": "^1.6.3" }, "engines": { @@ -3469,6 +3473,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "26.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz", @@ -4985,6 +5005,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/browserstack-local": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/browserstack-local/-/browserstack-local-1.5.13.tgz", + "integrity": "sha512-7helY+Ms3ss4BtIQZTIyshdAFZSvS9A7ZpEB9stRaobeZ9BM1BkJFTuMakQNTOj78llv0+/qDI5Ak+bkGWV1xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "https-proxy-agent": "^5.0.1", + "is-running": "^2.1.0", + "tree-kill": "^1.2.2" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -5050,6 +5083,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -5386,6 +5429,16 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -7516,6 +7569,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-running": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-running/-/is-running-2.1.0.tgz", + "integrity": "sha512-mjJd3PujZMl7j+D395WTIO5tU5RIDBfVSRtRR4VOJou3H66E38UjbjvDGh3slJzPuolsb+yQFqwHNNdyp5jg3w==", + "dev": true, + "license": "BSD" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -10285,6 +10345,13 @@ "node": ">=0.10.0" } }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -10349,6 +10416,13 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10412,6 +10486,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -10596,6 +10717,16 @@ "seedrandom": "^3.0.5" } }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -11135,6 +11266,45 @@ "node": ">= 10.13.0" } }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-handler/node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -11760,6 +11930,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/package.json b/package.json index 5e8c2a9..94cb129 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "test": "jest", "test:watch": "npm test -- --watch", + "test:browserstack": "playwright test --config=browserstack-demo/playwright.config.js browserstack-demo/browserstack-playwright.spec.js", "build": "turbo run build", "tsc": "turbo tsc", "changeset": "changeset", @@ -20,6 +21,10 @@ "devDependencies": { "@changesets/changelog-github": "^0.4.7", "@changesets/cli": "^2.25.2", + "@playwright/test": "^1.40.0", + "browserstack-local": "^1.2.8", + "playwright": "^1.40.0", + "serve-handler": "^6.1.5", "turbo": "^1.6.3" }, "jest": { From a2118df897a092a0858c43e9e718936c523fb3ba Mon Sep 17 00:00:00 2001 From: aidenbrown Date: Mon, 1 Jun 2026 11:21:28 -0400 Subject: [PATCH 2/4] Add new BrowserStack Playwright demo --- .../browserstack-playwright.spec.js | 317 ++++++++++-------- 1 file changed, 176 insertions(+), 141 deletions(-) diff --git a/browserstack-demo/browserstack-playwright.spec.js b/browserstack-demo/browserstack-playwright.spec.js index cfd1f74..a0501cb 100644 --- a/browserstack-demo/browserstack-playwright.spec.js +++ b/browserstack-demo/browserstack-playwright.spec.js @@ -14,167 +14,202 @@ let bsLocal; // Helper to start a local HTTP server function startHttpServer(port, rootDir) { - return new Promise((resolve, reject) => { - server = http.createServer((req, res) => { - handler(req, res, { - public: rootDir, - cleanUrls: false, - }); + return new Promise((resolve, reject) => { + server = http.createServer((req, res) => { + handler(req, res, { + public: rootDir, + cleanUrls: false, + }); + }); + + server.listen(port, 'localhost', () => { + console.log(`Server running on http://localhost:${port}`); + resolve(); + }); + + server.on('error', reject); }); - - server.listen(port, 'localhost', () => { - console.log(`Server running on http://localhost:${port}`); - resolve(); - }); - - server.on('error', reject); - }); } // Helper to stop the HTTP server function stopHttpServer() { - return new Promise((resolve, reject) => { - if (server) { - server.close((err) => { - if (err) reject(err); - else resolve(); - }); - } else { - resolve(); - } - }); + return new Promise((resolve, reject) => { + if (server) { + server.close((err) => { + if (err) reject(err); + else resolve(); + }); + } else { + resolve(); + } + }); } // Helper to start BrowserStack Local function startBrowserStackLocal(username, accessKey, localIdentifier) { - return new Promise((resolve, reject) => { - bsLocal = new BrowserStackLocal.Local(); - const bsLocalArgs = { - key: accessKey, - localIdentifier: localIdentifier, - forceLocal: true, - }; - - bsLocal.start(bsLocalArgs, (err) => { - if (err) { - reject(new Error(`Failed to start BrowserStack Local: ${err}`)); - } else { - console.log('BrowserStack Local started'); - resolve(); - } + return new Promise((resolve, reject) => { + bsLocal = new BrowserStackLocal.Local(); + const bsLocalArgs = { + key: accessKey, + localIdentifier: localIdentifier, + forceLocal: true, + }; + + bsLocal.start(bsLocalArgs, (err) => { + if (err) { + reject(new Error(`Failed to start BrowserStack Local: ${err}`)); + } else { + console.log('BrowserStack Local started'); + resolve(); + } + }); }); - }); } // Helper to stop BrowserStack Local function stopBrowserStackLocal() { - return new Promise((resolve) => { - if (bsLocal && bsLocal.isRunning()) { - bsLocal.stop(() => { - console.log('BrowserStack Local stopped'); - resolve(); - }); - } else { - resolve(); - } - }); + return new Promise((resolve) => { + if (bsLocal && bsLocal.isRunning()) { + bsLocal.stop(() => { + console.log('BrowserStack Local stopped'); + resolve(); + }); + } else { + resolve(); + } + }); } test.describe('BrowserStack Playwright Demo', () => { - const username = process.env.BROWSERSTACK_USERNAME; - const accessKey = process.env.BROWSERSTACK_ACCESS_KEY; - const localIdentifier = `local-${Date.now()}`; - const localPort = 8080; - const exampleDir = path.join(repoRoot, 'packages', 'plugin-button-click-counter'); - - test.beforeAll(async () => { - if (!username || !accessKey) { - throw new Error('BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables are required'); - } - - // Start local HTTP server - await startHttpServer(localPort, exampleDir); - - // Start BrowserStack Local - await startBrowserStackLocal(username, accessKey, localIdentifier); - }); + const username = process.env.BROWSERSTACK_USERNAME; + const accessKey = process.env.BROWSERSTACK_ACCESS_KEY; + const localIdentifier = `local-${Date.now()}`; + const localPort = 8080; + const exampleDir = path.join(repoRoot, 'packages', 'plugin-button-click-counter'); + + test.beforeAll(async () => { + if (!username || !accessKey) { + throw new Error('BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables are required'); + } + + // Start local HTTP server + await startHttpServer(localPort, exampleDir); + + // Start BrowserStack Local + await startBrowserStackLocal(username, accessKey, localIdentifier); + }); test.afterAll(async () => { - // Stop BrowserStack Local - await stopBrowserStackLocal(); - - // Stop HTTP server - await stopHttpServer(); - }); - - test('should load example page and interact with button', async () => { - const clientPlaywrightVersion = execSync('npx playwright --version') - .toString() - .trim() - .split(' ')[1]; - - const capabilities = { - browser: 'playwright-chromium', - os: 'osx', - os_version: 'sonoma', - name: 'Plugin Button Click Counter Example', - build: 'Training Repo Test', - project: 'BrowserStack Playwright Demo', - 'browserstack.username': username, - 'browserstack.accessKey': accessKey, - 'browserstack.local': 'true', - 'browserstack.localIdentifier': localIdentifier, - 'client.playwrightVersion': clientPlaywrightVersion, - }; - - const wsEndpoint = `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent( - JSON.stringify(capabilities) - )}`; - - console.log('BrowserStack capabilities:', { - browser: capabilities.browser, - os: capabilities.os, - os_version: capabilities.os_version, - name: capabilities.name, - build: capabilities.build, - project: capabilities.project, - local: capabilities['browserstack.local'], - localIdentifier: capabilities['browserstack.localIdentifier'], - clientPlaywrightVersion: capabilities['client.playwrightVersion'], - hasUsername: Boolean(capabilities['browserstack.username']), - hasAccessKey: Boolean(capabilities['browserstack.accessKey']), - }); + // Stop BrowserStack Local + await stopBrowserStackLocal(); - const browser = await chromium.connect({ - wsEndpoint, + // Stop HTTP server + await stopHttpServer(); }); - const context = await browser.newContext(); - const page = await context.newPage(); - - try { - await page.goto(`http://localhost:${localPort}/examples/index.html`, { - waitUntil: 'networkidle', - timeout: 30000, - }); - - const body = page.locator('body'); - await expect(body).toBeVisible(); - - const buttons = page.locator('button'); - const buttonCount = await buttons.count(); - - if (buttonCount > 0) { - const firstButton = buttons.first(); - await firstButton.click(); - console.log('Clicked the first button'); - } - - await expect(body).toBeVisible(); - console.log('Test passed: page loaded and interacted successfully'); - } finally { - await context.close(); - await browser.close(); - } - }); + test('should load example page and interact with button', async () => { + const clientPlaywrightVersion = execSync('npx playwright --version') + .toString() + .trim() + .split(' ')[1]; + + const capabilities = { + browser: 'playwright-chromium', + os: 'osx', + os_version: 'sonoma', + name: 'Plugin Button Click Counter Example', + build: 'Training Repo Test', + project: 'BrowserStack Playwright Demo', + 'browserstack.username': username, + 'browserstack.accessKey': accessKey, + 'browserstack.local': 'true', + 'browserstack.localIdentifier': localIdentifier, + 'client.playwrightVersion': clientPlaywrightVersion, + }; + + const wsEndpoint = `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent( + JSON.stringify(capabilities) + )}`; + + console.log('BrowserStack capabilities:', { + browser: capabilities.browser, + os: capabilities.os, + os_version: capabilities.os_version, + name: capabilities.name, + build: capabilities.build, + project: capabilities.project, + local: capabilities['browserstack.local'], + localIdentifier: capabilities['browserstack.localIdentifier'], + clientPlaywrightVersion: capabilities['client.playwrightVersion'], + hasUsername: Boolean(capabilities['browserstack.username']), + hasAccessKey: Boolean(capabilities['browserstack.accessKey']), + }); + + const browser = await chromium.connect({ + wsEndpoint, + }); + + const context = await browser.newContext(); + const page = await context.newPage(); + + try { + await page.goto(`http://localhost:${localPort}/examples/index.html`, { + waitUntil: 'networkidle', + timeout: 30000, + }); + + const body = page.locator('body'); + await expect(body).toBeVisible(); + + // Wait for the click-counter button to appear + const button = page.locator('button').first(); + await expect(button).toBeVisible({ timeout: 10000 }); + await expect(button).toBeEnabled(); + + // Read the initial counter from the page text + const initialBodyText = (await body.textContent())?.trim() ?? ''; + const initialMatch = initialBodyText.match(/Button clicks:\s*(\d+)/); + + if (!initialMatch) { + throw new Error(`Could not find initial button click count in page text: "${initialBodyText}"`); + } + + let previousCount = Number(initialMatch[1]); + console.log(`Initial button click count: ${previousCount}`); + + // Click multiple times and verify the counter increments + const clickAttempts = 3; + + for (let i = 1; i <= clickAttempts; i += 1) { + await button.click(); + + await expect + .poll(async () => { + const currentBodyText = (await body.textContent())?.trim() ?? ''; + const currentMatch = currentBodyText.match(/Button clicks:\s*(\d+)/); + return currentMatch ? Number(currentMatch[1]) : null; + }, { + timeout: 5000, + message: `Expected counter to update after click ${i}`, + }) + .toBe(previousCount + 1); + + previousCount += 1; + console.log(`Counter after click ${i}: ${previousCount}`); + + await expect(button).toBeVisible(); + await expect(button).toBeEnabled(); + } + + // Final safety check: page did not crash and button remains usable + await expect(body).toBeVisible(); + await expect(button).toBeVisible(); + await expect(button).toBeEnabled(); + + console.log('Test passed: click-counter button appeared, incremented after repeated clicks, and remained usable'); + } finally { + await context.close(); + await browser.close(); + } + }); }); From e5673e373d142b31a8a684c57f242e60c11e09a9 Mon Sep 17 00:00:00 2001 From: aidenbrown Date: Mon, 1 Jun 2026 11:25:48 -0400 Subject: [PATCH 3/4] Fixing issue with tracked .gitignore files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 963bbb2..fe4d088 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ coverage/ .vscode/ .turbo .env +local.log +test-results/ +playwright-report/ \ No newline at end of file From 9f1e63b8b9074fda5ec293bad3f0fd74068666ae Mon Sep 17 00:00:00 2001 From: aidenbrown Date: Mon, 1 Jun 2026 11:43:07 -0400 Subject: [PATCH 4/4] Document BrowserStack demo --- browserstack-demo/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 browserstack-demo/README.md diff --git a/browserstack-demo/README.md b/browserstack-demo/README.md new file mode 100644 index 0000000..c73c020 --- /dev/null +++ b/browserstack-demo/README.md @@ -0,0 +1,11 @@ +# BrowserStack Playwright Demo + +This demo runs a Playwright smoke test against the button click counter training plugin using BrowserStack. + +## Local setup + +Set BrowserStack credentials in your shell: + +```bash +export BROWSERSTACK_USERNAME="..." +export BROWSERSTACK_ACCESS_KEY="..." \ No newline at end of file