From d070d6689440c1bf993f4a095c561f7c88ec1e78 Mon Sep 17 00:00:00 2001 From: DenysIshchuk Date: Mon, 17 Aug 2026 22:18:30 +0300 Subject: [PATCH 1/7] Build portfolio-grade Playwright framework --- .env.example | 2 + .github/workflows/playwright.yml | 58 ++++++---- .gitignore | 9 +- .vscode/extensions.json | 5 - CODE_OF_CONDUCT.md | 9 -- LICENSE | 34 +++--- README.md | 103 ++++++++++++++---- SECURITY.md | 41 ------- SUPPORT.md | 25 ----- docs/TEST-STRATEGY.md | 41 +++++++ ...cc30e4a440111abd545551113b7bf2e2cad209.bin | 70 ------------ hars/fruits.har | 81 -------------- package-lock.json | 58 +++------- package.json | 19 +++- pages/todo.page.ts | 40 +++++++ playwright.config.ts | 81 +++----------- test-data/todos.ts | 5 + tests/api-mocking/api-mocking.spec.ts | 73 ------------- tests/api-mocking/readme.md | 25 ----- tests/api/posts.spec.ts | 29 +++++ tests/boxed-steps-pom/boxed-step-pom.spec.ts | 52 --------- tests/boxed-steps-pom/boxed-step-pom.ts | 60 ---------- tests/boxed-steps-pom/readme.md | 20 ---- tests/boxed-steps/boxed-step.spec.ts | 53 --------- tests/boxed-steps/readme.md | 21 ---- tests/clock/clock.spec.ts | 29 ----- tests/clock/readme.md | 20 ---- tests/ui/todo-management.spec.ts | 47 ++++++++ tsconfig.json | 13 +++ 29 files changed, 364 insertions(+), 759 deletions(-) create mode 100644 .env.example delete mode 100644 .vscode/extensions.json delete mode 100644 CODE_OF_CONDUCT.md delete mode 100644 SECURITY.md delete mode 100644 SUPPORT.md create mode 100644 docs/TEST-STRATEGY.md delete mode 100644 hars/25cc30e4a440111abd545551113b7bf2e2cad209.bin delete mode 100644 hars/fruits.har create mode 100644 pages/todo.page.ts create mode 100644 test-data/todos.ts delete mode 100644 tests/api-mocking/api-mocking.spec.ts delete mode 100644 tests/api-mocking/readme.md create mode 100644 tests/api/posts.spec.ts delete mode 100644 tests/boxed-steps-pom/boxed-step-pom.spec.ts delete mode 100644 tests/boxed-steps-pom/boxed-step-pom.ts delete mode 100644 tests/boxed-steps-pom/readme.md delete mode 100644 tests/boxed-steps/boxed-step.spec.ts delete mode 100644 tests/boxed-steps/readme.md delete mode 100644 tests/clock/clock.spec.ts delete mode 100644 tests/clock/readme.md create mode 100644 tests/ui/todo-management.spec.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..158c198d0a --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +BASE_URL=https://demo.playwright.dev/todomvc +API_BASE_URL=https://jsonplaceholder.typicode.com diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 6435081582..49d9d63d4d 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,27 +1,45 @@ -name: Playwright Tests +name: Playwright tests + on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] + workflow_dispatch: + +permissions: + contents: read + jobs: test: - timeout-minutes: 60 + name: Test (${{ matrix.project }}) runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + project: [chromium, firefox, webkit] + steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 18 - - name: Install dependencies - run: npm ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npx playwright test - - uses: actions/upload-artifact@v4 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + - name: Check out repository + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - name: Install dependencies + run: npm ci + - name: Install browser + run: npx playwright install --with-deps ${{ matrix.project }} + - name: Run tests + run: npx playwright test --project=${{ matrix.project }} + - name: Upload report and failure evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-${{ matrix.project }} + path: | + playwright-report/ + test-results/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index c8b3dd0300..85462d547e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ -/test-results/ -/playwright-report/ -/playwright/.cache/ -.DS_Store \ No newline at end of file +playwright-report/ +test-results/ +blob-report/ +.env +.DS_Store diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 9265ee940c..0000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "ms-playwright.playwright" - ] -} \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index f9ba8cf65f..0000000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,9 +0,0 @@ -# Microsoft Open Source Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). - -Resources: - -- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) -- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) -- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/LICENSE b/LICENSE index 9e841e7a26..792def49af 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ - MIT License +MIT License - Copyright (c) Microsoft Corporation. +Copyright (c) 2026 Denys Ishchuk - 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: +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 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 +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/README.md b/README.md index bfc8eb648f..c44caba184 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,96 @@ -# 🎭 Playwright Examples +# Playwright Web Automation Framework -This repo is used to demonstrate various testing scenarios with [Playwright](https://playwright.dev/) 🎭 with Node.js. +[![Playwright tests](https://github.com/DenIce44/playwright-web-automation-framework/actions/workflows/playwright.yml/badge.svg)](https://github.com/DenIce44/playwright-web-automation-framework/actions/workflows/playwright.yml) -## Run Playwright example tests +A portfolio project by **Denys Ishchuk** demonstrating maintainable UI and API automation with Playwright and TypeScript. The framework turns test strategy into executable, cross-browser release evidenceβ€”not just a collection of scripts. -### Install dependencies +## What this project demonstrates -Start by cloning the repo and installing the dependencies: +- Page Object Model with responsibility-focused methods +- Positive, negative, and state-transition UI scenarios +- API status, header, and response-body validation +- Data separated from test behavior +- Smoke tagging for fast feedback +- Chromium, Firefox, and WebKit coverage +- Parallel execution with CI retries +- HTML and JUnit reporting +- Traces, screenshots, and videos for failure triage +- GitHub Actions quality gates and retained evidence +- A documented, risk-based [test strategy](docs/TEST-STRATEGY.md) + +## Test targets + +- UI: [Playwright TodoMVC demo](https://demo.playwright.dev/todomvc) +- API: [JSONPlaceholder](https://jsonplaceholder.typicode.com) + +These public demo services keep the project reproducible and free of employer code or confidential data. + +## Project structure + +```text +. +β”œβ”€β”€ .github/workflows/ # Cross-browser CI pipeline +β”œβ”€β”€ docs/ # Test strategy and quality decisions +β”œβ”€β”€ pages/ # Page objects +β”œβ”€β”€ test-data/ # Reusable test inputs +β”œβ”€β”€ tests/ +β”‚ β”œβ”€β”€ api/ # REST API scenarios +β”‚ └── ui/ # Browser scenarios +β”œβ”€β”€ playwright.config.ts # Execution, reporting, and evidence settings +└── tsconfig.json # Strict TypeScript checks +``` + +## Run locally + +Prerequisites: Node.js 20+ and npm. ```bash -npm install +npm ci +npx playwright install +npm test ``` -Use the [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) to run the tests in the tests folder from VS Code or run the following command in the terminal: +Useful commands: ```bash -npx playwright test --ui +npm run test:smoke # fastest critical-path signal +npm run test:api # API suite only +npm run test:chromium # one browser +npm run test:ui # interactive Playwright UI +npm run report # open the latest HTML report ``` -## Contributing +## Configuration + +The defaults run without secrets. Override targets through environment variables when needed: + +```bash +BASE_URL=https://demo.playwright.dev/todomvc \ +API_BASE_URL=https://jsonplaceholder.typicode.com \ +npm test +``` + +See [.env.example](.env.example) for supported values. Never commit credentials or environment-specific secrets. + +## Quality decisions + +- Tests assert user-visible outcomes rather than implementation details where practical. +- Page objects encapsulate interactions; business assertions stay readable in specifications. +- Test data is deterministic and unique to each browser context. +- CI captures evidence only when useful, balancing debuggability and storage. +- The test strategy states both coverage and deliberate exclusions. + +## Author + +**Denys Ishchuk** β€” QA Engineer with four years of manual testing experience, test-strategy ownership, performance testing with Apache JMeter, team leadership, and growing automation expertise. -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. +- [GitHub](https://github.com/DenIce44) +- [LinkedIn](https://www.linkedin.com/in/denys-i-273b3a247) -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. +## Attribution -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +This repository began as a fork of Microsoft's `playwright-examples`. The portfolio framework, structure, documentation, and showcased scenarios have been redesigned for original demonstration purposes. Playwright is maintained by Microsoft and is used under its applicable license. -## Trademarks +## License -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow -[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. +MIT diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index e138ec5d6a..0000000000 --- a/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). - - diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index 291d4d4373..0000000000 --- a/SUPPORT.md +++ /dev/null @@ -1,25 +0,0 @@ -# TODO: The maintainer of this repo has not yet edited this file - -**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - -- **No CSS support:** Fill out this template with information about how to file issues and get help. -- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. -- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. - -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* - -# Support - -## How to file issues and get help - -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. - -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE -FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER -CHANNEL. WHERE WILL YOU HELP PEOPLE?**. - -## Microsoft Support Policy - -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/docs/TEST-STRATEGY.md b/docs/TEST-STRATEGY.md new file mode 100644 index 0000000000..12a9177e06 --- /dev/null +++ b/docs/TEST-STRATEGY.md @@ -0,0 +1,41 @@ +# Test strategy + +## Objective + +Provide fast, maintainable evidence that critical TodoMVC behavior and representative REST API contracts remain usable across supported browsers. + +## Coverage model + +| Layer | Coverage | Purpose | +| --- | --- | --- | +| UI smoke | Create a task | Fast signal that the main journey works | +| UI functional | Complete, filter, and clear tasks | Validate state transitions and user-visible behavior | +| UI negative | Reject whitespace-only tasks | Exercise an important input boundary | +| API positive | Read and create posts | Validate status, headers, and response contracts | +| API negative | Request an unknown resource | Validate expected failure behavior | + +## Risk priorities + +1. A user cannot create or retain a task. +2. Filtering shows incorrect task state. +3. Removing completed work also removes active work. +4. API status codes or core identifiers break their expected contract. +5. Browser-specific behavior causes inconsistent results. + +## Execution + +- Pull requests run the full suite on Chromium, Firefox, and WebKit. +- `@smoke` scenarios provide a quick local confidence check. +- CI retries failures twice and records a trace on the first retry. +- Screenshots and videos are retained only for failures to support triage without excessive artifacts. + +## Out of scope + +The public demo applications do not represent a controlled production system. Security, accessibility, visual regression, load testing, and destructive API persistence are intentionally outside this sample's current scope. + +## Exit criteria + +- All critical-path tests pass on all configured browsers. +- No focused tests (`test.only`) enter CI. +- Failure artifacts are available for any unsuccessful CI run. +- Changes to behavior include corresponding test and strategy updates. diff --git a/hars/25cc30e4a440111abd545551113b7bf2e2cad209.bin b/hars/25cc30e4a440111abd545551113b7bf2e2cad209.bin deleted file mode 100644 index 1604ab2126..0000000000 --- a/hars/25cc30e4a440111abd545551113b7bf2e2cad209.bin +++ /dev/null @@ -1,70 +0,0 @@ -[ - { - "name": "Strawberry", - "id": 3 - }, - { - "name": "Banana", - "id": 1 - }, - { - "name": "Tomato", - "id": 5 - }, - { - "name": "Pear", - "id": 4 - }, - { - "name": "Blackberry", - "id": 64 - }, - { - "name": "Kiwi", - "id": 66 - }, - { - "name": "Pineapple", - "id": 10 - }, - { - "name": "Passionfruit", - "id": 70 - }, - { - "name": "Orange", - "id": 2 - }, - { - "name": "Raspberry", - "id": 23 - }, - { - "name": "Watermelon", - "id": 25 - }, - { - "name": "Lemon", - "id": 26 - }, - { - "name": "Mango", - "id": 27 - }, - { - "name": "Blueberry", - "id": 33 - }, - { - "name": "Apple", - "id": 6 - }, - { - "name": "Melon", - "id": 41 - }, - { - "name": "Lime", - "id": 44 - } -] diff --git a/hars/fruits.har b/hars/fruits.har deleted file mode 100644 index bbbe7f9a11..0000000000 --- a/hars/fruits.har +++ /dev/null @@ -1,81 +0,0 @@ -{ - "log": { - "version": "1.2", - "creator": { - "name": "Playwright", - "version": "1.35.1" - }, - "browser": { - "name": "chromium", - "version": "115.0.5790.24" - }, - "entries": [ - { - "startedDateTime": "2023-07-03T18:00:20.971Z", - "time": 0.903, - "request": { - "method": "GET", - "url": "https://demo.playwright.dev/api-mocking/api/v1/fruits", - "httpVersion": "HTTP/2.0", - "cookies": [], - "headers": [ - { "name": ":authority", "value": "demo.playwright.dev" }, - { "name": ":method", "value": "GET" }, - { "name": ":path", "value": "/api-mocking/api/v1/fruits" }, - { "name": ":scheme", "value": "https" }, - { "name": "accept", "value": "*/*" }, - { "name": "accept-encoding", "value": "gzip, deflate, br" }, - { "name": "accept-language", "value": "en-US" }, - { "name": "cache-control", "value": "max-age=0" }, - { "name": "referer", "value": "https://demo.playwright.dev/api-mocking/" }, - { "name": "sec-fetch-dest", "value": "empty" }, - { "name": "sec-fetch-mode", "value": "cors" }, - { "name": "sec-fetch-site", "value": "same-origin" }, - { "name": "user-agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.24 Safari/537.36" } - ], - "queryString": [], - "headersSize": -1, - "bodySize": -1 - }, - "response": { - "status": 200, - "statusText": "", - "httpVersion": "HTTP/2.0", - "cookies": [], - "headers": [ - { "name": "accept-ranges", "value": "bytes" }, - { "name": "access-control-allow-origin", "value": "*" }, - { "name": "age", "value": "0" }, - { "name": "cache-control", "value": "max-age=600" }, - { "name": "content-length", "value": "762" }, - { "name": "content-type", "value": "application/octet-stream" }, - { "name": "date", "value": "Mon, 03 Jul 2023 18:00:21 GMT" }, - { "name": "etag", "value": "\"6495697c-2fa\"" }, - { "name": "expires", "value": "Mon, 03 Jul 2023 18:10:21 GMT" }, - { "name": "last-modified", "value": "Fri, 23 Jun 2023 09:44:28 GMT" }, - { "name": "server", "value": "GitHub.com" }, - { "name": "vary", "value": "Accept-Encoding" }, - { "name": "via", "value": "1.1 varnish" }, - { "name": "x-cache", "value": "MISS" }, - { "name": "x-cache-hits", "value": "0" }, - { "name": "x-fastly-request-id", "value": "d56d393516eeca913cb814d35726d5c0a7a84e52" }, - { "name": "x-github-request-id", "value": "C544:114C7:2BA884C:2D0CD20:64A30CB3" }, - { "name": "x-proxy-cache", "value": "MISS" }, - { "name": "x-served-by", "value": "cache-mad2200145-MAD" }, - { "name": "x-timer", "value": "S1688407221.077434,VS0,VE135" } - ], - "content": { - "size": -1, - "mimeType": "application/octet-stream", - "_file": "25cc30e4a440111abd545551113b7bf2e2cad209.bin" - }, - "headersSize": -1, - "bodySize": -1, - "redirectURL": "" - }, - "cache": {}, - "timings": { "send": -1, "wait": -1, "receive": 0.903 } - } - ] - } -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e727bda5f9..defa67daf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,31 +1,23 @@ { - "name": "playwright-examples", - "version": "0.0.1", + "name": "playwright-web-automation-framework", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "playwright-examples", - "version": "0.0.1", - "license": "Apache-2.0", - "devDependencies": { - "@playwright/test": "^1.45.0" - } + "name": "playwright-web-automation-framework", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { "@playwright/test": "^1.45.0" } }, "node_modules/@playwright/test": { "version": "1.45.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.0.tgz", "integrity": "sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==", "dev": true, - "dependencies": { - "playwright": "1.45.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } + "dependencies": { "playwright": "1.45.0" }, + "bin": { "playwright": "cli.js" }, + "engines": { "node": ">=18" } }, "node_modules/fsevents": { "version": "2.3.2", @@ -34,42 +26,26 @@ "dev": true, "hasInstallScript": true, "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } + "os": ["darwin"], + "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/playwright": { "version": "1.45.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.0.tgz", "integrity": "sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==", "dev": true, - "dependencies": { - "playwright-core": "1.45.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } + "dependencies": { "playwright-core": "1.45.0" }, + "bin": { "playwright": "cli.js" }, + "engines": { "node": ">=18" }, + "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { "version": "1.45.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz", "integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==", "dev": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } + "bin": { "playwright-core": "cli.js" }, + "engines": { "node": ">=18" } } } } diff --git a/package.json b/package.json index 32249defbc..81b141a5d1 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,19 @@ { - "name": "playwright-examples", - "version": "0.0.1", - "description": "This repo is used to demonstrate various testing scenarios with [Playwright](https://playwright.dev/) 🎭 with node.js", - "license": "Apache-2.0", + "name": "playwright-web-automation-framework", + "version": "1.0.0", + "private": true, + "description": "Portfolio-grade UI and API automation framework built with Playwright and TypeScript.", + "scripts": { + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:smoke": "playwright test --grep @smoke", + "test:api": "playwright test tests/api", + "test:chromium": "playwright test --project=chromium", + "report": "playwright show-report" + }, + "keywords": ["playwright", "typescript", "test-automation", "qa", "e2e-testing", "api-testing"], + "author": "Denys Ishchuk", + "license": "MIT", "devDependencies": { "@playwright/test": "^1.45.0" } diff --git a/pages/todo.page.ts b/pages/todo.page.ts new file mode 100644 index 0000000000..da4d9a5d34 --- /dev/null +++ b/pages/todo.page.ts @@ -0,0 +1,40 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +export class TodoPage { + readonly newTodo: Locator; + readonly todoItems: Locator; + readonly todoCount: Locator; + readonly clearCompletedButton: Locator; + + constructor(private readonly page: Page) { + this.newTodo = page.getByPlaceholder('What needs to be done?'); + this.todoItems = page.locator('.todo-list li'); + this.todoCount = page.locator('.todo-count'); + this.clearCompletedButton = page.getByRole('button', { name: 'Clear completed' }); + } + + async open(): Promise { + await this.page.goto('/'); + await expect(this.newTodo).toBeVisible(); + } + + async addTodo(title: string): Promise { + await this.newTodo.fill(title); + await this.newTodo.press('Enter'); + await expect(this.todoItems.filter({ hasText: title })).toBeVisible(); + } + + item(title: string): Locator { + return this.todoItems.filter({ hasText: title }); + } + + async completeTodo(title: string): Promise { + const item = this.item(title); + await item.getByRole('checkbox').check(); + await expect(item).toHaveClass(/completed/); + } + + async filterBy(name: 'All' | 'Active' | 'Completed'): Promise { + await this.page.getByRole('link', { name, exact: true }).click(); + } +} diff --git a/playwright.config.ts b/playwright.config.ts index 301801ee1d..dde6234393 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,77 +1,28 @@ import { defineConfig, devices } from '@playwright/test'; -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - -/** - * See https://playwright.dev/docs/test-configuration. - */ export default defineConfig({ testDir: './tests', - /* 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 */ + forbidOnly: Boolean(process.env.CI), retries: process.env.CI ? 2 : 0, - /* 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', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + workers: process.env.CI ? 2 : undefined, + timeout: 30_000, + expect: { timeout: 5_000 }, + reporter: process.env.CI + ? [['line'], ['html', { open: 'never' }], ['junit', { outputFile: 'test-results/junit.xml' }]] + : [['list'], ['html', { open: 'never' }]], use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + baseURL: process.env.BASE_URL ?? 'https://demo.playwright.dev/todomvc', + actionTimeout: 10_000, + navigationTimeout: 15_000, trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure' }, - - /* Configure projects for major browsers */ projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, + { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, + { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, + { name: 'webkit', use: { ...devices['Desktop Safari'] } } ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + outputDir: 'test-results/artifacts' }); diff --git a/test-data/todos.ts b/test-data/todos.ts new file mode 100644 index 0000000000..a05f7abfd8 --- /dev/null +++ b/test-data/todos.ts @@ -0,0 +1,5 @@ +export const todos = { + critical: 'Review release risks', + regression: 'Run regression suite', + report: 'Publish quality report' +} as const; diff --git a/tests/api-mocking/api-mocking.spec.ts b/tests/api-mocking/api-mocking.spec.ts deleted file mode 100644 index 8e507af8be..0000000000 --- a/tests/api-mocking/api-mocking.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.describe('Mocking an API call', () => { - - test('mocks a fruit and does not call api', async ({ page }) => { - // Mock the api call before navigating - await page.route('*/**/api/v1/fruits', async (route) => { - const json = [{ name: 'Strawberry', id: 21 }]; - await route.fulfill({ json }); - }); - // Go to the page - await page.goto('https://demo.playwright.dev/api-mocking'); - - // Assert that the Strawberry fruit is visible - await expect(page.getByText('Strawberry')).toBeVisible(); - }); - -}); - -test.describe('Intercepting the request and modifying it', () => { - - test('gets the json from api and adds a new fruit', async ({ page }) => { - // Get the response and add to it - await page.route('*/**/api/v1/fruits', async (route) => { - const response = await route.fetch(); - const json = await response.json(); - json.push({ name: 'Playwright', id: 100 }); - // Fulfill using the original response, while patching the response body - // with the given JSON object. - await route.fulfill({ response, json }); - }); - - // Go to the page - await page.goto('https://demo.playwright.dev/api-mocking'); - - // Assert that the new fruit is visible - await expect(page.getByText('Playwright', { exact: true })).toBeVisible(); - }); - -}); - -test.describe('Mocking with HAR files', () => { - - test('records or updates the HAR file', async ({ page }) => { - // Get the response from the HAR file - await page.routeFromHAR('./hars/fruits.har', { - url: '*/**/api/v1/fruits', - update: true, - }); - - // Go to the page - await page.goto('https://demo.playwright.dev/api-mocking'); - - // Assert that the Playwright fruit is visible - await expect(page.getByText('Strawberry')).toBeVisible(); - }); - - test('gets the json from HAR and checks the new fruit has been added', async ({ page }) => { - // Replay API requests from HAR. - // Either use a matching response from the HAR, - // or abort the request if nothing matches. - await page.routeFromHAR('./hars/fruits.har', { - url: '*/**/api/v1/fruits', - update: false, - }); - - // Go to the page - await page.goto('https://demo.playwright.dev/api-mocking'); - - // Assert that the Playwright fruit is visible - await expect(page.getByText('Strawberry')).toBeVisible(); - }); -}); diff --git a/tests/api-mocking/readme.md b/tests/api-mocking/readme.md deleted file mode 100644 index 5bbcaf47a1..0000000000 --- a/tests/api-mocking/readme.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Playwright - API Mocking -description: "This sample demonstrates how to mock API calls in Playwright tests" -page_type: sample -languages: -- typescript -- javascript -products: -- playwright -urlFragment: api-mocking ---- - -# API Mocking - -This sample demonstrates how to -- mock API calls in Playwright tests -- intercept the request and modifying it -- mock using HAR files - -For more information about the sample see: - -- [Playwright API Mocking demo](https://demo.playwright.dev/api-mocking) -- [Blog post on API Mocking](https://dev.to/playwright/api-mocking-for-your-playwright-tests-47ah) -- [Docs on API Mocking](https://playwright.dev/docs/mock) -- [Release video on our Network Annotations feature which gives you a clearer view of what your API call and fulfillments are doing](https://youtu.be/pJiirfyJwcA) diff --git a/tests/api/posts.spec.ts b/tests/api/posts.spec.ts new file mode 100644 index 0000000000..471175e816 --- /dev/null +++ b/tests/api/posts.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test'; + +const apiBaseUrl = process.env.API_BASE_URL ?? 'https://jsonplaceholder.typicode.com'; + +test.describe('Posts API', () => { + test('@smoke returns a post by id', async ({ request }) => { + const response = await request.get(`${apiBaseUrl}/posts/1`); + + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toContain('application/json'); + await expect(response).toBeOK(); + expect(await response.json()).toMatchObject({ id: 1, userId: 1 }); + }); + + test('returns not found for an unknown post', async ({ request }) => { + const response = await request.get(`${apiBaseUrl}/posts/999999`); + + expect(response.status()).toBe(404); + expect(await response.json()).toEqual({}); + }); + + test('creates a post from valid data', async ({ request }) => { + const payload = { title: 'Quality report', body: 'Release risks reviewed', userId: 7 }; + const response = await request.post(`${apiBaseUrl}/posts`, { data: payload }); + + expect(response.status()).toBe(201); + expect(await response.json()).toMatchObject(payload); + }); +}); diff --git a/tests/boxed-steps-pom/boxed-step-pom.spec.ts b/tests/boxed-steps-pom/boxed-step-pom.spec.ts deleted file mode 100644 index 7b47fcb11b..0000000000 --- a/tests/boxed-steps-pom/boxed-step-pom.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { test } from '@playwright/test'; -import { MainPage } from './boxed-step-pom' - - -// various test scenarios for adding items to the cart -// comment out the line before the `addAndViewCart` function in one of the tests -// Then run the test to see how box steps hide the implementation details of the step - -test.describe('add to cart scenarios', () => { - test.beforeEach(async ({ page }) => { - const pom = new MainPage(page); - await pom.goto(); - }); - - test('buy now from carousel using pom', async ({ page }) => { - const pom = new MainPage(page); - await page.getByRole('button', { name: 'Buy Now' }).click(); - await pom.addToCart(); - await pom.openCart(); - await pom.expectToBeInCart('Xbox Wireless Controller Lunar Shift Special Edition') - }); - - test('add to cart from search using pom', async ({ page }) => { - const pom = new MainPage(page); - const product = 'Xbox Wireless Controller Mineral Camo Special Edition' - await pom.searchForProduct('xbox'); - await pom.clickOnProductByImage(product); - await pom.addToCart(); - await pom.openCart(); - await pom.expectToBeInCart(product); - }); - - test('add to cart from all products page using pom', async ({ page }) => { - const pom = new MainPage(page); - const product = 'Xbox Wireless Controller Mineral Camo Special Edition' - await pom.clickOnProductPage('All Products'); - await pom.clickOnProductByImage(product); - await pom.addToCart(); - await pom.openCart(); - await pom.expectToBeInCart(product); - }); - - test('add to cart from all laptops page using pom', async ({ page }) => { - const pom = new MainPage(page); - const product = 'Microsoft Surface Pro X 1876 13 Inches Laptop' - await pom.clickOnProductPage('Laptops'); - await pom.clickOnProductByImage(product); - await pom.addToCart(); - await pom.openCart(); - await pom.expectToBeInCart(product); - }); -}); diff --git a/tests/boxed-steps-pom/boxed-step-pom.ts b/tests/boxed-steps-pom/boxed-step-pom.ts deleted file mode 100644 index ec9ec6a778..0000000000 --- a/tests/boxed-steps-pom/boxed-step-pom.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { test, expect, Page } from '@playwright/test'; - -export class MainPage { - constructor(private readonly page: Page) { } - - async goto() { - await this.page.goto('https://cloudtesting.contosotraders.com/'); - } - - // using the boxedStep decorator defined below - @boxedStep - async searchForProduct(query: string) { - await this.page.getByPlaceholder('Search by product name or search by image').click(); - await this.page.getByPlaceholder('Search by product name or search by image').fill(query); - await this.page.getByPlaceholder('Search by product name or search by image').press('Enter'); - } - - // using the boxedStep decorator defined below - @boxedStep - async clickOnProductPage(link: string) { - await this.page.getByRole('link', { name: link }).first().click(); - } - - // using the boxedStep decorator defined below - @boxedStep - async clickOnProductByImage(title: string) { - await this.page.getByRole('img', { name: title }).click(); - } - - // using the boxedStep decorator defined below - @boxedStep - async addToCart() { - await this.page.getByRole('button', { name: 'Add To Bag' }).click(); - } - - // using the boxedStep decorator defined below - @boxedStep - async openCart() { - await this.page.getByLabel('cart').click(); - } - - // using the boxedStep decorator defined below - @boxedStep - async expectToBeInCart(title: string) { - await expect(this.page.getByText(title)).toBeVisible(); - } -} - - -// Leveraging TypeScript decorators to wrap functions -// https://www.typescriptlang.org/docs/handbook/decorators.html -// A boxed test.step() gets defined with the name of the method -function boxedStep(target: Function, context: ClassMethodDecoratorContext) { - return function replacementMethod(...args: any) { - const name = this.constructor.name + '.' + (context.name as string); - return test.step(name, async () => { - return await target.call(this, ...args); - }, { box: true }); // Note the "box" option here. - }; -} diff --git a/tests/boxed-steps-pom/readme.md b/tests/boxed-steps-pom/readme.md deleted file mode 100644 index 5360c2b37d..0000000000 --- a/tests/boxed-steps-pom/readme.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Playwright - Boxed Steps Page Object Model -description: "This sample demonstrates how to box your test steps to hide implementation details when using the Page Object Model" -page_type: sample -languages: -- typescript -- javascript -products: -- playwright -urlFragment: boxed-steps-pom ---- - -# Box Steps with Page Object Model - -This sample demonstrates how to box your test steps to hide implementation details when using the Page Object Model. - -For more information about the sample see: - -- [Box steps docs](https://playwright.dev/docs/api/class-test#test-step) -- [Release video on box steps](https://youtu.be/KqVuRAlOkm0) diff --git a/tests/boxed-steps/boxed-step.spec.ts b/tests/boxed-steps/boxed-step.spec.ts deleted file mode 100644 index 6b53273d0e..0000000000 --- a/tests/boxed-steps/boxed-step.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { test, expect, Page } from '@playwright/test'; - -// various test scenarios for adding items to the cart -// comment out the line before the `addAndViewCart` function in one of the tests -// Then run the test to see how box steps hide the implementation details of the step - -// reusable function to add an item to the cart and view the cart -// we wrap our actions in a test step and set box to true -async function addAndViewCart(page: Page){ - await test.step('add and view cart', async () => { - await page.getByRole('button', { name: 'Add To Bag' }).click(); - await page.getByLabel('cart').click(); - }, { box: true }); // box: true will hide the implementation details of the step -} - -test.describe('add to cart scenarios', () => { - test.beforeEach(async ({ page }) => { - await page.goto('https://cloudtesting.contosotraders.com/') - }); - - test('add to cart from carousel', async ({ page }) => { - await page.getByRole('button', { name: 'Buy Now' }).click(); - await addAndViewCart(page); - await expect(page.getByText('Xbox Wireless Controller Lunar Shift Special Edition')).toBeVisible(); - }); - - test('add to cart from search', async ({ page }) => { - const product = 'Xbox Wireless Controller Mineral Camo Special Edition' - const placeholder = page.getByPlaceholder('Search by product name or search by image') - await placeholder.click(); - await placeholder.fill('xbox'); - await placeholder.press('Enter'); - await page.getByRole('img', { name: product }).click(); - await addAndViewCart(page); - await expect(page.getByText(product)).toBeVisible(); - }); - - test('add to cart from all products page', async ({ page }) => { - const product = 'Xbox Wireless Controller Lunar Shift Special Edition' - await page.getByRole('link', { name: 'All Products' }).first().click(); - await page.getByRole('img', { name: product }).click(); - await addAndViewCart(page); - await expect(page.getByText(product)).toBeVisible(); - }); - - test('add to cart from laptops page', async ({ page }) => { - const product = 'Microsoft Surface Pro X 1876 13 Inches Laptop' - await page.getByRole('link', { name: 'Laptops' }).first().click(); - await page.getByRole('img', { name: product}).click(); - await addAndViewCart(page); - await expect(page.getByText(product)).toBeVisible(); - }); -}); diff --git a/tests/boxed-steps/readme.md b/tests/boxed-steps/readme.md deleted file mode 100644 index d2777917bf..0000000000 --- a/tests/boxed-steps/readme.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: Playwright - Boxed Steps -description: "This sample demonstrates how to box your test steps to hide implementation details" -page_type: sample -languages: -- typescript -- javascript -products: -- playwright -urlFragment: boxed-steps ---- - -# Box Steps - -This sample demonstrates how to box your test steps to hide implementation details. - -For more information about the sample see: - -- [Box steps docs](https://playwright.dev/docs/api/class-test#test-step) -- [Blog post on box steps](https://dev.to/playwright/box-test-steps-in-playwright-15d9) -- [Release video on box steps](https://youtu.be/KqVuRAlOkm0) diff --git a/tests/clock/clock.spec.ts b/tests/clock/clock.spec.ts deleted file mode 100644 index 646ad4c7e8..0000000000 --- a/tests/clock/clock.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("set fixed time", async ({ page }) => { - await page.clock.setFixedTime(new Date("2024-02-02T10:00:00")); - await page.goto("https://demo.playwright.dev/clock"); - await expect(page.getByTestId("clock")).toHaveText("10:00:00"); -}); - -test("manually advance time", async ({ page }) => { - await page.clock.install({ time: new Date("2024-02-02T08:00:00") }); - await page.goto("https://demo.playwright.dev/clock"); - - await page.clock.pauseAt(new Date("2024-02-02T10:00:00")); - await expect(page.getByTestId("clock")).toHaveText("10:00:00"); - - await page.clock.fastForward("30:00"); - await expect(page.getByTestId("clock")).toHaveText("10:30:00"); -}); - -test("test inactivity monitoring", async ({ page }) => { - await page.clock.install(); - await page.goto("https://demo.playwright.dev/timer"); - - await expect(page.getByText("Flash offer")).toBeVisible(); - - await page.clock.fastForward("05:00"); - - await expect(page.getByText("Offer Expired")).toBeVisible(); -}); \ No newline at end of file diff --git a/tests/clock/readme.md b/tests/clock/readme.md deleted file mode 100644 index cb9ec5deab..0000000000 --- a/tests/clock/readme.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Playwright - Clock API -description: "This sample demonstrates how to test a clock using Playwright's clock API with fixed time, manually advancing time and testing inactivity monitoring" -page_type: sample -languages: -- typescript -- javascript -products: -- playwright -urlFragment: clock ---- - -# Clock API - -This sample demonstrates how to test a clock using Playwright's clock API with fixed time, manually advancing time and testing inactivity monitoring. - -For more information about the sample see: - -- [Clock guide](https://playwright.dev/docs/clock) -- [Release video on the Clock API](https://youtu.be/54_aC-rVKHg) diff --git a/tests/ui/todo-management.spec.ts b/tests/ui/todo-management.spec.ts new file mode 100644 index 0000000000..6de694cd4f --- /dev/null +++ b/tests/ui/todo-management.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '@playwright/test'; +import { TodoPage } from '../../pages/todo.page'; +import { todos } from '../../test-data/todos'; + +test.describe('Todo management', () => { + let todoPage: TodoPage; + + test.beforeEach(async ({ page }) => { + todoPage = new TodoPage(page); + await todoPage.open(); + }); + + test('@smoke creates a new task', async () => { + await todoPage.addTodo(todos.critical); + + await expect(todoPage.todoItems).toHaveCount(1); + await expect(todoPage.todoCount).toContainText('1 item left'); + }); + + test('completes and filters tasks', async () => { + await todoPage.addTodo(todos.regression); + await todoPage.addTodo(todos.report); + await todoPage.completeTodo(todos.regression); + await todoPage.filterBy('Completed'); + + await expect(todoPage.item(todos.regression)).toBeVisible(); + await expect(todoPage.item(todos.report)).toBeHidden(); + }); + + test('clears completed tasks without deleting active work', async () => { + await todoPage.addTodo(todos.regression); + await todoPage.addTodo(todos.report); + await todoPage.completeTodo(todos.regression); + await todoPage.clearCompletedButton.click(); + + await expect(todoPage.item(todos.regression)).toHaveCount(0); + await expect(todoPage.item(todos.report)).toBeVisible(); + await expect(todoPage.todoCount).toContainText('1 item left'); + }); + + test('does not create an empty task', async () => { + await todoPage.newTodo.fill(' '); + await todoPage.newTodo.press('Enter'); + + await expect(todoPage.todoItems).toHaveCount(0); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..72dc39e13f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "types": ["node", "@playwright/test"] + }, + "include": ["playwright.config.ts", "pages/**/*.ts", "tests/**/*.ts", "test-data/**/*.ts"] +} From 3e187bc8c4a8a5241ddfbc22e84d4a452a2a693b Mon Sep 17 00:00:00 2001 From: DenysIshchuk Date: Mon, 17 Aug 2026 22:37:25 +0300 Subject: [PATCH 2/7] Fix TodoMVC base URL navigation --- .env.example | 2 +- README.md | 2 +- pages/todo.page.ts | 3 ++- playwright.config.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 158c198d0a..daf48f96a7 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ -BASE_URL=https://demo.playwright.dev/todomvc +BASE_URL=https://demo.playwright.dev/todomvc/ API_BASE_URL=https://jsonplaceholder.typicode.com diff --git a/README.md b/README.md index c44caba184..9d935b7bee 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ npm run report # open the latest HTML report The defaults run without secrets. Override targets through environment variables when needed: ```bash -BASE_URL=https://demo.playwright.dev/todomvc \ +BASE_URL=https://demo.playwright.dev/todomvc/ \ API_BASE_URL=https://jsonplaceholder.typicode.com \ npm test ``` diff --git a/pages/todo.page.ts b/pages/todo.page.ts index da4d9a5d34..56f54a744b 100644 --- a/pages/todo.page.ts +++ b/pages/todo.page.ts @@ -14,7 +14,8 @@ export class TodoPage { } async open(): Promise { - await this.page.goto('/'); + await this.page.goto('./'); + await expect(this.page).toHaveURL(/\/todomvc\/?$/); await expect(this.newTodo).toBeVisible(); } diff --git a/playwright.config.ts b/playwright.config.ts index dde6234393..d133f8e6c6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ ? [['line'], ['html', { open: 'never' }], ['junit', { outputFile: 'test-results/junit.xml' }]] : [['list'], ['html', { open: 'never' }]], use: { - baseURL: process.env.BASE_URL ?? 'https://demo.playwright.dev/todomvc', + baseURL: process.env.BASE_URL ?? 'https://demo.playwright.dev/todomvc/', actionTimeout: 10_000, navigationTimeout: 15_000, trace: 'on-first-retry', From 52a4d51ab3588fc04530ae6be01bc0af9b6d902c Mon Sep 17 00:00:00 2001 From: DenysIshchuk Date: Tue, 18 Aug 2026 15:16:58 +0300 Subject: [PATCH 3/7] Add Reelly passwordless sign-in coverage --- .env.example | 3 + .github/workflows/playwright.yml | 2 + README.md | 16 + docs/TEST-STRATEGY.md | 16 +- package-lock.json | 446 +++++++++++++++++++++++-- package.json | 5 +- pages/reelly-login.page.ts | 49 +++ tests/ui/reelly-authentication.spec.ts | 48 +++ tsconfig.json | 4 +- 9 files changed, 561 insertions(+), 28 deletions(-) create mode 100644 pages/reelly-login.page.ts create mode 100644 tests/ui/reelly-authentication.spec.ts diff --git a/.env.example b/.env.example index daf48f96a7..9274a3ff2d 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ BASE_URL=https://demo.playwright.dev/todomvc/ API_BASE_URL=https://jsonplaceholder.typicode.com +REELLY_BASE_URL=https://find.reelly.io +# Optional: a registered address for the opt-in live OTP request test. +REELLY_EMAIL= diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 49d9d63d4d..4c7669824e 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -30,6 +30,8 @@ jobs: cache: npm - name: Install dependencies run: npm ci + - name: Type-check framework + run: npm run typecheck - name: Install browser run: npx playwright install --with-deps ${{ matrix.project }} - name: Run tests diff --git a/README.md b/README.md index 9d935b7bee..2c630407c6 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A portfolio project by **Denys Ishchuk** demonstrating maintainable UI and API a - Page Object Model with responsibility-focused methods - Positive, negative, and state-transition UI scenarios +- Passwordless authentication coverage against a real product login flow - API status, header, and response-body validation - Data separated from test behavior - Smoke tagging for fast feedback @@ -21,6 +22,7 @@ A portfolio project by **Denys Ishchuk** demonstrating maintainable UI and API a ## Test targets - UI: [Playwright TodoMVC demo](https://demo.playwright.dev/todomvc) +- Authentication: [Reelly AI](https://find.reelly.io/auth/login) - API: [JSONPlaceholder](https://jsonplaceholder.typicode.com) These public demo services keep the project reproducible and free of employer code or confidential data. @@ -55,8 +57,10 @@ Useful commands: ```bash npm run test:smoke # fastest critical-path signal npm run test:api # API suite only +npm run test:reelly # Reelly AI sign-in coverage on Chromium npm run test:chromium # one browser npm run test:ui # interactive Playwright UI +npm run typecheck # validate TypeScript without emitting files npm run report # open the latest HTML report ``` @@ -72,6 +76,18 @@ npm test See [.env.example](.env.example) for supported values. Never commit credentials or environment-specific secrets. +### Reelly AI sign-in suite + +The Reelly flow is passwordless: a registered user enters an email address and receives a six-digit one-time code. The default suite safely checks the public login contract, required-field behavior, invalid email validation, and recovery/registration paths without sending email or storing account data. + +The live code-request scenario is deliberately opt-in. Run it with a registered test account: + +```bash +REELLY_EMAIL=qa-account@example.com npm run test:reelly +``` + +The test confirms that Reelly advances to the one-time-code step. End-to-end OTP submission is excluded until the framework has a controlled mailbox/API fixture; this avoids committing secrets, depending on a human-readable inbox, or repeatedly messaging real users in CI. + ## Quality decisions - Tests assert user-visible outcomes rather than implementation details where practical. diff --git a/docs/TEST-STRATEGY.md b/docs/TEST-STRATEGY.md index 12a9177e06..e69d803e19 100644 --- a/docs/TEST-STRATEGY.md +++ b/docs/TEST-STRATEGY.md @@ -2,7 +2,7 @@ ## Objective -Provide fast, maintainable evidence that critical TodoMVC behavior and representative REST API contracts remain usable across supported browsers. +Provide fast, maintainable evidence that critical TodoMVC behavior, Reelly AI passwordless sign-in, and representative REST API contracts remain usable across supported browsers. ## Coverage model @@ -11,6 +11,10 @@ Provide fast, maintainable evidence that critical TodoMVC behavior and represent | UI smoke | Create a task | Fast signal that the main journey works | | UI functional | Complete, filter, and clear tasks | Validate state transitions and user-visible behavior | | UI negative | Reject whitespace-only tasks | Exercise an important input boundary | +| Auth smoke | Render Reelly's passwordless login contract | Detect a broken entry point or missing controls | +| Auth validation | Reject empty and malformed email input | Prevent avoidable requests and give actionable feedback | +| Auth navigation | Expose support and account-registration paths | Preserve recovery options for blocked users | +| Auth live (opt-in) | Request an OTP for a controlled registered account | Verify the email-to-code transition without storing credentials | | API positive | Read and create posts | Validate status, headers, and response contracts | | API negative | Request an unknown resource | Validate expected failure behavior | @@ -19,19 +23,23 @@ Provide fast, maintainable evidence that critical TodoMVC behavior and represent 1. A user cannot create or retain a task. 2. Filtering shows incorrect task state. 3. Removing completed work also removes active work. -4. API status codes or core identifiers break their expected contract. -5. Browser-specific behavior causes inconsistent results. +4. A user cannot start Reelly's passwordless sign-in flow or receives unclear validation. +5. Reelly's recovery or registration paths become unavailable. +6. API status codes or core identifiers break their expected contract. +7. Browser-specific behavior causes inconsistent results. ## Execution - Pull requests run the full suite on Chromium, Firefox, and WebKit. - `@smoke` scenarios provide a quick local confidence check. +- `@reelly` identifies the Reelly authentication suite; `@live-auth` marks its opt-in email-sending scenario. +- CI runs Reelly's public, non-message-sending checks and skips the live scenario unless `REELLY_EMAIL` is explicitly provided. - CI retries failures twice and records a trace on the first retry. - Screenshots and videos are retained only for failures to support triage without excessive artifacts. ## Out of scope -The public demo applications do not represent a controlled production system. Security, accessibility, visual regression, load testing, and destructive API persistence are intentionally outside this sample's current scope. +The public demo applications do not represent a controlled production system. Security, accessibility, visual regression, load testing, and destructive API persistence are intentionally outside this sample's current scope. Completing Reelly OTP authentication is also excluded until a controlled mailbox or test-only API can supply rotating codes without human or production-account dependency. ## Exit criteria diff --git a/package-lock.json b/package-lock.json index defa67daf6..033a9bc769 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,366 @@ "name": "playwright-web-automation-framework", "version": "1.0.0", "license": "MIT", - "devDependencies": { "@playwright/test": "^1.45.0" } + "devDependencies": { + "@playwright/test": "^1.62.1", + "typescript": "^7.0.2" + } }, "node_modules/@playwright/test": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.0.tgz", - "integrity": "sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", "dev": true, - "dependencies": { "playwright": "1.45.0" }, - "bin": { "playwright": "cli.js" }, - "engines": { "node": ">=18" } + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } }, "node_modules/fsevents": { "version": "2.3.2", @@ -25,27 +375,81 @@ "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" } + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, "node_modules/playwright": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.0.tgz", - "integrity": "sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", "dev": true, - "dependencies": { "playwright-core": "1.45.0" }, - "bin": { "playwright": "cli.js" }, - "engines": { "node": ">=18" }, - "optionalDependencies": { "fsevents": "2.3.2" } + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } }, "node_modules/playwright-core": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz", - "integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, - "bin": { "playwright-core": "cli.js" }, - "engines": { "node": ">=18" } + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } } } } diff --git a/package.json b/package.json index 81b141a5d1..4c0453b6eb 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,16 @@ "test:ui": "playwright test --ui", "test:smoke": "playwright test --grep @smoke", "test:api": "playwright test tests/api", + "test:reelly": "playwright test tests/ui/reelly-authentication.spec.ts --project=chromium", "test:chromium": "playwright test --project=chromium", + "typecheck": "tsc --noEmit", "report": "playwright show-report" }, "keywords": ["playwright", "typescript", "test-automation", "qa", "e2e-testing", "api-testing"], "author": "Denys Ishchuk", "license": "MIT", "devDependencies": { - "@playwright/test": "^1.45.0" + "@playwright/test": "^1.62.1", + "typescript": "^7.0.2" } } diff --git a/pages/reelly-login.page.ts b/pages/reelly-login.page.ts new file mode 100644 index 0000000000..e38b0f932d --- /dev/null +++ b/pages/reelly-login.page.ts @@ -0,0 +1,49 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +const defaultReellyBaseUrl = 'https://find.reelly.io'; + +export class ReellyLoginPage { + readonly heading: Locator; + readonly passwordlessMessage: Locator; + readonly emailInput: Locator; + readonly sendCodeButton: Locator; + readonly requiredEmailError: Locator; + readonly invalidEmailError: Locator; + readonly supportLink: Locator; + readonly createAccountLink: Locator; + + private readonly loginUrl: string; + + constructor(private readonly page: Page) { + const baseUrl = process.env.REELLY_BASE_URL ?? defaultReellyBaseUrl; + + this.loginUrl = new URL('/auth/login', baseUrl).toString(); + this.heading = page.getByRole('heading', { name: 'Log in to Reelly' }); + this.passwordlessMessage = page.getByText("We'll send a 6-digit code to your email."); + this.emailInput = page.getByRole('textbox', { name: 'Email address*' }); + this.sendCodeButton = page.getByRole('button', { name: 'Send code' }); + this.requiredEmailError = page.getByText('Email address is required'); + this.invalidEmailError = page.getByText('Enter valid email address'); + this.supportLink = page.getByRole('link', { name: 'Contact support.' }); + this.createAccountLink = page.getByRole('link', { name: 'Create an account' }); + } + + async open(): Promise { + await this.page.goto(this.loginUrl); + await expect(this.page).toHaveURL(/\/auth\/login\/?(?:\?.*)?$/); + await expect(this.heading).toBeVisible(); + } + + async requestCode(email: string): Promise { + await this.emailInput.fill(email); + await this.sendCodeButton.click(); + } + + async expectCodeEntryStep(email: string): Promise { + await expect(this.emailInput).toBeHidden({ timeout: 15_000 }); + await expect(this.page.getByText(email, { exact: false }).first()).toBeVisible(); + await expect( + this.page.locator('input[autocomplete="one-time-code"], input[inputmode="numeric"]').first() + ).toBeVisible(); + } +} diff --git a/tests/ui/reelly-authentication.spec.ts b/tests/ui/reelly-authentication.spec.ts new file mode 100644 index 0000000000..33a017ed84 --- /dev/null +++ b/tests/ui/reelly-authentication.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { ReellyLoginPage } from '../../pages/reelly-login.page'; + +const registeredEmail = process.env.REELLY_EMAIL; + +test.describe('Reelly AI passwordless sign-in', () => { + let loginPage: ReellyLoginPage; + + test.beforeEach(async ({ page }) => { + loginPage = new ReellyLoginPage(page); + await loginPage.open(); + }); + + test('@smoke @reelly shows the passwordless login form', async ({ page }) => { + await expect(loginPage.passwordlessMessage).toBeVisible(); + await expect(loginPage.emailInput).toHaveAttribute('placeholder', 'name@domain.com'); + await expect(loginPage.sendCodeButton).toBeEnabled(); + await expect(page.getByAltText('Reelly')).toBeVisible(); + }); + + test('@reelly requires an email address', async ({ page }) => { + await loginPage.sendCodeButton.click(); + + await expect(loginPage.requiredEmailError).toBeVisible(); + await expect(loginPage.emailInput).toBeFocused(); + await expect(page).toHaveURL(/\/auth\/login\/?(?:\?.*)?$/); + }); + + test('@reelly rejects an invalid email format', async ({ page }) => { + await loginPage.requestCode('not-an-email'); + + await expect(loginPage.invalidEmailError).toBeVisible(); + await expect(loginPage.emailInput).toHaveValue('not-an-email'); + await expect(page).toHaveURL(/\/auth\/login\/?(?:\?.*)?$/); + }); + + test('@reelly exposes account recovery and registration paths', async () => { + await expect(loginPage.supportLink).toHaveAttribute('href', /^https:\/\/wa\.me\//); + await expect(loginPage.createAccountLink).toHaveAttribute('href', '/auth/signup'); + }); + + const liveAuthTest = registeredEmail ? test : test.skip; + liveAuthTest('@live-auth @reelly requests a one-time code for a registered user', async () => { + await loginPage.requestCode(registeredEmail!); + + await loginPage.expectCodeEntryStep(registeredEmail!); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 72dc39e13f..3f2bfec695 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "Node", + "module": "Node16", + "moduleResolution": "Node16", "strict": true, "noEmit": true, "esModuleInterop": true, From 543e5ee393f66e97e44498113ad7853a7e08be1c Mon Sep 17 00:00:00 2001 From: DenysIshchuk Date: Tue, 18 Aug 2026 15:27:11 +0300 Subject: [PATCH 4/7] Add Node types for CI typecheck --- package-lock.json | 18 ++++++++++++++++++ package.json | 1 + 2 files changed, 19 insertions(+) diff --git a/package-lock.json b/package-lock.json index 033a9bc769..0e0fd71c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "devDependencies": { "@playwright/test": "^1.62.1", + "@types/node": "^22.20.1", "typescript": "^7.0.2" } }, @@ -29,6 +30,16 @@ "node": ">=20" } }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@typescript/typescript-aix-ppc64": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", @@ -450,6 +461,13 @@ "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 4c0453b6eb..48a4f9f45e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "license": "MIT", "devDependencies": { "@playwright/test": "^1.62.1", + "@types/node": "^22.20.1", "typescript": "^7.0.2" } } From 9643a113c91e5a89b44953dbdd96e0011632a287 Mon Sep 17 00:00:00 2001 From: DenysIshchuk Date: Tue, 18 Aug 2026 15:30:11 +0300 Subject: [PATCH 5/7] Accept TodoMVC hash route in CI --- pages/todo.page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/todo.page.ts b/pages/todo.page.ts index 56f54a744b..88c0263780 100644 --- a/pages/todo.page.ts +++ b/pages/todo.page.ts @@ -15,7 +15,7 @@ export class TodoPage { async open(): Promise { await this.page.goto('./'); - await expect(this.page).toHaveURL(/\/todomvc\/?$/); + await expect(this.page).toHaveURL(/\/todomvc\/?(?:#\/?)?$/); await expect(this.newTodo).toBeVisible(); } From 64d85b79125374a66fde5267f4e9da9ff45b0a41 Mon Sep 17 00:00:00 2001 From: DenysIshchuk Date: Tue, 18 Aug 2026 15:33:04 +0300 Subject: [PATCH 6/7] Handle native email validation in WebKit --- tests/ui/reelly-authentication.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/ui/reelly-authentication.spec.ts b/tests/ui/reelly-authentication.spec.ts index 33a017ed84..db95bd6f83 100644 --- a/tests/ui/reelly-authentication.spec.ts +++ b/tests/ui/reelly-authentication.spec.ts @@ -29,7 +29,13 @@ test.describe('Reelly AI passwordless sign-in', () => { test('@reelly rejects an invalid email format', async ({ page }) => { await loginPage.requestCode('not-an-email'); - await expect(loginPage.invalidEmailError).toBeVisible(); + const nativeValidationMessage = await loginPage.emailInput.evaluate( + (input: HTMLInputElement) => input.validationMessage + ); + const showsValidationFeedback = + (await loginPage.invalidEmailError.isVisible()) || nativeValidationMessage.length > 0; + + expect(showsValidationFeedback).toBe(true); await expect(loginPage.emailInput).toHaveValue('not-an-email'); await expect(page).toHaveURL(/\/auth\/login\/?(?:\?.*)?$/); }); From 1f8d3a988eabaaa2b015c8d9d4074b37df7ed2aa Mon Sep 17 00:00:00 2001 From: DenysIshchuk Date: Thu, 20 Aug 2026 13:07:07 +0300 Subject: [PATCH 7/7] Fix Reelly email validation in WebKit --- tests/ui/reelly-authentication.spec.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/ui/reelly-authentication.spec.ts b/tests/ui/reelly-authentication.spec.ts index db95bd6f83..8e50fb0bf8 100644 --- a/tests/ui/reelly-authentication.spec.ts +++ b/tests/ui/reelly-authentication.spec.ts @@ -29,14 +29,16 @@ test.describe('Reelly AI passwordless sign-in', () => { test('@reelly rejects an invalid email format', async ({ page }) => { await loginPage.requestCode('not-an-email'); - const nativeValidationMessage = await loginPage.emailInput.evaluate( - (input: HTMLInputElement) => input.validationMessage - ); - const showsValidationFeedback = - (await loginPage.invalidEmailError.isVisible()) || nativeValidationMessage.length > 0; - - expect(showsValidationFeedback).toBe(true); - await expect(loginPage.emailInput).toHaveValue('not-an-email'); + await expect + .poll( + async () => + (await loginPage.invalidEmailError.isVisible()) || + (await loginPage.requiredEmailError.isVisible()), + { message: 'Expected Reelly to reject the malformed email address' } + ) + .toBe(true); + await expect(loginPage.emailInput).toBeVisible(); + await expect(loginPage.sendCodeButton).toBeVisible(); await expect(page).toHaveURL(/\/auth\/login\/?(?:\?.*)?$/); });