diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3679719 --- /dev/null +++ b/.gitignore @@ -0,0 +1,111 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock +package-lock.json + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# IDE files +.idea/ + +**/target/ +**/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 5830aae..42e7b54 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,78 @@ -# cucumber-js-playwright-browserstack -Creating a sample repo for different Playwright languages and runners +# Running Playwright Tests with Cucumber.js on BrowserStack + +Cucumber.js is a JavaScript-based open-source framework for web automation testing. It runs on Node.js and latest web browsers. Cucumber.js allows you to write and execute tests in Gherkin - a non-technical and human-readable language. + +By default, Cucumber JS will automatically search for Step Definitions in the same folder as the feature files. + +This repository provides an example setup for running Playwright tests with Cucumber.js on the BrowserStack cloud platform. + +## Prerequisites + +Before getting started, make sure you have the following prerequisites installed: + +- [Node.js](https://nodejs.org) - The JavaScript runtime environment +- [npm](https://www.npmjs.com/) - The Node.js package manager +- [Playwright](https://playwright.dev/) - A Node.js library for browser automation + +## Setup + +1. Clone this repository to your local machine: + +```bash +git clone https://github.com/browserstack/cucumber-js-playwright-browserstack.git +``` + +2. Install the dependencies by navigating to the project directory and running: + + +```bash +npm install +``` + +3. Set up your BrowserStack Environment variables + +```plaintext +BROWSERSTACK_USERNAME= +BROWSERSTACK_ACCESS_KEY= +``` + +Replace `` and `` with your BrowserStack username and access key, which you can obtain from the BrowserStack dashboard. + +## Browserstack Configuration +To view and update the Browser-OS combinations, please refer to the "features/steps/setup.js" file. + +## Execution +To run the tests in parallel with different browser configurations, use the following command: + +## Running your Tests: +To run a sample Test, run + +```bash +npm run sample-test +``` +This command will execute the Cucumber.js tests in parallel on Chrome and Edge browsers based on the configurations specified in the "features/steps/setup.js" file.` + +## Run Test on Locally hosted Websites + +```bash +npm sample-local-test +``` + +## Reporting + +By default, Cucumber.js generates HTML reports in the `reports` directory after the test execution. You can open the HTML report in your browser to view the test results and detailed information. + +## Conclusion + +This repository provides a basic setup for running Playwright tests with Cucumber.js on the BrowserStack cloud platform. Feel free to modify and expand it based on your project requirements. + +For more information on Playwright and Cucumber.js, refer to their respective documentation: + +- [Playwright Documentation](https://playwright.dev/docs/intro) +- [Cucumber.js Documentation](https://github.com/cucumber/cucumber-js) + +For additional details on configuring and using BrowserStack with Playwright, consult the BrowserStack documentation: + +- [BrowserStack Documentation](https://www.browserstack.com/docs) + +Happy testing! \ No newline at end of file diff --git a/features/config/config.js b/features/config/config.js new file mode 100644 index 0000000..f0f636c --- /dev/null +++ b/features/config/config.js @@ -0,0 +1,4 @@ +Object.assign(global, { + BASE_URL: 'https://bstackdemo.com/', + LOCAL_URL: 'https://www.example.com/' +}); \ No newline at end of file diff --git a/features/steps/assertions.js b/features/steps/assertions.js new file mode 100644 index 0000000..0cdc387 --- /dev/null +++ b/features/steps/assertions.js @@ -0,0 +1,4 @@ +const chai = require('chai'); +global.expect = chai.expect; +global.assert = chai.assert; +global.should = chai.should; \ No newline at end of file diff --git a/features/steps/setup.js b/features/steps/setup.js new file mode 100644 index 0000000..9dd8eb4 --- /dev/null +++ b/features/steps/setup.js @@ -0,0 +1,108 @@ +const { setWorldConstructor, World, Before, After, Status, setDefaultTimeout } = require("@cucumber/cucumber"); +const { chromium, devices } = require('playwright'); +const cp = require('child_process'); +const BrowserStackLocal = require('browserstack-local'); +const playwrightClientVersion = cp.execSync('npx playwright --version').toString().trim().split(' ')[1]; +setDefaultTimeout(120 * 1000); + +const enableLocalTesting = false; // Set this flag to true to enable BrowserStack Local testing + +const browserConfigs = [ + + { + browserName: 'chrome', + browserTagName: '@chrome', + browserVersion: 'latest', + os: 'OS X', + osVersion: 'Monterey', + resolution: '1280x1024', + }, + { + browserName: 'playwright-firefox', + browserTagName: '@playwright-firefox', + browserVersion: 'latest', + os: 'Windows', + osVersion: '11', + resolution: '1280x1024', + }, + { + browserName: 'edge', + browserTagName: '@edge', + browserVersion: 'latest', + os: 'OS X', + osVersion: 'Ventura', + resolution: '1280x1024', + }, +]; + +const browserConnections = []; + +Before(async (scenario) => { + const tagName = scenario.pickle.tags[0].name; + const filteredBrowserConfigs = browserConfigs.filter(config => config.browserTagName === tagName); + + for (const browserConfig of filteredBrowserConfigs) { + const caps = { + ...browserConfig, + 'browserstack.username': process.env.BROWSERSTACK_USERNAME || 'YOUR_USERNAME', + 'browserstack.accessKey': process.env.BROWSERSTACK_ACCESS_KEY || 'YOUR_ACCESS_KEY', + 'project': 'PLAYWRIGHT-CUCUMBER-JS', + 'build': 'playwright-cucumber-build-1', + 'name': scenario.pickle.name, + 'buildTag': 'Regression', + 'browserstack.playwrightVersion': '1.latest', + 'client.playwrightVersion': '1.latest' + }; + + console.log('enableLocalTesting', enableLocalTesting); + if (enableLocalTesting && browserConnections.length === 0) { + const bsLocal = new BrowserStackLocal.Local(); + const bsLocalArgs = { + key: process.env.BROWSERSTACK_ACCESS_KEY, // Replace with your BrowserStack access key + localIdentifier: 'local_connection_name' // Replace with your desired local connection name + }; + + await new Promise((resolve, reject) => { + bsLocal.start(bsLocalArgs, (error) => { + if (error) { + console.error('Failed to start BrowserStack Local:', error); + reject(error); + } else { + console.log('BrowserStack Local started successfully'); + resolve(); + } + }); + }); + } + + // Create page and browser globals to be used in the scenarios + const browser = await chromium.connect({ + wsEndpoint: `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify(caps))}`, + }); + + const context = await browser.newContext(); + global.page = await context.newPage(); + + browserConnections.push(browser); + } +}); + +After(async () => { + await Promise.all(browserConnections.map(browser => browser.close())); +}); + +After(async (scenario) => { + if (scenario.result.status === Status.PASSED) { + await page.evaluate(_ => { }, `browserstack_executor: ${JSON.stringify({ action: 'setSessionStatus', arguments: { status: 'passed', reason: 'Test Passed' } })}`); + } else if (scenario.result.status === Status.FAILED) { + await page.evaluate(_ => { }, `browserstack_executor: ${JSON.stringify({ action: 'setSessionStatus', arguments: { status: 'failed', reason: 'Test Failed' } })}`); + } +}); + +setWorldConstructor(function () { + this.testCaseRetry = 2; // Set the number of retries for each test case +}); + +module.exports = { + parallel: 2, // Set the number of parallel test workers to the number of browsers +}; \ No newline at end of file diff --git a/features/steps/viewBrowserstack.js b/features/steps/viewBrowserstack.js new file mode 100644 index 0000000..6a130d4 --- /dev/null +++ b/features/steps/viewBrowserstack.js @@ -0,0 +1,48 @@ +const { Given, When, Then } = require("@cucumber/cucumber"); +const { HomePage } = require('../../page-objects/home-page') +const { LoginPage } = require('../../page-objects/login-page') +const { CheckoutPage } = require('../../page-objects/checkout-page') +const homePage = new HomePage(); +const loginPage = new LoginPage(); +const checkoutPage = new CheckoutPage(); + +Given("Open BrowserStack Demo website", { timeout: 60 * 1000 }, async function () { + await homePage.navigateToAutomate(); + await homePage.verifyHomePageIsDisplayed(); +}); + +Given("Open local hosted website", { timeout: 60 * 1000 }, async function () { + await homePage.navigateToLocalWebsite(); +}); + +Given('I SignIn as {string} with {string} password', async function (username, password) { + await homePage.clickSignIn(); + await loginPage.submitLoginForm(username, password); + await homePage.verifyAfterLoginPage(); +}); + +When("I add iPhone 12 to cart", async function () { + await homePage.clickProduct(); + // Click Checkout + await page.click("text=Checkout"); +}); + +When("I add the shipping address and submit the details", async function (dataTable) { + const userPromises = dataTable.hashes().map(async (element) => { + await checkoutPage.setUserDetails(element.FirstName, element.LastName, element.Address, element.State, element.PostalCode); + }); + + await Promise.all(userPromises); +}); + +Then("I should see product has been placed successfully", async function () { + await checkoutPage.clickSubmit(); + await wait(3000); + await checkoutPage.verifyConfirmationMessage(); +}); + +function wait(timeout) { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} \ No newline at end of file diff --git a/features/view-browserstack-local.feature b/features/view-browserstack-local.feature new file mode 100644 index 0000000..e62e174 --- /dev/null +++ b/features/view-browserstack-local.feature @@ -0,0 +1,6 @@ +Feature: Execute the Test in Local Environment + + @browser:1 + Scenario: Verify if User is able run on the local server + Given Open local hosted website + \ No newline at end of file diff --git a/features/view-browserstack.feature b/features/view-browserstack.feature new file mode 100644 index 0000000..16a0c46 --- /dev/null +++ b/features/view-browserstack.feature @@ -0,0 +1,21 @@ +Feature: View BrowserStack Demo Site + + @chrome + Scenario: Verify if User is able to place the Order + Given Open BrowserStack Demo website + And I SignIn as "fav_user" with "testingisfun99" password + When I add iPhone 12 to cart + And I add the shipping address and submit the details + | FirstName | LastName | Address | State | PostalCode | + | Demo | User | H.no 123 | Telangana | 500019 | + Then I should see product has been placed successfully + + @edge + Scenario: Verify if User is able to place the Order on Edge + Given Open BrowserStack Demo website + And I SignIn as "fav_user" with "testingisfun99" password + When I add iPhone 12 to cart + And I add the shipping address and submit the details + | FirstName | LastName | Address | State | PostalCode | + | Demo | User | H.no 123 | Telangana | 500019 | + Then I should see product has been placed successfully \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3da8281 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "playwright-cucumber-js", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "./node_modules/.bin/cucumber-js", + "sample-local-test": "npx cucumber-js ./features/view-browserstack-local.feature", + "sample-test": "npx cucumber-js ./features/view-browserstack.feature", + "test:parallel": "npm-run-all -p test:chrome test:firefox", + "parallel-test": "npx cucumber-parallel --features ./features/*.feature --require ./features/steps/viewBrowserstack.js", + "test:chrome": "cucumber-js --world-parameters \"{\\\"browserConfigIndex\\\": 0}\"", + "test:firefox": "cucumber-js --world-parameters \"{\\\"browserConfigIndex\\\": 1}\"", + "postinstall": "npm update browserstack-node-sdk", + "test-browserstack": "browserstack-node-sdk ./node_modules/.bin/cucumber-js", + "sample-local-test-browserstack": "npx browserstack-node-sdk cucumber-js ./features/view-browserstack-local.feature", + "sample-test-browserstack": "npx browserstack-node-sdk cucumber-js ./features/view-browserstack.feature", + "test:chrome-browserstack": "browserstack-node-sdk cucumber-js --world-parameters \"{\\\"browserConfigIndex\\\": 0}\"", + "test:firefox-browserstack": "browserstack-node-sdk cucumber-js --world-parameters \"{\\\"browserConfigIndex\\\": 1}\"" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@cucumber/cucumber": "^7.3.2", + "assert": "^2.0.0", + "browserstack-local": "^1.5.3", + "cucumber": "^6.0.7", + "cucumber-parallel": "^2.0.3", + "playwright": "1.29.2" + }, + "devDependencies": { + "browserstack-node-sdk": "^1.28.0", + "chai": "^4.3.7", + "concurrently": "^8.2.1", + "npm-run-all": "^4.1.5" + } +} diff --git a/page-objects/checkout-page.js b/page-objects/checkout-page.js new file mode 100644 index 0000000..ef6a21c --- /dev/null +++ b/page-objects/checkout-page.js @@ -0,0 +1,42 @@ +locators = { + "submit_button": "#checkout-shipping-continue", + "confirmation_message": "#confirmation-message" +} + +class CheckoutPage { + + async setUserDetails(firstName, lastName, address, state, pincode) { + await page.locator("//input[@id='firstNameInput']").fill(firstName); + await page.locator("//input[@id='lastNameInput']").fill(lastName); + await page.locator("//input[@id='addressLine1Input']").fill(address); + await page.locator("//input[@id='provinceInput']").fill(state); + await page.locator("//input[@id='postCodeInput']").fill(pincode); + + } + + async verifyHomePageIsDisplayed() { + return expect(await page.title()).to.equal('StackDemo'); + } + + async clickSubmit() { + await page.locator("//button[@id='checkout-shipping-continue']").dblclick(); + + } + + async clickProduct() { + await page.locator("(//div[text()='Add to cart'])[1]").click(); + } + + async verifyAfterLoginPage() { + await page.waitForSelector(locators.username_text); + const visible = await page.isVisible(locators.username_text); + return expect(visible).to.equal(true); + } + + async verifyConfirmationMessage() { + const visible = await page.locator("//legend[@id='confirmation-message']").isVisible(); + return expect(visible).to.equal(true); + } +} + +module.exports = { CheckoutPage }; \ No newline at end of file diff --git a/page-objects/home-page.js b/page-objects/home-page.js new file mode 100644 index 0000000..4569ca5 --- /dev/null +++ b/page-objects/home-page.js @@ -0,0 +1,40 @@ +const locators = { + "sign_in": "#signin", + "username_input": "#user-name", + "password_input": "#password", + "login_button": "#login-button", + "username_text": "//span[text()='fav_user']", +} + +class HomePage { + + async navigateToAutomate() { + return await page.goto(global.BASE_URL); + } + + async navigateToLocalWebsite() { + return await page.goto(global.LOCAL_URL); + } + + async verifyHomePageIsDisplayed() { + return expect(await page.title()).to.equal('StackDemo'); + } + + async clickSignIn() { + const signInLocator = locators.sign_in; + const element = await page.waitForSelector(signInLocator); + await page.click(signInLocator); + } + + async clickProduct() { + await page.locator("(//div[text()='Add to cart'])[1]").click(); + } + + async verifyAfterLoginPage() { + await page.waitForSelector(locators.username_text); + const visible = await page.isVisible(locators.username_text); + return expect(visible).to.equal(true); + } +} + +module.exports = { HomePage }; \ No newline at end of file diff --git a/page-objects/login-page.js b/page-objects/login-page.js new file mode 100644 index 0000000..5f782f7 --- /dev/null +++ b/page-objects/login-page.js @@ -0,0 +1,42 @@ +const locators = { + "sign_in": "#signin", + "username_field": "#username", + "username_input": "//input[@id='react-select-2-input']", + "password_field": "#password", + "password_input": "//input[@id='react-select-3-input']", + "login_button": "#login-btn", + "username_text": "//span[text()='fav_user']" +} + +class LoginPage { + + async navigateToAutomate() { + return await page.goto(global.BASE_URL); + } + + async verifyHomePageIsDisplayed() { + return expect(await page.title()).to.equal('StackDemo'); + } + + async clickSignIn() { + const element = await page.waitForSelector(locators.sign_in); + await page.click(locators.sign_in); + } + + async submitLoginForm(username, password) { + const element = await page.waitForSelector(locators.username_field); + // Click Username field + await page.click(locators.username_field); + // Enter Username + await page.fill(locators.username_input, username); + await page.keyboard.press('Enter'); + // Click Password field + await page.click(locators.password_field); + await page.fill(locators.password_input, password); + await page.keyboard.press('Enter'); + // Click Login Button + await page.click(locators.login_button); + } +} + +module.exports = { LoginPage }; \ No newline at end of file