diff --git a/.gitignore b/.gitignore index 44e6085..6523b69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ node_modules -playwright-report \ No newline at end of file +playwright-report +local.log +local.log +package-lock.json +local.log diff --git a/README.md b/README.md index bbb8842..d8b03bb 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ - To run a sample test, run `npm run sample-test` +## Running your tests on mobile + +- To run a sample test, run `npm run mobile-test` + ## Run tests on locally hosted websites * Run `npm run sample-local-test` diff --git a/browserstack.config.js b/browserstack.config.js deleted file mode 100644 index 6f7132d..0000000 --- a/browserstack.config.js +++ /dev/null @@ -1,52 +0,0 @@ -const base = require('@playwright/test'); -const cp = require('child_process'); -const clientPlaywrightVersion = cp - .execSync('npx playwright --version') - .toString() - .trim() - .split(' ')[1]; -const BrowserStackLocal = require('browserstack-local'); -const util = require('util'); - -// BrowserStack Specific Capabilities. -// Set 'browserstack.local:true For Local testing -const caps = { - browser: 'chrome', - os: 'osx', - os_version: 'catalina', - name: 'My first playwright test', - build: 'playwright-build', - 'browserstack.username': process.env.BROWSERSTACK_USERNAME || 'USERNAME', - 'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY || 'ACCESSKEY', - 'browserstack.local': process.env.BROWSERSTACK_LOCAL || true, - 'client.playwrightVersion': clientPlaywrightVersion, -}; - -exports.bsLocal = new BrowserStackLocal.Local(); - -// replace YOUR_ACCESS_KEY with your key. You can also set an environment variable - "BROWSERSTACK_ACCESS_KEY". -exports.BS_LOCAL_ARGS = { - key: process.env.BROWSERSTACK_ACCESS_KEY || 'ACCESSKEY', -}; - -// Patching the capabilities dynamically according to the project name. -const patchCaps = (name, title) => { - let combination = name.split(/@browserstack/)[0]; - let [browerCaps, osCaps] = combination.split(/:/); - let [browser, browser_version] = browerCaps.split(/@/); - let osCapsSplit = osCaps.split(/ /); - let os = osCapsSplit.shift(); - let os_version = osCapsSplit.join(' '); - caps.browser = browser ? browser : 'chrome'; - caps.browser_version = browser_version ? browser_version : 'latest'; - caps.os = os ? os : 'osx'; - caps.os_version = os_version ? os_version : 'catalina'; - caps.name = title; -}; - -exports.getCdpEndpoint = (name, title) => { - patchCaps(name, title) - const cdpUrl = `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(caps))}` - console.log(`--> ${cdpUrl}`) - return cdpUrl; -} diff --git a/fixture.js b/fixture.js new file mode 100644 index 0000000..f9ef1b2 --- /dev/null +++ b/fixture.js @@ -0,0 +1,155 @@ +const base = require("@playwright/test"); +const cp = require("child_process"); +const { _android } = require("playwright"); +const clientPlaywrightVersion = cp + .execSync("npx playwright --version") + .toString() + .trim() + .split(" ")[1]; +const BrowserStackLocal = require("browserstack-local"); +const util = require("util"); + +// BrowserStack Specific Capabilities. +// Set 'browserstack.local:true For Local testing +const caps = { + osVersion: "13.0", + deviceName: "Samsung Galaxy S23", // "Samsung Galaxy S22 Ultra", "Google Pixel 7 Pro", "OnePlus 9", etc. + browserName: "chrome", + realMobile: "true", + name: "My android playwright test", + build: "playwright-build-1", + "browserstack.username": process.env.BROWSERSTACK_USERNAME || "", + "browserstack.accessKey": + process.env.BROWSERSTACK_ACCESS_KEY || "", + "browserstack.local": process.env.BROWSERSTACK_LOCAL || false, +}; + +exports.bsLocal = new BrowserStackLocal.Local(); + +// replace YOUR_ACCESS_KEY with your key. You can also set an environment variable - "BROWSERSTACK_ACCESS_KEY". +exports.BS_LOCAL_ARGS = { + key: process.env.BROWSERSTACK_ACCESS_KEY || "ACCESSKEY", +}; + +// Patching the capabilities dynamically according to the project name. +const patchMobileCaps = (name, title) => { + let combination = name.split(/@browserstack/)[0]; + let [browerCaps, osCaps] = combination.split(/:/); + let [browser, deviceName] = browerCaps.split(/@/); + let osCapsSplit = osCaps.split(/ /); + let os = osCapsSplit.shift(); + let osVersion = osCapsSplit.join(" "); + caps.browser = browser ? browser : "chrome"; + caps.deviceName = deviceName ? deviceName : "Samsung Galaxy S22 Ultra"; + caps.osVersion = osVersion ? osVersion : "12.0"; + caps.name = title; + caps.realMobile = "true"; +}; + +const patchCaps = (name, title) => { + let combination = name.split(/@browserstack/)[0]; + let [browerCaps, osCaps] = combination.split(/:/); + let [browser, browser_version] = browerCaps.split(/@/); + let osCapsSplit = osCaps.split(/ /); + let os = osCapsSplit.shift(); + let os_version = osCapsSplit.join(" "); + caps.browser = browser ? browser : "chrome"; + caps.browser_version = browser_version ? browser_version : "latest"; + caps.os = os ? os : "osx"; + caps.os_version = os_version ? os_version : "catalina"; + caps.name = title; +}; + +const isHash = (entity) => + Boolean(entity && typeof entity === "object" && !Array.isArray(entity)); +const nestedKeyValue = (hash, keys) => + keys.reduce((hash, key) => (isHash(hash) ? hash[key] : undefined), hash); +const isUndefined = (val) => val === undefined || val === null || val === ""; +const evaluateSessionStatus = (status) => { + if (!isUndefined(status)) { + status = status.toLowerCase(); + } + if (status === "passed") { + return "passed"; + } else if (status === "failed" || status === "timedout") { + return "failed"; + } else { + return ""; + } +}; + +exports.test = base.test.extend({ + page: async ({ page, playwright }, use, testInfo) => { + if (testInfo.project.name.match(/browserstack/)) { + let vBrowser, vContext, vDevice; + const isMobile = testInfo.project.name.match(/browserstack-mobile/); + if (isMobile) { + patchMobileCaps( + testInfo.project.name, + `${testInfo.file} - ${testInfo.title}` + ); + vDevice = await playwright._android.connect( + `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent( + JSON.stringify(caps) + )}` + ); + await vDevice.shell("am force-stop com.android.chrome"); + vContext = await vDevice.launchBrowser(); + } else { + patchCaps(testInfo.project.name, `${testInfo.title}`); + delete caps.osVersion; + delete caps.deviceName; + delete caps.realMobile; + vBrowser = await playwright.chromium.connect({ + wsEndpoint: + `wss://cdp.browserstack.com/playwright?caps=` + + `${encodeURIComponent(JSON.stringify(caps))}`, + }); + vContext = await vBrowser.newContext(testInfo.project.use); + } + const vPage = await vContext.newPage(); + await use(vPage); + + await vPage.close(); + + if (isMobile) { + await vDevice.close(); + } else { + await vBrowser.close(); + } + } else { + use(page); + } + }, + + beforeEach: [ + async ({ page }, use) => { + await page + .context() + .tracing.start({ screenshots: true, snapshots: true, sources: true }); + await use(); + }, + { auto: true }, + ], + + afterEach: [ + async ({ page }, use, testInfo) => { + await use(); + if (testInfo.status == "failed") { + await page + .context() + .tracing.stop({ path: `${testInfo.outputDir}/trace.zip` }); + await page.screenshot({ path: `${testInfo.outputDir}/screenshot.png` }); + await testInfo.attach("screenshot", { + path: `${testInfo.outputDir}/screenshot.png`, + contentType: "image/png", + }); + await testInfo.attach("trace", { + path: `${testInfo.outputDir}/trace.zip`, + contentType: "application/zip", + }); + } + }, + { auto: true }, + ], +}); diff --git a/global-setup.js b/global-setup.js index 207f048..437df95 100644 --- a/global-setup.js +++ b/global-setup.js @@ -1,24 +1,28 @@ // global-setup.js -const { bsLocal, BS_LOCAL_ARGS } = require('./browserstack.config'); -const { promisify } = require('util'); +const { bsLocal, BS_LOCAL_ARGS } = require("./fixture"); +const { promisify } = require("util"); const sleep = promisify(setTimeout); -const redColour = '\x1b[31m'; -const whiteColour = '\x1b[0m'; +const redColour = "\x1b[31m"; +const whiteColour = "\x1b[0m"; module.exports = async () => { - console.log('Starting BrowserStackLocal ...'); - // Starts the Local instance with the required arguments - let localResponseReceived = false; - bsLocal.start(BS_LOCAL_ARGS, (err) => { - if (err) { - console.error( - `${redColour}Error starting BrowserStackLocal${whiteColour}` - ); - } else { - console.log('BrowserStackLocal Started'); + if (process.env.BROWSERSTACK_LOCAL === "true") { + console.log("Starting BrowserStackLocal ..."); + // Starts the Local instance with the required arguments + let localResponseReceived = false; + bsLocal.start(BS_LOCAL_ARGS, (err) => { + if (err) { + console.error( + `${redColour}Error starting BrowserStackLocal${whiteColour}` + ); + } else { + console.log("BrowserStackLocal Started"); + } + localResponseReceived = true; + }); + while (!localResponseReceived) { + await sleep(1000); } - localResponseReceived = true; - }); - while (!localResponseReceived) { - await sleep(1000); + } else { + console.log("Skipping BrowserStackLocal..."); } }; diff --git a/global-teardown.js b/global-teardown.js index 8e16cb3..52f1569 100644 --- a/global-teardown.js +++ b/global-teardown.js @@ -1,6 +1,6 @@ // global-teardown.js -const { bsLocal } = require('./browserstack.config'); -const { promisify } = require('util'); +const { bsLocal } = require("./fixture"); +const { promisify } = require("util"); const sleep = promisify(setTimeout); module.exports = async () => { // Stop the Local instance after your test run is completed, i.e after driver.quit @@ -9,10 +9,10 @@ module.exports = async () => { if (bsLocal && bsLocal.isRunning()) { bsLocal.stop(() => { localStopped = true; - console.log('Stopped BrowserStackLocal'); + console.log("Stopped BrowserStackLocal"); }); while (!localStopped) { await sleep(1000); } } -} +}; diff --git a/local.log b/local.log new file mode 100644 index 0000000..e1e494e Binary files /dev/null and b/local.log differ diff --git a/package.json b/package.json index 4585890..83e4a8d 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "description": "", "main": "index.js", "scripts": { - "sample-local-test":"npx playwright test tests/local_test.js", - "sample-test":"npx playwright test tests/sample_test.js" + "sample-local-test": "BROWSERSTACK_LOCAL=true npx playwright test tests/local_test.js", + "sample-test": "npx playwright test tests/sample_test.js", + "mobile-test": "npx playwright test tests/sample_test.js --project='chrome@Samsung Galaxy S22:13@browserstack-mobile'" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.29.1", - "browserstack-local": "^1.5.1" + "@playwright/test": "^1.34.3", + "browserstack-local": "^1.5.1", + "playwright": "1.34.3", + "prettier": "2.8.8" } } diff --git a/playwright.config.js b/playwright.config.js index c7380f4..5ef9f05 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,6 +1,5 @@ // @ts-check -const { devices } = require('@playwright/test'); -const { getCdpEndpoint } = require('./browserstack.config.js') +const { devices } = require("@playwright/test"); /** * Read environment variables from file. @@ -8,17 +7,17 @@ const { getCdpEndpoint } = require('./browserstack.config.js') */ // require('dotenv').config(); - /** * @see https://playwright.dev/docs/test-configuration * @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { - testDir: './tests', - testMatch: '**/*.js', - - globalSetup: require.resolve('./global-setup'), - globalTeardown: require.resolve('./global-teardown'), + testDir: "./tests", + testMatch: "**/*.js", + + // Use globalSetup & globalTearedown only if browserstack.local = true + globalSetup: require.resolve("./global-setup"), + globalTeardown: require.resolve("./global-teardown"), /* Maximum time one test can run for. */ timeout: 90 * 1000, @@ -27,7 +26,7 @@ const config = { * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 5000 + timeout: 5000, }, /* Run tests in files in parallel */ fullyParallel: true, @@ -38,7 +37,7 @@ const config = { /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ @@ -47,30 +46,33 @@ const config = { // baseURL: 'http://localhost:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { - name: 'chrome@latest:Windows 11', + name: "chrome@latest:Windows 11@browserstack", use: { - connectOptions: { wsEndpoint: getCdpEndpoint('chrome@latest:Windows 11','test1') }, + browserName: "chromium", + channel: "chrome", }, - } - , + }, { - name: 'playwright-webkit@latest:OSX Ventura', + name: "playwright-webkit@latest:OSX Ventura@browserstack", use: { - connectOptions: { wsEndpoint: getCdpEndpoint('playwright-webkit@latest:OSX Ventura', 'test2') } + browserName: "chromium", + channel: "chrome", }, }, { - name: 'playwright-firefox:Windows 11', + name: "chrome@Samsung Galaxy S22:13@browserstack-mobile", use: { - connectOptions: { wsEndpoint: getCdpEndpoint('playwright-firefox:Windows 11', 'test3') } + baseURL: "https://www.bstackdemo.com/", + browserName: "chromium", + channel: "chrome", }, - } + }, ], /* Folder for test artifacts such as screenshots, videos, traces, etc. */ diff --git a/tests/local_test.js b/tests/local_test.js index 6546dba..6923f26 100644 --- a/tests/local_test.js +++ b/tests/local_test.js @@ -1,22 +1,21 @@ // @ts-check -const { test, expect } = require('@playwright/test'); +const { test } = require("../fixture"); +const { expect } = require("@playwright/test"); -test('Local Testing', async ({ page },testInfo) => { +test("Local Testing", async ({ page }, testInfo) => { + try { + await page.evaluate((_) => {}, + `browserstack_executor: ${JSON.stringify({ action: "setSessionName", arguments: { name: testInfo.project.name } })}`); - try{ + await page.waitForTimeout(5000); - await page.evaluate(_ => {},`browserstack_executor: ${JSON.stringify({action: "setSessionName", arguments: {name:testInfo.project.name}})}`); - - await page.waitForTimeout(5000); - - await page.goto('http://localhost:5500/'); - - await page.evaluate(_ => {}, `browserstack_executor: ${JSON.stringify({action: 'setSessionStatus',arguments: {status: 'passed',reason: 'Local success'}})}`); - -} catch (e) { - console.log(e); - await page.evaluate(_ => {}, `browserstack_executor: ${JSON.stringify({action: 'setSessionStatus',arguments: {status: 'failed',reason: 'Local fail'}})}`); - -} + await page.goto("http://localhost:5500/"); + await page.evaluate((_) => {}, + `browserstack_executor: ${JSON.stringify({ action: "setSessionStatus", arguments: { status: "passed", reason: "Local success" } })}`); + } catch (e) { + console.log(e); + await page.evaluate((_) => {}, + `browserstack_executor: ${JSON.stringify({ action: "setSessionStatus", arguments: { status: "failed", reason: "Local fail" } })}`); + } }); diff --git a/tests/sample_test.js b/tests/sample_test.js index b014a0d..6b55f16 100644 --- a/tests/sample_test.js +++ b/tests/sample_test.js @@ -1,40 +1,41 @@ // @ts-check -const { test, expect } = require('@playwright/test'); - -test('BstackDemo Add to cart', async ({ page },testInfo) => { - -try{ - - await page.evaluate(_ => {},`browserstack_executor: ${JSON.stringify({action: "setSessionName", arguments: {name:testInfo.project.name}})}`); - await page.waitForTimeout(5000); - - await page.goto('https://www.bstackdemo.com/',{ waitUntil: 'networkidle' }); - await page.locator('[id="\\32 "]').getByText('Add to cart').click(); - await page.getByText('Checkout').click(); - await page.locator('#username svg').click(); - await page.locator('#react-select-2-option-0-0').click(); - await page.locator('#password svg').click(); - await page.locator('#react-select-3-option-0-0').click(); - await page.getByRole('button', { name: 'Log In' }).click(); - await page.getByLabel('First Name').click(); - await page.getByLabel('First Name').fill('SampleFirst'); - await page.getByLabel('Last Name').click(); - await page.getByLabel('Last Name').fill('sampleLast'); - await page.getByLabel('Address').click(); - await page.getByLabel('Address').fill('sampleAddress'); - await page.getByLabel('State/Province').click(); - await page.getByLabel('State/Province').fill('SampleState'); - await page.getByLabel('Postal Code').click(); - await page.getByLabel('Postal Code').fill('123456'); - await page.getByRole('button', { name: 'Submit' }).click(); - await page.getByRole('button', { name: 'Continue Shopping »' }).click(); - - await page.evaluate(_ => {}, `browserstack_executor: ${JSON.stringify({action: 'setSessionStatus',arguments: {status: 'passed',reason: 'Product added to cart'}})}`); - -} catch (e) { - console.log(e); - await page.evaluate(_ => {}, `browserstack_executor: ${JSON.stringify({action: 'setSessionStatus',arguments: {status: 'failed',reason: 'Test failed'}})}`); - -} - +const { test } = require("../fixture"); +const { expect } = require("@playwright/test"); + +test("BstackDemo Add to cart", async ({ page }, testInfo) => { + try { + await page.evaluate((_) => {}, + `browserstack_executor: ${JSON.stringify({ action: "setSessionName", arguments: { name: testInfo.project.name } })}`); + await page.waitForTimeout(5000); + + await page.goto("https://www.bstackdemo.com/", { + waitUntil: "networkidle", + }); + await page.locator('[id="\\32 "]').getByText("Add to cart").click(); + await page.getByText("Checkout").click(); + await page.locator("#username svg").click(); + await page.locator("#react-select-2-option-0-0").click({ force: true }); + await page.locator("#password svg").click({ force: true }); + await page.locator("#react-select-3-option-0-0").click({ force: true }); + await page.getByRole("button", { name: "Log In" }).click(); + await page.getByLabel("First Name").click(); + await page.getByLabel("First Name").fill("SampleFirst"); + await page.getByLabel("Last Name").click(); + await page.getByLabel("Last Name").fill("sampleLast"); + await page.getByLabel("Address").click(); + await page.getByLabel("Address").fill("sampleAddress"); + await page.getByLabel("State/Province").click(); + await page.getByLabel("State/Province").fill("SampleState"); + await page.getByLabel("Postal Code").click(); + await page.getByLabel("Postal Code").fill("123456"); + await page.getByRole("button", { name: "Submit" }).click(); + await page.getByRole("button", { name: "Continue Shopping »" }).click(); + + await page.evaluate((_) => {}, + `browserstack_executor: ${JSON.stringify({ action: "setSessionStatus", arguments: { status: "passed", reason: "Product added to cart" } })}`); + } catch (e) { + console.log(e); + await page.evaluate((_) => {}, + `browserstack_executor: ${JSON.stringify({ action: "setSessionStatus", arguments: { status: "failed", reason: "Test failed" } })}`); + } });