diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..363fb36e --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,38 @@ +name: Playwright + +on: + push: + branches: + - main + paths: + - '**/playwright/**' + pull_request: + paths: + - '**/playwright/**' + workflow_dispatch: + +env: + SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} + SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} + GITHUB_TOKEN: ${{ github.token }} + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + strategy: + matrix: + target: [ reporter, grid, saucectl ] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + - name: Install dependencies + working-directory: playwright + run: npm install + - name: Run ${{matrix.target}} tests + working-directory: playwright + run: npm run test.${{matrix.target}} diff --git a/playwright/.gitignore b/playwright/.gitignore new file mode 100644 index 00000000..6503957e --- /dev/null +++ b/playwright/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +artifacts/ +sauce-app-payload* +/test-results/ +/playwright-report/ +/playwright/.cache/ +/__assets__ +.DS_Store + diff --git a/playwright/.sauce/config.yml b/playwright/.sauce/config.yml new file mode 100644 index 00000000..5458476a --- /dev/null +++ b/playwright/.sauce/config.yml @@ -0,0 +1,22 @@ +apiVersion: v1alpha +kind: playwright +sauce: + region: us-west-1 + concurrency: 10 # Controls how many suites are executed at the same time. +playwright: + version: package.json # See https://docs.saucelabs.com/web-apps/automated-testing/playwright/#supported-testing-platforms for a list of supported versions. + configFile: playwright.config.js # See https://docs.saucelabs.com/web-apps/automated-testing/playwright/yaml/#configfile for a list of supported configuration files. +# Controls what files are available in the context of a test run (unless explicitly excluded by .sauceignore). +rootDir: ./ +suites: + - name: "Playwright Saucectl" + platformName: "macOS 13" + screenResolution: "1440x900" + testMatch: [".*.js"] + params: + browserName: "chromium" + project: "saucectl" + +reporters: + spotlight: # Prints an overview of failed or otherwise interesting jobs. + enabled: true diff --git a/playwright/.sauceignore b/playwright/.sauceignore new file mode 100644 index 00000000..ac76ecab --- /dev/null +++ b/playwright/.sauceignore @@ -0,0 +1,14 @@ +# This file instructs saucectl to not package any files mentioned here. +.git/ +.github/ +.DS_Store +.hg/ +.vscode/ +.idea/ +.gitignore +.hgignore +.gitlab-ci.yml +.npmrc +*.gif +# Remove this to have node_modules uploaded with code +node_modules/ diff --git a/playwright/LICENSE b/playwright/LICENSE new file mode 100644 index 00000000..e304480f --- /dev/null +++ b/playwright/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 Sauce Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/playwright/README.md b/playwright/README.md new file mode 100644 index 00000000..41c7dbbc --- /dev/null +++ b/playwright/README.md @@ -0,0 +1,61 @@ +# Playwright + +## Overview + +Playwright uses a low-level protocol to provide access to implementation details of the application under test. +It allows setting event listeners to take specific actions when certain criteria are met +(e.g., whenever the app requests to load an image asset, tell it to display a kitten photo instead). +It is primarily written in JavaScript, but provides support for Java, Python and C# as well. +The primary limitation currently is that it does not work with the production version of browsers, +which means tests must be executed against a modified copy of the latest open source version +of the desired browser. These custom browsers must be downloaded with each new version of Playwright. +This is especially limiting for browsers like Safari, which is quite distinct from the available open source Webkit code. + +## Playwright on Sauce Labs + +There are 3 ways to execute Playwright tests with Sauce Labs, and this project demonstrates running +the same tests with each approach. You can run these tests by cloning the repo and navigating to this directory. +Ensure you [set the environment variables](https://docs.saucelabs.com/basics/environment-variables/), +and run: +```bash +npm install +``` + +### Saucectl + +* **Purpose**: Execute Playwright tests written in JavaScript on any Sauce Labs platform configuration. +* **Use Case**: Ideal for running cross-browser/cross-platform tests without maintaining local environments. +* **Reference**: https://docs.saucelabs.com/web-apps/automated-testing/playwright/ + +For more information on the saucectl product, read the [saucectl documentation](https://docs.saucelabs.com/dev/cli/saucectl/). +For a more thorough demonstration of ways to use playwright with saucectl, take a look at the +[Saucectl Playwright Example Repo](https://github.com/saucelabs/saucectl-playwright-example). + +To execute Playwright with saucectl from this directory, run: +```bash +npm run test.saucectl +``` + +### Remote Grid + +* **Purpose**: Execute Playwright tests in any [suppported language](https://playwright.dev/docs/languages) +by connecting to a remote Selenium Grid. +* **Use Case**: Suitable for developers comfortable configuring custom code environments +and seeking detailed command execution logs. This solution is limited to testing Chrome and Microsoft Edge. +* **Reference**: https://playwright.dev/docs/selenium-grid + +To execute Playwright with a remote grid from this directory, run: +```bash +npm run test.grid +``` + +### Playwright Reporter + +* **Purpose**: Execute Playwright tests locally and display detailed reports on Sauce Labs. +* **Use Case**: Best for executing JavaScript-based Playwright tests on local machines when cross-platform testing is not needed. +* **Reference**: https://docs.saucelabs.com/web-apps/automated-testing/playwright/ + +To execute Playwright with the playwright reporter from this directory, run: +```bash +npm run test.reporter +``` diff --git a/playwright/package-lock.json b/playwright/package-lock.json new file mode 100644 index 00000000..42eb5757 --- /dev/null +++ b/playwright/package-lock.json @@ -0,0 +1,395 @@ +{ + "name": "playwright-examples", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "playwright-examples", + "version": "1.0.0", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@saucelabs/playwright-reporter": "^1.5.0" + }, + "devDependencies": { + "@playwright/test": "1.49.0", + "saucectl": "^0.186.4" + } + }, + "node_modules/@playwright/test": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0.tgz", + "integrity": "sha512-DMulbwQURa8rNIQrf94+jPJQ4FmOVdpE5ZppRNvWVjvhC+6sOeo28r8MgIpQRYouXRtt/FCCXU7zn20jnHR4Qw==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@saucelabs/bin-wrapper": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@saucelabs/bin-wrapper/-/bin-wrapper-2.1.1.tgz", + "integrity": "sha512-wzG3tx5wOmzFxdKqCUAq7aPR0zHk6PIiV+rGjTJMApyGyJtjAWSXPGzdfejio06A6sdBNkDeZXm/Fb+yR3h8Bg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "adm-zip": "^0.5.15", + "axios": "^1.7.5", + "https-proxy-agent": "^7.0.5", + "tar-stream": "^3.1.7" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + } + }, + "node_modules/@saucelabs/playwright-reporter": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@saucelabs/playwright-reporter/-/playwright-reporter-1.5.0.tgz", + "integrity": "sha512-9x596YwohFLBm0NBlNMWQGnrA3vi8WhZjQm0HtRBpctIvKsNUo2wOLvtLx6TjC2K0+8kzbG6gUe6O8kdbE9QOA==", + "license": "MIT", + "dependencies": { + "@saucelabs/sauce-json-reporter": "4.1.0", + "@saucelabs/testcomposer": "3.0.1", + "axios": "1.7.5", + "debug": "^4.3.6", + "ua-parser-js": "^1.0.39" + }, + "engines": { + "node": ">=16.13.2" + }, + "peerDependencies": { + "@playwright/test": "^1.16.3" + } + }, + "node_modules/@saucelabs/sauce-json-reporter": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@saucelabs/sauce-json-reporter/-/sauce-json-reporter-4.1.0.tgz", + "integrity": "sha512-UhqXXsaW4yRA/7v10qeCp1B7tIw09fghsNIr2wuU93XhbkqxXmMJTYItqFX66k1JJSEzHsay8Md7sTkUlJfV6Q==", + "license": "MIT", + "engines": { + "node": ">=16.13.2" + } + }, + "node_modules/@saucelabs/testcomposer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@saucelabs/testcomposer/-/testcomposer-3.0.1.tgz", + "integrity": "sha512-4Ye6v09vXsxud89YSoQ1Ag6JFUsZUTYYMZtOux/S1sUch2nN9jkhb4Itn6PxmUqwA7vjMNII8qHOPdLByEFQhA==", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.7.5", + "form-data": "^4.0.0" + }, + "engines": { + "node": ">=16.13.2" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.49.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/saucectl": { + "version": "0.186.4", + "resolved": "https://registry.npmjs.org/saucectl/-/saucectl-0.186.4.tgz", + "integrity": "sha512-vX1CHDIiKndJx0RGls3GfB7NmW/jriAizv4WLsAzOtqKQpAOPG8PJPaQOO70i26ppihfofLYk/flgnBYixA7Ew==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@saucelabs/bin-wrapper": "^2.1.0" + }, + "bin": { + "saucectl": "index.js" + }, + "engines": { + "node": ">=16.13.2" + } + }, + "node_modules/streamx": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", + "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", + "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + } + } +} diff --git a/playwright/package.json b/playwright/package.json new file mode 100644 index 00000000..b87851b1 --- /dev/null +++ b/playwright/package.json @@ -0,0 +1,33 @@ +{ + "name": "playwright-examples", + "version": "1.0.0", + "description": "Example running saucectl with playwright.", + "main": "index.js", + "directories": { + "test": "tests" + }, + "scripts": { + "postinstall": "npx playwright install", + "test.reporter": "npx playwright test --project=\"Playwright Reporter\"", + "test.grid": "npx playwright test --project=grid --reporter=list", + "test.saucectl": "saucectl run" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/saucelabs-training/demo-js.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/saucelabs-training/demo-js/issues" + }, + "homepage": "https://github.com/saucelabs-training/demo-js#readme", + "devDependencies": { + "@playwright/test": "1.49.0", + "saucectl": "^0.186.4" + }, + "dependencies": { + "@saucelabs/playwright-reporter": "^1.5.0" + } +} diff --git a/playwright/playwright.config.js b/playwright/playwright.config.js new file mode 100644 index 00000000..bbaec8eb --- /dev/null +++ b/playwright/playwright.config.js @@ -0,0 +1,69 @@ +// @ts-check +const { devices } = require('@playwright/test'); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +const isRunningInSauceLabs = process.env.SAUCE_VM; + +/** + * @see https://playwright.dev/docs/test-configuration + * @type {import('@playwright/test').PlaywrightTestConfig} + */ +const config = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* 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). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'on', + }, + + /* Use Playwright reporter when not executing with saucectl */ + reporter: isRunningInSauceLabs + ? [['html', { open: 'never', outputFolder: '__assets__/html-report/', attachmentsBaseURL: './'}]] + : [['@saucelabs/playwright-reporter'], ['list']], + + projects: [ + { + name: 'Playwright Reporter', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'saucectl', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'grid', + timeout: 120000, + use: { + ...devices['Desktop Chrome'], + }, + } + ] +}; + +module.exports = config; diff --git a/playwright/tests/authentication.spec.js b/playwright/tests/authentication.spec.js new file mode 100644 index 00000000..7d93c51b --- /dev/null +++ b/playwright/tests/authentication.spec.js @@ -0,0 +1,36 @@ +const { test } = require('./fixture'); +const {expect} = require('@playwright/test'); + +test('cancel from cart and return to inventory', async ({page}) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('input[data-test="username"]').fill('standard_user'); + await page.locator('input[data-test="password"]').fill('secret_sauce'); + await page.locator('input[data-test="login-button"]').click(); + await page.locator('.shopping_cart_link').click(); + + await page.locator('button[data-test="continue-shopping"]').click(); + + await expect(page).toHaveURL('https://www.saucedemo.com/inventory.html'); +}); + +test('sign in successfully', async ({page}) => { + await page.goto('https://www.saucedemo.com/'); + + await page.fill('[data-test="username"]', 'standard_user'); + await page.fill('[data-test="password"]', 'secret_sauce'); + await page.click('[data-test="login-button"]'); + + await expect(page).toHaveURL('https://www.saucedemo.com/inventory.html', {message: 'Login Not Successful'}); +}); + +test('logout successfully', async ({page}) => { + await page.goto('https://www.saucedemo.com/'); + await page.fill('[data-test="username"]', 'standard_user'); + await page.fill('[data-test="password"]', 'secret_sauce'); + await page.click('[data-test="login-button"]'); + + await page.click('#react-burger-menu-btn'); + await page.click('#logout_sidebar_link'); + + await expect(page).toHaveURL('https://www.saucedemo.com/', {message: 'Logout Not Successful'}); +}); \ No newline at end of file diff --git a/playwright/tests/cart.spec.js b/playwright/tests/cart.spec.js new file mode 100644 index 00000000..2a98b218 --- /dev/null +++ b/playwright/tests/cart.spec.js @@ -0,0 +1,61 @@ +const { test } = require('../tests/fixture'); +const {expect} = require('@playwright/test'); + +test('add item from product page', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('[data-test="username"]').fill('standard_user'); + await page.locator('[data-test="password"]').fill('secret_sauce'); + await page.locator('[data-test="login-button"]').click(); + + await page.locator('[data-test="add-to-cart-sauce-labs-bolt-t-shirt"]').click(); + + await expect(page.locator('.shopping_cart_badge')).toHaveText('1', { message: 'Item not correctly added to cart' }); +}); + +test('remove item from product page', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('[data-test="username"]').fill('standard_user'); + await page.locator('[data-test="password"]').fill('secret_sauce'); + await page.locator('[data-test="login-button"]').click(); + await page.locator('[data-test="add-to-cart-sauce-labs-bolt-t-shirt"]').click(); + + await page.locator('[data-test="remove-sauce-labs-bolt-t-shirt"]').click(); + + await expect(page.locator('.shopping_cart_badge')).toHaveCount(0, { message: 'Item not correctly removed from cart' }); +}); + +test('add item from inventory page', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('[data-test="username"]').fill('standard_user'); + await page.locator('[data-test="password"]').fill('secret_sauce'); + await page.locator('[data-test="login-button"]').click(); + + await page.locator('[data-test="add-to-cart-sauce-labs-onesie"]').click(); + + await expect(page.locator('.shopping_cart_badge')).toHaveText('1'); +}); + +test('remove item from inventory page', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('[data-test="username"]').fill('standard_user'); + await page.locator('[data-test="password"]').fill('secret_sauce'); + await page.locator('[data-test="login-button"]').click(); + await page.locator('[data-test="add-to-cart-sauce-labs-bike-light"]').click(); + + await page.locator('[data-test="remove-sauce-labs-bike-light"]').click(); + + await expect(page.locator('.shopping_cart_badge')).toHaveCount(0, { message: 'Shopping Cart is not empty' }); +}); + +test('remove item from cart page', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('[data-test="username"]').fill('standard_user'); + await page.locator('[data-test="password"]').fill('secret_sauce'); + await page.locator('[data-test="login-button"]').click(); + await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click(); + await page.locator('.shopping_cart_link').click(); + + await page.locator('[data-test="remove-sauce-labs-backpack"]').click(); + + await expect(page.locator('.shopping_cart_badge')).toHaveCount(0, { message: 'Shopping Cart is not empty' }); +}); diff --git a/playwright/tests/checkout.spec.js b/playwright/tests/checkout.spec.js new file mode 100644 index 00000000..e2c4c9a3 --- /dev/null +++ b/playwright/tests/checkout.spec.js @@ -0,0 +1,54 @@ +const { test } = require('./fixture'); +const {expect} = require('@playwright/test'); + +test('checkout with bad information shows error', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('[data-test="username"]').fill('standard_user'); + await page.locator('[data-test="password"]').fill('secret_sauce'); + await page.locator('[data-test="login-button"]').click(); + await page.locator('[data-test="add-to-cart-sauce-labs-onesie"]').click(); + await page.locator('.shopping_cart_link').click(); + await page.locator('[data-test="checkout"]').click(); + + await page.locator('[data-test="continue"]').click(); + + const errorClass = await page.locator('[data-test="firstName"]').getAttribute('class'); + expect(errorClass).toContain('error', { message: 'Expected error not found on page' }); +}); + +test('checkout with good information proceeds to next step', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('[data-test="username"]').fill('standard_user'); + await page.locator('[data-test="password"]').fill('secret_sauce'); + await page.locator('[data-test="login-button"]').click(); + await page.locator('[data-test="add-to-cart-sauce-labs-onesie"]').click(); + await page.locator('.shopping_cart_link').click(); + await page.locator('[data-test="checkout"]').click(); + + await page.locator('[data-test="firstName"]').fill('Luke'); + await page.locator('[data-test="lastName"]').fill('Perry'); + await page.locator('[data-test="postalCode"]').fill('90210'); + + await page.locator('[data-test="continue"]').click(); + + await expect(page).toHaveURL('https://www.saucedemo.com/checkout-step-two.html', { message: 'Information Submission Unsuccessful' }); +}); + +test('complete checkout successfully', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('[data-test="username"]').fill('standard_user'); + await page.locator('[data-test="password"]').fill('secret_sauce'); + await page.locator('[data-test="login-button"]').click(); + await page.locator('[data-test="add-to-cart-sauce-labs-onesie"]').click(); + await page.locator('.shopping_cart_link').click(); + await page.locator('[data-test="checkout"]').click(); + await page.locator('[data-test="firstName"]').fill('Luke'); + await page.locator('[data-test="lastName"]').fill('Perry'); + await page.locator('[data-test="postalCode"]').fill('90210'); + await page.locator('[data-test="continue"]').click(); + + await page.locator('[data-test="finish"]').click(); + + await expect(page).toHaveURL('https://www.saucedemo.com/checkout-complete.html'); + await expect(page.locator('.complete-text')).toBeVisible(); +}); \ No newline at end of file diff --git a/playwright/tests/fixture.js b/playwright/tests/fixture.js new file mode 100644 index 00000000..72b7a8ed --- /dev/null +++ b/playwright/tests/fixture.js @@ -0,0 +1,97 @@ +const { test: base, chromium, request } = require('@playwright/test'); + +const SAUCE_USERNAME = process.env.SAUCE_USERNAME; +const SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY; +const SAUCE_URL = 'https://ondemand.us-west-1.saucelabs.com/wd/hub/'; +const START_TIME = Date.now(); + +const test = base.extend({ + page: async ({page}, use, testInfo) => { + if (testInfo.project.name === 'grid') { + process.env.SAUCE_BUILD_NAME = process.env.SAUCE_BUILD_NAME || `Playwright Grid: ${START_TIME}`; + const remotePage = await remoteSetup(testInfo.title); + await use(remotePage); + await remoteTeardown(remotePage, testInfo.status); + } else { + await use(page); + } + }, +}, { timeout: 60000 }); + +async function getSessionPayload(testName) { + return { + capabilities: { + alwaysMatch: { + platformName: 'macOS 13', + browserName: 'Chrome', + 'sauce:options': { + username: SAUCE_USERNAME, + accessKey: SAUCE_ACCESS_KEY, + devTools: true, + _tptCommanderVersion: 'stable', + name: testName, + build: process.env.SAUCE_BUILD_NAME, + } + } + } + }; +} + +async function createSession(requestContext, testName) { + const payload = await getSessionPayload(testName); + const response = await requestContext.post(`${SAUCE_URL}session`, { + data: payload + }); + const sessionData = await response.json(); + return { + sessionId: sessionData.value.sessionId, + cdpEndpoint: sessionData.value.capabilities['se:cdp'] + }; +} + +async function remoteSetup(testName) { + const requestContext = await request.newContext({ baseURL: SAUCE_URL }); + const { sessionId, cdpEndpoint } = await createSession(requestContext, testName); + process.env.SAUCE_SESSION_ID = sessionId; + + const browserInstance = await chromium.connectOverCDP(cdpEndpoint); + const context = await browserInstance.newContext(); + + return await context.newPage(); +} + +async function remoteTeardown(page, status) { + const context = page.context(); + const browser = context.browser(); + const sessionId = process.env.SAUCE_SESSION_ID; + + await page.close(); + await browser.close(); + + await updateJobStatus(sessionId, status); + + const requestContext = await request.newContext(); + await requestContext.delete(`${SAUCE_URL}session/${sessionId}`); + + console.log(`SauceOnDemandSessionID=${sessionId}`); + console.log(`Job Link: https://app.saucelabs.com/tests/${sessionId}`); +} + +async function updateJobStatus(sessionId, status) { + const url = `https://api.us-west-1.saucelabs.com/rest/v1/${SAUCE_USERNAME}/jobs/${sessionId}`; + const auth = Buffer.from(`${SAUCE_USERNAME}:${SAUCE_ACCESS_KEY}`).toString('base64'); + + const requestContext = await request.newContext(); + await requestContext.put(url, { + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json' + }, + data: { + passed: status === 'passed' + } + }); + await requestContext.dispose(); +} + +module.exports = { test }; diff --git a/playwright/tests/navigation.spec.js b/playwright/tests/navigation.spec.js new file mode 100644 index 00000000..5d9d7dce --- /dev/null +++ b/playwright/tests/navigation.spec.js @@ -0,0 +1,59 @@ +const { test } = require('./fixture'); +const {expect} = require('@playwright/test'); + +test('cancel from cart returns to inventory', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('input[data-test="username"]').fill('standard_user'); + await page.locator('input[data-test="password"]').fill('secret_sauce'); + await page.locator('input[data-test="login-button"]').click(); + await page.locator('.shopping_cart_link').click(); + + await page.locator('button[data-test="continue-shopping"]').click(); + + await expect(page).toHaveURL('https://www.saucedemo.com/inventory.html'); +}); + +test('cancel from info page returns to cart', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('input[data-test="username"]').fill('standard_user'); + await page.locator('input[data-test="password"]').fill('secret_sauce'); + await page.locator('input[data-test="login-button"]').click(); + await page.locator('button[data-test="add-to-cart-sauce-labs-onesie"]').click(); + await page.locator('.shopping_cart_link').click(); + await page.locator('button[data-test="checkout"]').click(); + + await page.locator('button[data-test="cancel"]').click(); + + await expect(page).toHaveURL('https://www.saucedemo.com/cart.html'); +}); + +test('cancel from checkout page returns to inventory', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('input[data-test="username"]').fill('standard_user'); + await page.locator('input[data-test="password"]').fill('secret_sauce'); + await page.locator('input[data-test="login-button"]').click(); + await page.locator('button[data-test="add-to-cart-sauce-labs-onesie"]').click(); + await page.locator('.shopping_cart_link').click(); + await page.locator('button[data-test="checkout"]').click(); + await page.locator('input[data-test="firstName"]').fill('Luke'); + await page.locator('input[data-test="lastName"]').fill('Perry'); + await page.locator('input[data-test="postalCode"]').fill('90210'); + await page.locator('input[data-test="continue"]').click(); + + await page.locator('button[data-test="cancel"]').click(); + + await expect(page).toHaveURL('https://www.saucedemo.com/inventory.html'); +}); + +test('start checkout navigates to checkout page', async ({ page }) => { + await page.goto('https://www.saucedemo.com/'); + await page.locator('input[data-test="username"]').fill('standard_user'); + await page.locator('input[data-test="password"]').fill('secret_sauce'); + await page.locator('input[data-test="login-button"]').click(); + await page.locator('button[data-test="add-to-cart-sauce-labs-onesie"]').click(); + await page.locator('.shopping_cart_link').click(); + + await page.locator('button[data-test="checkout"]').click(); + + await expect(page).toHaveURL('https://www.saucedemo.com/checkout-step-one.html'); +});