From ab2bf6cd64c4c6a2f0d2b46b064e59565ecfc458 Mon Sep 17 00:00:00 2001 From: Alphabin Date: Thu, 28 Aug 2025 17:08:14 +0530 Subject: [PATCH 01/67] Initial commit: Playwright tests with TestDino integration --- .github/workflows/test.yml | 26 ++++++++++++++------------ playwright.config.js | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47f397f..645dcff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,8 @@ name: Run Playwright tests on: + push: # runs on every push + pull_request: # runs on new PRs or PR updates schedule: - cron: '30 3 * * 1-5' workflow_dispatch: @@ -29,17 +31,17 @@ jobs: node-version: '18' - name: Create .env file run: | - echo "USERNAME=${{ secrets.USERNAME }}" >> .env - echo "USERNAME1=${{ secrets.USERNAME1 }}" >> .env - echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env - echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env - echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env - echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env - echo "CITY=${{ secrets.CITY }}" >> .env - echo "STATE=${{ secrets.STATE }}" >> .env - echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env - echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - + echo "USERNAME=${{ secrets.USERNAME }}" >> .env + echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env + echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env + echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env + echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env + echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env + echo "CITY=${{ secrets.CITY }}" >> .env + echo "STATE=${{ secrets.STATE }}" >> .env + echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env + echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env + - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -111,6 +113,6 @@ jobs: - name: Send TestDino report run: | npx --yes tdpw ./playwright-report \ - --token="trx_production_75deca4b6fbb32963853ca189506496d60835761c32e54fd9f6787b00c86158f" \ + --token="trx_production_b0b86d5f044ecfa1d02cbdfa2f9d644dd7e4889912e0494b802abfe8f251db8e" \ --upload-html \ --verbose \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index c3dd637..f6c3911 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -24,7 +24,7 @@ export default defineConfig({ use: { baseURL: 'https://demo.alphabin.co/', - headless: false, + headless: true, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', From b96de44a114b9cf02f741dd129d795caaecc4462 Mon Sep 17 00:00:00 2001 From: testdino Date: Fri, 29 Aug 2025 15:27:55 +0530 Subject: [PATCH 02/67] Initial commit --- pages/AllPages.js | 3 + pages/CartPage.js | 4 ++ pages/ContactUsPage.js | 40 +++++++++++ pages/HomePage.js | 22 +++++- pages/ProductDetailsPage.js | 52 ++++++++++++++- tests/example.spec.js | 130 +++++++++++++++++++++++++++++++++++- 6 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 pages/ContactUsPage.js diff --git a/pages/AllPages.js b/pages/AllPages.js index f9f7c91..c4c22f0 100644 --- a/pages/AllPages.js +++ b/pages/AllPages.js @@ -9,6 +9,7 @@ import CheckoutPage from "./CheckoutPage"; import OrderPage from "./OrderPage"; // Import OrderPage import UserPage from "./UserPage"; // Import UserPage import OrderDetailsPage from "./OrderDetailsPage"; +import ContactUsPage from "./ContactUsPage"; class AllPages { constructor(page) { @@ -24,6 +25,8 @@ class AllPages { this.orderPage = new OrderPage(page); // Instantiate OrderPage this.userPage = new UserPage(page); // Instantiate UserPage this.orderDetailsPage = new OrderDetailsPage(page); + this.contactUsPage = new ContactUsPage(page); + } } diff --git a/pages/CartPage.js b/pages/CartPage.js index 9de37c5..c04f233 100644 --- a/pages/CartPage.js +++ b/pages/CartPage.js @@ -124,6 +124,10 @@ class CartPage extends BasePage{ await this.page.waitForTimeout(2000); await this.page.locator(this.locators.checkoutButton).click({ force: true }); } + + async verifyIncreasedQuantity(expectedQuantity) { + await expect(this.page.locator(this.locators.cartItemQuantity)).toHaveText(expectedQuantity); + } } export default CartPage; \ No newline at end of file diff --git a/pages/ContactUsPage.js b/pages/ContactUsPage.js new file mode 100644 index 0000000..5053db5 --- /dev/null +++ b/pages/ContactUsPage.js @@ -0,0 +1,40 @@ +import BasePage from './BasePage.js'; +import { expect } from '@playwright/test'; + +class ContactUsPage extends BasePage{ + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + this.page = page; + } + + locators = { + contactUsBtn: `[data-testid="header-menu-contact-us"]`, + contactUsTitle: `[data-testid="contact-us-heading"]`, + firstNameInput: `[data-testid="contact-us-first-name-input"]`, + lastNameInput: `[data-testid="contact-us-last-name-input"]`, + subjectInput: `[data-testid="contact-us-subject-input"]`, + messageInput: `[data-testid="contact-us-message-input"]`, + sendMessageBtn: `//button[@data-testid="contact-us-submit-button"]`, + successMessage: `[data-testid="contact-us-success-message"]` + } + + async assertContactUsTitle() { + await expect(this.page.locator(this.locators.contactUsTitle)).toHaveText('Contact Us'); + } + + async fillContactUsForm() { + await this.page.fill(this.locators.firstNameInput, 'John'); + await this.page.fill(this.locators.lastNameInput, 'Doe'); + await this.page.fill(this.locators.subjectInput, 'Test Subject'); + await this.page.fill(this.locators.messageInput, 'This is a test message.'); + await this.page.click(this.locators.sendMessageBtn); + } + async verifySuccessContactUsFormSubmission() { + await expect(this.page.locator(this.locators.successMessage)).toBeVisible(); + } +} +export default ContactUsPage; \ No newline at end of file diff --git a/pages/HomePage.js b/pages/HomePage.js index 72c55b3..593898a 100644 --- a/pages/HomePage.js +++ b/pages/HomePage.js @@ -23,7 +23,8 @@ class HomePage extends BasePage{ AddCartNotification: `div[role="status"][aria-live="polite"]:has-text("Added to the cart")`, priceRangeSlider2 : `[data-testid="all-products-price-range-input-1"]`, priceRangeSlider1 : `[data-testid="all-products-price-range-input-0"]`, - filterButton : `[data-testid="all-products-filter-toggle"]` + filterButton : `[data-testid="all-products-filter-toggle"]`, + aboutUsTitle: `[data-testid="about-us-title"]`, } } @@ -88,6 +89,25 @@ class HomePage extends BasePage{ return this.page.locator(this.locators.navbar.showNowButton); } + async clickOnContactUsLink() { + await this.getContactUsNav().click(); + } + + async clickBackToHomeButton() { + await this.getHomeNav().click(); + } + + async assertHomePage() { + await expect(this.page.locator(this.locators.navbar.homeNav)).toBeVisible({ timeout: 10000 }); + } + + async clickAboutUsNav() { + await this.getAboutUsNav().click(); + } + + async assertAboutUsTitle() { + await expect(this.page.locator(this.locators.navbar.aboutUsTitle)).toBeVisible({ timeout: 10000 }); + } } export default HomePage; \ No newline at end of file diff --git a/pages/ProductDetailsPage.js b/pages/ProductDetailsPage.js index 1256cbf..748b3ef 100644 --- a/pages/ProductDetailsPage.js +++ b/pages/ProductDetailsPage.js @@ -17,7 +17,16 @@ class ProductDetailsPage extends BasePage{ addToCartButton: 'ADD TO CART', headerCartIcon: 'header-cart-icon', productAdditionalInfoTab : `[data-testid="additional-info-tab"]`, - productReviewsTab : `[data-testid="reviews-tab"]` + productReviewsTab : `[data-testid="reviews-tab"]`, + writeReviewBtn: `//button[@data-testid="write-review-button"]`, + yourNameInput: `[data-testid="review-form-name-input"]`, + emailInput: `[data-testid="review-form-email-input"]`, + ratingStars: `[data-testid="review-form-rating-4"]`, + reviewTitleInput: `[data-testid="review-form-title-input"]`, + giveYourOpinionInput: `[data-testid="review-form-review-input"]`, + submitBtn: `[data-testid="review-form-submit-button"]`, + editReviewBtn: `[data-testid="edit-review-button"]`, + deleteReviewBtn: `[data-testid="delete-review-button"]` } @@ -85,6 +94,47 @@ class ProductDetailsPage extends BasePage{ async clickCartIcon() { await this.getCartIcon().click(); } + + async clickOnWriteAReviewBtn() { + await this.page.locator(this.locators.writeReviewBtn).click(); + } + + async fillReviewForm() { + await this.page.fill(this.locators.yourNameInput, 'John Doe'); + await this.page.fill(this.locators.emailInput, 'testing@gmail.com'); + await this.page.click(this.locators.ratingStars); + await this.page.fill(this.locators.reviewTitleInput, 'Great Product'); + await this.page.fill(this.locators.giveYourOpinionInput, 'This product exceeded my expectations. Highly recommend!'); + await this.page.click(this.locators.submitBtn); + } + async assertSubmittedReview({ name, title, opinion }) { + await this.page.waitForTimeout(3000); // Wait for 1 second to ensure the form is ready + await expect(this.page.locator(`text=${name}`)).toBeVisible(); + await expect(this.page.locator(`text=${title}`)).toBeVisible(); + await expect(this.page.locator(`text=${opinion}`)).toBeVisible(); + } + + async clickOnEditReviewBtn() { + await this.page.locator(this.locators.editReviewBtn).click(); + } + + async updateReviewForm() { + await this.page.waitForTimeout(3000); // Wait for 1 second to ensure the form is ready + await this.page.fill(this.locators.reviewTitleInput, 'Updated Review Title'); + await this.page.fill(this.locators.giveYourOpinionInput, 'This is an updated review opinion.'); + await this.page.click(this.locators.submitBtn); + } + + async assertUpdatedReview({ title, opinion }) { + await this.page.waitForTimeout(3000); // Wait for 1 second to ensure the form is ready + await expect(this.page.locator(`text=${title}`)).toBeVisible(); + await expect(this.page.locator(`text=${opinion}`)).toBeVisible(); + } + + async clickOnDeleteReviewBtn() { + await this.page.locator(this.locators.deleteReviewBtn).click(); + await this.page.keyboard.press('Enter'); + } } export default ProductDetailsPage; \ No newline at end of file diff --git a/tests/example.spec.js b/tests/example.spec.js index cf0e23f..bb9660a 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -461,4 +461,132 @@ test('Verify That a New User Can Successfully Complete the Journey from Registra await allPages.checkoutPage.clickOnPlaceOrder(); await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }) -}); \ No newline at end of file +}); + +test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + await test.step('Verify that user can register successfully', async () => { + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.loginPage.validateSignInPage(); + await allPages.loginPage.clickOnSignupLink(); + await allPages.signupPage.assertSignupPage(); + await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + await allPages.signupPage.verifySuccessSignUp(); + }) + + await test.step('Verify that user can login successfully', async () => { + await allPages.loginPage.validateSignInPage(); + await allPages.loginPage.login(email, process.env.PASSWORD); + await allPages.loginPage.verifySuccessSignIn(); + await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + }) +}) + +test('Verify that user is able to fill Contact Us page successfully', async () => { + await login(); + await allPages.homePage.clickOnContactUsLink(); + await allPages.contactUsPage.assertContactUsTitle(); + await allPages.contactUsPage.fillContactUsForm(); + await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); +}); + +test('Verify that user is able to submit a product review ', async () => { + await test.step('Login as existing user and navigate to a product', async () => { + await login(); + }) + + await test.step('Navigate to all product section and select a product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + }) + + await test.step('Submit a product review and verify submission', async () => { + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + + await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + await allPages.productDetailsPage.fillReviewForm(); + await allPages.productDetailsPage.assertSubmittedReview({ + name: 'John Doe', + title: 'Great Product', + opinion: 'This product exceeded my expectations. Highly recommend!' + }); + }) +}); + +test('Verify that user can edit and delete a product review', async () => { + await test.step('Login as existing user and navigate to a product', async () => { + await login(); + }) + + await test.step('Navigate to all product section and select a product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + }) + + await test.step('Submit a product review and verify submission', async () => { + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + + await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + await allPages.productDetailsPage.fillReviewForm(); + await allPages.productDetailsPage.assertSubmittedReview({ + name: 'John Doe', + title: 'Great Product', + opinion: 'This product exceeded my expectations. Highly recommend!' + }); + }) + + await test.step('Edit the submitted review and verify changes', async () => { + await allPages.productDetailsPage.clickOnEditReviewBtn(); + await allPages.productDetailsPage.updateReviewForm(); + await allPages.productDetailsPage.assertUpdatedReview({ + title: 'Updated Review Title', + opinion: 'This is an updated review opinion.' + }) + }); + + await test.step('Delete the submitted review and verify deletion', async () => { + await allPages.productDetailsPage.clickOnDeleteReviewBtn(); + }) +}); + +test('Verify that user can purchase multiple quantities in a single order', async () => { + const productName = 'GoPro HERO10 Black'; + await login(); + await allPages.inventoryPage.clickOnShopNowButton(); + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); + + await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.verifyCartItemVisible(productName); + await allPages.cartPage.clickIncreaseQuantityButton(); + await allPages.cartPage.verifyIncreasedQuantity('3'); + await allPages.cartPage.clickOnCheckoutButton(); + await allPages.checkoutPage.verifyCheckoutTitle(); + await allPages.checkoutPage.verifyProductInCheckout(productName); + await allPages.checkoutPage.selectCashOnDelivery(); + await allPages.checkoutPage.verifyCashOnDeliverySelected(); + await allPages.checkoutPage.clickOnPlaceOrder(); + await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +}); + +test('Verify that all the navbar are working properly', async () => { + await login(); + await allPages.homePage.clickBackToHomeButton(); + // await allPages.homePage.assertHomePage(); + await allPages.homePage.clickAllProductsNav(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.homePage.clickOnContactUsLink(); + await allPages.contactUsPage.assertContactUsTitle(); + await allPages.homePage.clickAboutUsNav(); + await allPages.homePage.assertAboutUsTitle(); +}); + From 199dffdb1f0037ec8bc275abac9660db2c086303 Mon Sep 17 00:00:00 2001 From: testdino Date: Fri, 29 Aug 2025 15:31:39 +0530 Subject: [PATCH 03/67] Initial commit --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 645dcff..520ce78 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,8 +19,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1,2,3] - shardTotal: [2] + shardIndex: [1,2,3,4,5] + shardTotal: [5] steps: - uses: actions/checkout@v4 From 97516ee7f89fae87ff6f431e793905435520d065 Mon Sep 17 00:00:00 2001 From: testdino Date: Fri, 29 Aug 2025 16:58:45 +0530 Subject: [PATCH 04/67] Initial commit --- README.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3adf5e..4d0c48f 100644 --- a/README.md +++ b/README.md @@ -1 +1,97 @@ -# alphabin-demo-test-playwright \ No newline at end of file +# alphabin-demo-test-playwright + +Automated end-to-end tests for [Alphabin Demo](https://demo.alphabin.co/) using [Playwright](https://playwright.dev/). + +--- + +## Project Structure + +- `pages/` — Page Object Models +- `tests/` — Test specifications +- `playwright.config.js` — Playwright configuration +- `playwright-report/` — HTML test reports +- `.github/workflows/test.yml` — CI/CD pipeline + +--- + +## Prerequisites + +- [Node.js](https://nodejs.org/) v16+ +- [npm](https://www.npmjs.com/) + +--- + +## Installation + +```sh +npm install +``` + +--- + +## Local Test Execution + +Run all tests: +```sh +npx playwright test +``` + +View the HTML report: +```sh +npx playwright show-report +``` + +--- + +## Testdino Integration + +[Testdino](https://testdino.com/) enables cloud-based Playwright reporting. + +### Local Execution + +After your tests complete and the report is generated in `playwright-report`, upload it to Testdino: + +```sh +npx --yes tdpw ./playwright-report --token="your-token" --upload-html +``` + +Replace the token above with your own Testdino API key. + +See all available commands: +```sh +npx tdpw --help +``` + +--- + +## CI/CD Pipeline Integration + +### GitHub Actions + +Add the following step to your workflow after tests and report generation: + +```yaml +- name: Send Testdino report + run: | + npx --yes tdpw ./playwright-report --token="trx_production_035e6ed4a1a2be1f5a10eb45b837afa25b2740cc8b94ff8baca31ee3fe5e2d15" --upload-html +``` + +Ensure your API key is correctly placed in the command. + +--- + +## Continuous Integration + +Automated test runs and report merging are configured in `.github/workflows/test.yml`. + +--- + +## Contributing + +Pull requests and issues are welcome! + +--- + +## License + +MIT \ No newline at end of file From 776fe529aec05e2c0c721cdec70df15c79ad5413 Mon Sep 17 00:00:00 2001 From: testdino Date: Fri, 29 Aug 2025 18:12:11 +0530 Subject: [PATCH 05/67] added more test cases --- pages/CartPage.js | 21 +++++++++++++++++++++ playwright.config.js | 2 +- tests/example.spec.js | 17 +++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pages/CartPage.js b/pages/CartPage.js index c04f233..5b667b8 100644 --- a/pages/CartPage.js +++ b/pages/CartPage.js @@ -1,3 +1,4 @@ +import { start } from 'repl'; import BasePage from './BasePage.js'; import { expect } from '@playwright/test'; @@ -29,6 +30,9 @@ class CartPage extends BasePage{ checkoutButton: '[data-testid="checkout-button"]', viewCartButton: '[data-testid="view-cart-button"]', shoppingCartIcon: `[data-testid="header-cart-icon"]`, + deleteItemButton: '[aria-label="Remove item"]', + cartEmpty: `[data-testid="empty-cart"]`, + startShoppingButton: `[data-testid="continue-shopping-btn"]` } async assertYourCartTitle() { @@ -128,6 +132,23 @@ class CartPage extends BasePage{ async verifyIncreasedQuantity(expectedQuantity) { await expect(this.page.locator(this.locators.cartItemQuantity)).toHaveText(expectedQuantity); } + + async clickOnDeleteProductIcon() { + await this.page.locator(this.locators.deleteItemButton).click(); + } + + async verifyCartItemDeleted() { + await expect(this.page.locator(this.locators.cartItemName)).toHaveCount(0); + } + + async verifyEmptyCartMessage() { + await expect(this.page.locator(this.locators.cartEmpty)).toBeVisible(); + } + + async clickOnStartShoppingButton() { + await this.page.locator(this.locators.startShoppingButton).click(); + } + } export default CartPage; \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index f6c3911..c3dd637 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -24,7 +24,7 @@ export default defineConfig({ use: { baseURL: 'https://demo.alphabin.co/', - headless: true, + headless: false, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', diff --git a/tests/example.spec.js b/tests/example.spec.js index bb9660a..9f96433 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -590,3 +590,20 @@ test('Verify that all the navbar are working properly', async () => { await allPages.homePage.assertAboutUsTitle(); }); +test.only('Verify that user is able to delete selected product from cart', async () => { + const productName = 'GoPro HERO10 Black'; + await login(); + await allPages.inventoryPage.clickOnShopNowButton(); + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); + + await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.verifyCartItemVisible(productName); + await allPages.cartPage.clickOnDeleteProductIcon(); + await allPages.cartPage.verifyCartItemDeleted(productName); + await allPages.cartPage.verifyEmptyCartMessage(); + await allPages.cartPage.clickOnStartShoppingButton(); + await allPages.allProductsPage.assertAllProductsTitle(); +}); \ No newline at end of file From 8fb08a0b43502c597f34ae84aa2d7de18c82c99f Mon Sep 17 00:00:00 2001 From: Testdino Date: Tue, 2 Sep 2025 10:36:49 +0530 Subject: [PATCH 06/67] modified the test cases names --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 520ce78..a6019cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -112,7 +112,7 @@ jobs: - name: Send TestDino report run: | - npx --yes tdpw ./playwright-report \ - --token="trx_production_b0b86d5f044ecfa1d02cbdfa2f9d644dd7e4889912e0494b802abfe8f251db8e" \ - --upload-html \ - --verbose \ No newline at end of file + npx --yes tdpw ./playwright-report \ + --token="${{ secrets.TESTDINO_TOKEN }}" \ + --upload-html \ + --verbose From 4bd85df3a343b9319604d7ba06f67e4c5bc12969 Mon Sep 17 00:00:00 2001 From: Testdino Date: Wed, 3 Sep 2025 16:26:37 +0530 Subject: [PATCH 07/67] implement test sharding across multiple jobs for faster execution --- tests/example.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/example.spec.js b/tests/example.spec.js index bb9660a..983940e 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -11,6 +11,7 @@ test.beforeEach(async ({ page }) => { await page.goto('/'); }); + async function login(username = process.env.USERNAME, password = process.env.PASSWORD) { await allPages.loginPage.clickOnUserProfileIcon(); await allPages.loginPage.validateSignInPage(); From ce29b092e6fa6ff59990ea6fec98746cc46d7095 Mon Sep 17 00:00:00 2001 From: Testdino Date: Wed, 3 Sep 2025 16:34:07 +0530 Subject: [PATCH 08/67] Implement test sharding across multiple jobs for faster execution --- tests/example.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 983940e..bb9660a 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -11,7 +11,6 @@ test.beforeEach(async ({ page }) => { await page.goto('/'); }); - async function login(username = process.env.USERNAME, password = process.env.PASSWORD) { await allPages.loginPage.clickOnUserProfileIcon(); await allPages.loginPage.validateSignInPage(); From 7382bcc80f2655924cae02c1453f174f65642cb4 Mon Sep 17 00:00:00 2001 From: testing Date: Fri, 12 Sep 2025 12:45:59 +0530 Subject: [PATCH 09/67] changes applied in the playwright.config file --- README.md | 22 +++++++++++++++++----- playwright.config.js | 11 +++++++---- tests/example.spec.js | 2 +- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4d0c48f..95c8154 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# alphabin-demo-test-playwright +# Ecommerce demo store - Playwright (javascript) tests -Automated end-to-end tests for [Alphabin Demo](https://demo.alphabin.co/) using [Playwright](https://playwright.dev/). +Automated end-to-end tests for Ecommerce demo store using [Playwright](https://playwright.dev/). --- @@ -47,12 +47,24 @@ npx playwright show-report [Testdino](https://testdino.com/) enables cloud-based Playwright reporting. +> **Important:** +> Make sure your `playwright.config.js` includes both the HTML and JSON reporters. +> The HTML report and JSON report must be available for Testdino to process your test results. + +Example configuration: +```js +reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['json', { outputFile: './playwright-report/report.json' }], +] +``` + ### Local Execution After your tests complete and the report is generated in `playwright-report`, upload it to Testdino: ```sh -npx --yes tdpw ./playwright-report --token="your-token" --upload-html +npx --yes tdpw ./playwright-report --token="YOUR_TESTDINO_API_KEY" --upload-html ``` Replace the token above with your own Testdino API key. @@ -73,7 +85,7 @@ Add the following step to your workflow after tests and report generation: ```yaml - name: Send Testdino report run: | - npx --yes tdpw ./playwright-report --token="trx_production_035e6ed4a1a2be1f5a10eb45b837afa25b2740cc8b94ff8baca31ee3fe5e2d15" --upload-html + npx --yes tdpw ./playwright-report --token="YOUR_TESTDINO_API_KEY" --upload-html ``` Ensure your API key is correctly placed in the command. @@ -94,4 +106,4 @@ Pull requests and issues are welcome! ## License -MIT \ No newline at end of file +MIT diff --git a/playwright.config.js b/playwright.config.js index c3dd637..4b7761d 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -10,9 +10,7 @@ export default defineConfig({ retries: isCI ? 1 : 0, workers: isCI ? 1 : 1, - timeout: 60 * 1000, // ⏱️ each test fails after 1 min - // In CI we only show a list reporter. The workflow sets --reporter=blob. - // Locally you also get HTML and JSON. + timeout: 60 * 1000, reporter: [ ['html', { outputFolder: 'playwright-report', @@ -24,7 +22,7 @@ export default defineConfig({ use: { baseURL: 'https://demo.alphabin.co/', - headless: false, + headless: true, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', @@ -35,5 +33,10 @@ export default defineConfig({ name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, ], + }); \ No newline at end of file diff --git a/tests/example.spec.js b/tests/example.spec.js index 9f96433..8119a0a 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -590,7 +590,7 @@ test('Verify that all the navbar are working properly', async () => { await allPages.homePage.assertAboutUsTitle(); }); -test.only('Verify that user is able to delete selected product from cart', async () => { +test('Verify that user is able to delete selected product from cart', async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); From 7e5bc7d9e1543a9ab280dc3083f278c052cba67f Mon Sep 17 00:00:00 2001 From: testing Date: Fri, 12 Sep 2025 15:05:35 +0530 Subject: [PATCH 10/67] changes applied in the playwright.config file --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 520ce78..c9d55ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -112,7 +112,7 @@ jobs: - name: Send TestDino report run: | - npx --yes tdpw ./playwright-report \ - --token="trx_production_b0b86d5f044ecfa1d02cbdfa2f9d644dd7e4889912e0494b802abfe8f251db8e" \ - --upload-html \ - --verbose \ No newline at end of file + npx --yes tdpw ./playwright-report \ + --token="${{ secrets.TESTDINO_TOKEN }}" \ + --upload-html \ + --verbose \ No newline at end of file From 92cc7d0c0e4f6e6ac94bce014940800a4012ed20 Mon Sep 17 00:00:00 2001 From: testing Date: Fri, 12 Sep 2025 16:02:00 +0530 Subject: [PATCH 11/67] updated the yml by adding multiple browser --- .github/workflows/test.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9d55ea..ef586ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,7 @@ jobs: matrix: shardIndex: [1,2,3,4,5] shardTotal: [5] + browser: [chromium, firefox] # ✅ added browsers here steps: - uses: actions/checkout@v4 @@ -29,11 +30,11 @@ jobs: uses: actions/setup-node@v3 with: node-version: '18' + - name: Create .env file run: | echo "USERNAME=${{ secrets.USERNAME }}" >> .env echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env - echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env @@ -55,14 +56,14 @@ jobs: npm ci npx playwright install --with-deps - - name: Run shard ${{ matrix.shardIndex }} - run: npx playwright test --project=chromium --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Run shard ${{ matrix.shardIndex }} on ${{ matrix.browser }} + run: npx playwright test --project=${{ matrix.browser }} --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: blob-report-${{ matrix.shardIndex }} + name: blob-report-${{ matrix.browser }}-${{ matrix.shardIndex }} path: ./blob-report retention-days: 1 @@ -115,4 +116,4 @@ jobs: npx --yes tdpw ./playwright-report \ --token="${{ secrets.TESTDINO_TOKEN }}" \ --upload-html \ - --verbose \ No newline at end of file + --verbose From 4dd014fd11378f47bf08c2a1f63ace85b1576c51 Mon Sep 17 00:00:00 2001 From: testing Date: Fri, 12 Sep 2025 16:43:57 +0530 Subject: [PATCH 12/67] updated the yml by adding multiple browser --- .github/workflows/test.yml | 58 ++++++++++++++++++++++++++++---------- playwright.config.js | 22 +++++++++++++-- tests/example.spec.js | 40 +++++++++++++------------- 3 files changed, 82 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef586ad..a18c768 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,12 +1,9 @@ # .github/workflows/playwright-daily.yml -# Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) -# plus anytime you trigger it manually from the Actions tab. - name: Run Playwright tests on: - push: # runs on every push - pull_request: # runs on new PRs or PR updates + push: + pull_request: schedule: - cron: '30 3 * * 1-5' workflow_dispatch: @@ -19,9 +16,40 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1,2,3,4,5] - shardTotal: [5] - browser: [chromium, firefox] # ✅ added browsers here + include: + # Chromium → 25% (5 tests, 2 shards) + - browser: chromium + shardIndex: 1 + shardTotal: 8 + - browser: chromium + shardIndex: 2 + shardTotal: 8 + + # Firefox → 25% (5 tests, 2 shards) + - browser: firefox + shardIndex: 3 + shardTotal: 8 + - browser: firefox + shardIndex: 4 + shardTotal: 8 + + # WebKit → 25% (5 tests, 2 shards) + - browser: webkit + shardIndex: 5 + shardTotal: 8 + - browser: webkit + shardIndex: 6 + shardTotal: 8 + + # Android → 10% (2 tests, 1 shard) + - browser: 'Pixel 5' + shardIndex: 7 + shardTotal: 8 + + # iOS → 10% (2 tests, 1 shard) + - browser: 'iPhone 13' + shardIndex: 8 + shardTotal: 8 steps: - uses: actions/checkout@v4 @@ -42,7 +70,7 @@ jobs: echo "STATE=${{ secrets.STATE }}" >> .env echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - + - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -57,7 +85,7 @@ jobs: npx playwright install --with-deps - name: Run shard ${{ matrix.shardIndex }} on ${{ matrix.browser }} - run: npx playwright test --project=${{ matrix.browser }} --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + run: npx playwright test --project="${{ matrix.browser }}" --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report if: ${{ !cancelled() }} @@ -70,7 +98,7 @@ jobs: merge-reports: name: Merge Reports needs: run-tests - if: always() # run even if some shards fail + if: always() runs-on: ubuntu-latest steps: @@ -113,7 +141,7 @@ jobs: - name: Send TestDino report run: | - npx --yes tdpw ./playwright-report \ - --token="${{ secrets.TESTDINO_TOKEN }}" \ - --upload-html \ - --verbose + npx --yes tdpw ./playwright-report \ + --token="${{ secrets.TESTDINO_TOKEN }}" \ + --upload-html \ + --verbose diff --git a/playwright.config.js b/playwright.config.js index 4b7761d..f6d55df 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -16,7 +16,7 @@ export default defineConfig({ outputFolder: 'playwright-report', open: 'never' }], - ['blob', { outputDir: 'blob-report' }], // Use blob reporter + ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging ['json', { outputFile: './playwright-report/report.json' }], ], @@ -32,11 +32,27 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + grep: /@chromium/, // only run tests tagged @chromium }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, + grep: /@firefox/, // only run tests tagged @firefox + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + grep: /@webkit/, // only run tests tagged @webkit + }, + { + name: 'android', + use: { ...devices['Pixel 5'] }, + grep: /@android/, // only run tests tagged @android + }, + { + name: 'ios', + use: { ...devices['iPhone 12'] }, + grep: /@ios/, // only run tests tagged @ios }, ], - -}); \ No newline at end of file +}); diff --git a/tests/example.spec.js b/tests/example.spec.js index 8119a0a..184ddb5 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -28,19 +28,19 @@ async function logout() { await allPages.loginPage.clickOnLogoutButton(); } -test('Verify that user can login and logout successfully', async () => { +test('Verify that user can login and logout successfully @chromium', async () => { await login(); await logout(); }); -test('Verify that user can update personal information', async () => { +test('Verify that user can update personal information @chromium', async () => { await login(); await allPages.userPage.clickOnUserProfileIcon(); await allPages.userPage.updatePersonalInfo(); await allPages.userPage.verifyPersonalInfoUpdated(); }); -test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { +test('Verify that User Can Add, Edit, and Delete Addresses after Logging In @chromium', async () => { await login(); await test.step('Verify that user is able to add address successfully', async () => { @@ -62,7 +62,7 @@ test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', as }); }); -test('Verify that user can change password successfully', async () => { +test('Verify that user can change password successfully @chromium', async () => { await test.step('Login with existing password', async () => { await login1(); }); @@ -88,7 +88,7 @@ test('Verify that user can change password successfully', async () => { }) }); -test('Verify that the New User is able to add Addresses in the Address section', async () => { +test('Verify that the New User is able to add Addresses in the Address section @chromium', async () => { await login(); await allPages.userPage.clickOnUserProfileIcon(); await allPages.userPage.clickOnAddressTab(); @@ -97,7 +97,7 @@ test('Verify that the New User is able to add Addresses in the Address section', await allPages.userPage.fillAddressForm(); }); -test('Verify that User Can Complete the Journey from Login to Order Placement', async () => { +test('Verify that User Can Complete the Journey from Login to Order Placement @firefox', async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -117,7 +117,7 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test('Verify user can place and cancel an order', async () => { +test('Verify user can place and cancel an order @firefox', async () => { const productName = 'GoPro HERO10 Black'; const productPriceAndQuantity = '₹49,999 × 1'; const productQuantity = '1'; @@ -175,7 +175,7 @@ test('Verify user can place and cancel an order', async () => { }) }); -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', async () => { +test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement @firefox', async () => { // fresh test data const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; @@ -287,7 +287,7 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra }); }); -test('Verify that user add product to cart before logging in and then complete order after logging in', async () => { +test('Verify that user add product to cart before logging in and then complete order after logging in @firefox', async () => { await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); @@ -307,7 +307,7 @@ test('Verify that user add product to cart before logging in and then complete o }) }); -test('Verify that user can filter products by price range', async () => { +test('Verify that user can filter products by price range @firefox', async () => { await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); @@ -315,7 +315,7 @@ test('Verify that user can filter products by price range', async () => { await allPages.homePage.clickOnFilterButton(); }); -test('Verify if user can add product to wishlist, moves it to card and then checks out', async () => { +test('Verify if user can add product to wishlist, moves it to card and then checks out @webkit', async () => { await login(); await test.step('Add product to wishlistand then add to cart', async () => { @@ -339,7 +339,7 @@ test('Verify if user can add product to wishlist, moves it to card and then chec }); -test('Verify new user views and cancels an order in my orders', async () => { +test('Verify new user views and cancels an order in my orders @webkit', async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -397,7 +397,7 @@ test('Verify new user views and cancels an order in my orders', async () => { }); }); -test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { +test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement @webkit', async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -463,7 +463,7 @@ test('Verify That a New User Can Successfully Complete the Journey from Registra }) }); -test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', async () => { +test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully @webkit', async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -485,7 +485,7 @@ test('Verify that the new user is able to Sign Up, Log In, and Navigate to the H }) }) -test('Verify that user is able to fill Contact Us page successfully', async () => { +test('Verify that user is able to fill Contact Us page successfully @webkit', async () => { await login(); await allPages.homePage.clickOnContactUsLink(); await allPages.contactUsPage.assertContactUsTitle(); @@ -493,7 +493,7 @@ test('Verify that user is able to fill Contact Us page successfully', async () = await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); }); -test('Verify that user is able to submit a product review ', async () => { +test('Verify that user is able to submit a product review @andriod', async () => { await test.step('Login as existing user and navigate to a product', async () => { await login(); }) @@ -518,7 +518,7 @@ test('Verify that user is able to submit a product review ', async () => { }) }); -test('Verify that user can edit and delete a product review', async () => { +test('Verify that user can edit and delete a product review @andriod', async () => { await test.step('Login as existing user and navigate to a product', async () => { await login(); }) @@ -556,7 +556,7 @@ test('Verify that user can edit and delete a product review', async () => { }) }); -test('Verify that user can purchase multiple quantities in a single order', async () => { +test('Verify that user can purchase multiple quantities in a single order @ios', async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -578,7 +578,7 @@ test('Verify that user can purchase multiple quantities in a single order', asyn await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test('Verify that all the navbar are working properly', async () => { +test('Verify that all the navbar are working properly @ios', async () => { await login(); await allPages.homePage.clickBackToHomeButton(); // await allPages.homePage.assertHomePage(); @@ -590,7 +590,7 @@ test('Verify that all the navbar are working properly', async () => { await allPages.homePage.assertAboutUsTitle(); }); -test('Verify that user is able to delete selected product from cart', async () => { +test('Verify that user is able to delete selected product from cart @chromium', async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); From 311253583215502d06631fbfe3abeba87047fbc1 Mon Sep 17 00:00:00 2001 From: testing Date: Fri, 12 Sep 2025 17:55:29 +0530 Subject: [PATCH 13/67] updated the yml by adding multiple browser --- .github/workflows/test.yml | 83 ++++++++++++++++++-------------------- tests/example.spec.js | 4 +- 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a18c768..4b91e32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,12 @@ # .github/workflows/playwright-daily.yml +# Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) +# plus anytime you trigger it manually from the Actions tab. + name: Run Playwright tests on: - push: - pull_request: + push: # runs on every push + pull_request: # runs on new PRs or PR updates schedule: - cron: '30 3 * * 1-5' workflow_dispatch: @@ -17,39 +20,31 @@ jobs: fail-fast: false matrix: include: - # Chromium → 25% (5 tests, 2 shards) - - browser: chromium - shardIndex: 1 - shardTotal: 8 - - browser: chromium - shardIndex: 2 - shardTotal: 8 - - # Firefox → 25% (5 tests, 2 shards) - - browser: firefox - shardIndex: 3 - shardTotal: 8 - - browser: firefox - shardIndex: 4 - shardTotal: 8 - - # WebKit → 25% (5 tests, 2 shards) - - browser: webkit - shardIndex: 5 - shardTotal: 8 - - browser: webkit - shardIndex: 6 - shardTotal: 8 - - # Android → 10% (2 tests, 1 shard) - - browser: 'Pixel 5' - shardIndex: 7 - shardTotal: 8 - - # iOS → 10% (2 tests, 1 shard) - - browser: 'iPhone 13' - shardIndex: 8 - shardTotal: 8 + # chromium → shards 1-5 (5 runs) + - { project: chromium, shardIndex: 1, shardTotal: 20 } + - { project: chromium, shardIndex: 2, shardTotal: 20 } + - { project: chromium, shardIndex: 3, shardTotal: 20 } + - { project: chromium, shardIndex: 4, shardTotal: 20 } + - { project: chromium, shardIndex: 5, shardTotal: 20 } + # firefox → shards 6-10 (5 runs) + - { project: firefox, shardIndex: 6, shardTotal: 20 } + - { project: firefox, shardIndex: 7, shardTotal: 20 } + - { project: firefox, shardIndex: 8, shardTotal: 20 } + - { project: firefox, shardIndex: 9, shardTotal: 20 } + - { project: firefox, shardIndex: 10, shardTotal: 20 } + # webkit → shards 11-15 (5 runs) + - { project: webkit, shardIndex: 11, shardTotal: 20 } + - { project: webkit, shardIndex: 12, shardTotal: 20 } + - { project: webkit, shardIndex: 13, shardTotal: 20 } + - { project: webkit, shardIndex: 14, shardTotal: 20 } + - { project: webkit, shardIndex: 15, shardTotal: 20 } + # android → shards 16-17 (2 runs) + - { project: android, shardIndex: 16, shardTotal: 20 } + - { project: android, shardIndex: 17, shardTotal: 20 } + # ios → shards 18-20 (3 runs) + - { project: ios, shardIndex: 18, shardTotal: 20 } + - { project: ios, shardIndex: 19, shardTotal: 20 } + - { project: ios, shardIndex: 20, shardTotal: 20 } steps: - uses: actions/checkout@v4 @@ -70,7 +65,7 @@ jobs: echo "STATE=${{ secrets.STATE }}" >> .env echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - + - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -84,21 +79,21 @@ jobs: npm ci npx playwright install --with-deps - - name: Run shard ${{ matrix.shardIndex }} on ${{ matrix.browser }} - run: npx playwright test --project="${{ matrix.browser }}" --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Run shard ${{ matrix.shardIndex }} on ${{ matrix.project }} + run: npx playwright test --project=${{ matrix.project }} --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: blob-report-${{ matrix.browser }}-${{ matrix.shardIndex }} + name: blob-report-${{ matrix.project }}-${{ matrix.shardIndex }} path: ./blob-report retention-days: 1 merge-reports: name: Merge Reports needs: run-tests - if: always() + if: always() # run even if some shards fail runs-on: ubuntu-latest steps: @@ -141,7 +136,7 @@ jobs: - name: Send TestDino report run: | - npx --yes tdpw ./playwright-report \ - --token="${{ secrets.TESTDINO_TOKEN }}" \ - --upload-html \ - --verbose + npx --yes tdpw ./playwright-report \ + --token="${{ secrets.TESTDINO_TOKEN }}" \ + --upload-html \ + --verbose diff --git a/tests/example.spec.js b/tests/example.spec.js index 184ddb5..81553dc 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -493,7 +493,7 @@ test('Verify that user is able to fill Contact Us page successfully @webkit', as await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); }); -test('Verify that user is able to submit a product review @andriod', async () => { +test('Verify that user is able to submit a product review @android', async () => { await test.step('Login as existing user and navigate to a product', async () => { await login(); }) @@ -590,7 +590,7 @@ test('Verify that all the navbar are working properly @ios', async () => { await allPages.homePage.assertAboutUsTitle(); }); -test('Verify that user is able to delete selected product from cart @chromium', async () => { +test('Verify that user is able to delete selected product from cart @ios', async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); From a1357e7f5271bfe03e95fea33027a6a3c05cec6a Mon Sep 17 00:00:00 2001 From: testing Date: Fri, 12 Sep 2025 18:15:16 +0530 Subject: [PATCH 14/67] updated the yml by adding multiple browser --- .github/workflows/test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b91e32..d88861b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ on: jobs: run-tests: - name: Run Playwright shards + name: Run ${{ matrix.project }} shard ${{ matrix.shardIndex }}/20 runs-on: ubuntu-latest strategy: @@ -77,7 +77,10 @@ jobs: - name: Install deps + browsers run: | npm ci - npx playwright install --with-deps + npx playwright install --with-deps chromium firefox webkit + + - name: List Playwright projects (debug) + run: npx playwright list --projects | cat - name: Run shard ${{ matrix.shardIndex }} on ${{ matrix.project }} run: npx playwright test --project=${{ matrix.project }} --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} From 4e30306a2b062c72ebb94ab97286649fd75db1d1 Mon Sep 17 00:00:00 2001 From: testing Date: Fri, 12 Sep 2025 18:28:43 +0530 Subject: [PATCH 15/67] changes applied in the playwright.config file --- .github/workflows/test.yml | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d88861b..c4aa02c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,38 +13,14 @@ on: jobs: run-tests: - name: Run ${{ matrix.project }} shard ${{ matrix.shardIndex }}/20 + name: Run shard ${{ matrix.shardIndex }}/5 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - include: - # chromium → shards 1-5 (5 runs) - - { project: chromium, shardIndex: 1, shardTotal: 20 } - - { project: chromium, shardIndex: 2, shardTotal: 20 } - - { project: chromium, shardIndex: 3, shardTotal: 20 } - - { project: chromium, shardIndex: 4, shardTotal: 20 } - - { project: chromium, shardIndex: 5, shardTotal: 20 } - # firefox → shards 6-10 (5 runs) - - { project: firefox, shardIndex: 6, shardTotal: 20 } - - { project: firefox, shardIndex: 7, shardTotal: 20 } - - { project: firefox, shardIndex: 8, shardTotal: 20 } - - { project: firefox, shardIndex: 9, shardTotal: 20 } - - { project: firefox, shardIndex: 10, shardTotal: 20 } - # webkit → shards 11-15 (5 runs) - - { project: webkit, shardIndex: 11, shardTotal: 20 } - - { project: webkit, shardIndex: 12, shardTotal: 20 } - - { project: webkit, shardIndex: 13, shardTotal: 20 } - - { project: webkit, shardIndex: 14, shardTotal: 20 } - - { project: webkit, shardIndex: 15, shardTotal: 20 } - # android → shards 16-17 (2 runs) - - { project: android, shardIndex: 16, shardTotal: 20 } - - { project: android, shardIndex: 17, shardTotal: 20 } - # ios → shards 18-20 (3 runs) - - { project: ios, shardIndex: 18, shardTotal: 20 } - - { project: ios, shardIndex: 19, shardTotal: 20 } - - { project: ios, shardIndex: 20, shardTotal: 20 } + shardIndex: [1,2,3,4,5] + shardTotal: [5] steps: - uses: actions/checkout@v4 @@ -82,14 +58,14 @@ jobs: - name: List Playwright projects (debug) run: npx playwright list --projects | cat - - name: Run shard ${{ matrix.shardIndex }} on ${{ matrix.project }} - run: npx playwright test --project=${{ matrix.project }} --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Run shard ${{ matrix.shardIndex }} + run: npx playwright test --grep="@chromium|@firefox|@webkit|@android|@ios" --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: - name: blob-report-${{ matrix.project }}-${{ matrix.shardIndex }} + name: blob-report-${{ matrix.shardIndex }} path: ./blob-report retention-days: 1 From cead7c9183e134c948dce5ee30e5864a77230d71 Mon Sep 17 00:00:00 2001 From: TestDino Date: Thu, 18 Sep 2025 18:55:14 +0530 Subject: [PATCH 16/67] Merge branch 'main' into prod --- tests/example.spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 100941e..517f522 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -33,14 +33,14 @@ test('Verify that user can login and logout successfully', async () => { await logout(); }); -test('Verify that user can update personal information', async () => { +test.skip('Verify that user can update personal information', async () => { await login(); await allPages.userPage.clickOnUserProfileIcon(); await allPages.userPage.updatePersonalInfo(); await allPages.userPage.verifyPersonalInfoUpdated(); }); -test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { +test.skip('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { await login(); await test.step('Verify that user is able to add address successfully', async () => { @@ -62,7 +62,7 @@ test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', as }); }); -test('Verify that user can change password successfully', async () => { +test.skip('Verify that user can change password successfully', async () => { await test.step('Login with existing password', async () => { await login1(); }); @@ -88,7 +88,7 @@ test('Verify that user can change password successfully', async () => { }) }); -test('Verify that the New User is able to add Addresses in the Address section', async () => { +test.skip('Verify that the New User is able to add Addresses in the Address section', async () => { await login(); await allPages.userPage.clickOnUserProfileIcon(); await allPages.userPage.clickOnAddressTab(); @@ -117,7 +117,7 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test('Verify user can place and cancel an order', async () => { +test.skip('Verify user can place and cancel an order', async () => { const productName = 'GoPro HERO10 Black'; const productPriceAndQuantity = '₹49,999 × 1'; const productQuantity = '1'; @@ -397,7 +397,7 @@ test('Verify new user views and cancels an order in my orders', async () => { }); }); -test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { +test.skip('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; From f0340f877444ca183e2146b6d412c4a10113e8f3 Mon Sep 17 00:00:00 2001 From: TestDino Date: Sat, 20 Sep 2025 10:47:30 +0530 Subject: [PATCH 17/67] Merge branch 'main' into prod --- tests/example.spec.js | 370 +++++++++++++++++++++--------------------- 1 file changed, 185 insertions(+), 185 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 517f522..76e4130 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -33,69 +33,69 @@ test('Verify that user can login and logout successfully', async () => { await logout(); }); -test.skip('Verify that user can update personal information', async () => { - await login(); - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.updatePersonalInfo(); - await allPages.userPage.verifyPersonalInfoUpdated(); -}); - -test.skip('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { - await login(); - - await test.step('Verify that user is able to add address successfully', async () => { - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnAddressTab(); - await allPages.userPage.clickOnAddAddressButton(); - await allPages.userPage.fillAddressForm(); - await allPages.userPage.verifytheAddressIsAdded(); - }); - - await test.step('Verify that user is able to edit address successfully', async () => { - await allPages.userPage.clickOnEditAddressButton(); - await allPages.userPage.updateAddressForm(); - await allPages.userPage.verifytheUpdatedAddressIsAdded(); - }) - - await test.step('Verify that user is able to delete address successfully', async () => { - await allPages.userPage.clickOnDeleteAddressButton(); - }); -}); - -test.skip('Verify that user can change password successfully', async () => { - await test.step('Login with existing password', async () => { - await login1(); - }); - - await test.step('Change password and verify login with new password', async () => { - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnSecurityButton(); - await allPages.userPage.enterNewPassword(); - await allPages.userPage.enterConfirmNewPassword(); - await allPages.userPage.clickOnUpdatePasswordButton(); - await allPages.userPage.getUpdatePasswordNotification(); - }); - await test.step('Verify login with new password and revert back to original password', async () => { - // Re-login with new password - await logout(); - await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); - - // Revert back - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnSecurityButton(); - await allPages.userPage.revertPasswordBackToOriginal(); - await allPages.userPage.getUpdatePasswordNotification(); - }) -}); - -test.skip('Verify that the New User is able to add Addresses in the Address section', async () => { - await login(); - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnAddressTab(); - await allPages.userPage.clickOnAddAddressButton(); - await allPages.userPage.checkAddNewAddressMenu(); - await allPages.userPage.fillAddressForm(); -}); +// test.skip('Verify that user can update personal information', async () => { +// await login(); +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.updatePersonalInfo(); +// await allPages.userPage.verifyPersonalInfoUpdated(); +// }); + +// test.skip('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { +// await login(); + +// await test.step('Verify that user is able to add address successfully', async () => { +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.clickOnAddressTab(); +// await allPages.userPage.clickOnAddAddressButton(); +// await allPages.userPage.fillAddressForm(); +// await allPages.userPage.verifytheAddressIsAdded(); +// }); + +// await test.step('Verify that user is able to edit address successfully', async () => { +// await allPages.userPage.clickOnEditAddressButton(); +// await allPages.userPage.updateAddressForm(); +// await allPages.userPage.verifytheUpdatedAddressIsAdded(); +// }) + +// await test.step('Verify that user is able to delete address successfully', async () => { +// await allPages.userPage.clickOnDeleteAddressButton(); +// }); +// }); + +// test.skip('Verify that user can change password successfully', async () => { +// await test.step('Login with existing password', async () => { +// await login1(); +// }); + +// await test.step('Change password and verify login with new password', async () => { +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.clickOnSecurityButton(); +// await allPages.userPage.enterNewPassword(); +// await allPages.userPage.enterConfirmNewPassword(); +// await allPages.userPage.clickOnUpdatePasswordButton(); +// await allPages.userPage.getUpdatePasswordNotification(); +// }); +// await test.step('Verify login with new password and revert back to original password', async () => { +// // Re-login with new password +// await logout(); +// await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); + +// // Revert back +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.clickOnSecurityButton(); +// await allPages.userPage.revertPasswordBackToOriginal(); +// await allPages.userPage.getUpdatePasswordNotification(); +// }) +// }); + +// test.skip('Verify that the New User is able to add Addresses in the Address section', async () => { +// await login(); +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.clickOnAddressTab(); +// await allPages.userPage.clickOnAddAddressButton(); +// await allPages.userPage.checkAddNewAddressMenu(); +// await allPages.userPage.fillAddressForm(); +// }); test('Verify that User Can Complete the Journey from Login to Order Placement', async () => { const productName = 'GoPro HERO10 Black'; @@ -117,63 +117,63 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test.skip('Verify user can place and cancel an order', async () => { - const productName = 'GoPro HERO10 Black'; - const productPriceAndQuantity = '₹49,999 × 1'; - const productQuantity = '1'; - const orderStatusProcessing = 'Processing'; - const orderStatusCanceled = 'Canceled'; - - await test.step('Verify that user can login successfully', async () => { - await login(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - }) - - await test.step('Add product to cart and checkout', async () => { - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnCheckoutButton(); - }) - - await test.step('Place order and click on continue shopping', async () => { - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.checkoutPage.verifyOrderItemName(productName); - await allPages.inventoryPage.clickOnContinueShopping(); - }) - - await test.step('Verify order in My Orders', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.verifyMyOrdersTitle(); - await allPages.orderPage.clickOnPaginationButton(2); - await allPages.orderPage.verifyProductInOrderList(productName); - await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); - await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); - await allPages.orderPage.clickOnPaginationButton(1); - await allPages.orderPage.clickViewDetailsButton(1); - await allPages.orderPage.verifyOrderDetailsTitle(); - await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); - }) - - await test.step('Cancel order and verify status is updated to Canceled', async () => { - await allPages.orderPage.clickCancelOrderButton(2); - await allPages.orderPage.confirmCancellation(); - await allPages.orderPage.verifyCancellationConfirmationMessage(); - await allPages.orderPage.verifyMyOrdersCount(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.verifyMyOrdersTitle(); - await allPages.orderPage.clickOnPaginationButton(2); - await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); - }) -}); +// test.skip('Verify user can place and cancel an order', async () => { +// const productName = 'GoPro HERO10 Black'; +// const productPriceAndQuantity = '₹49,999 × 1'; +// const productQuantity = '1'; +// const orderStatusProcessing = 'Processing'; +// const orderStatusCanceled = 'Canceled'; + +// await test.step('Verify that user can login successfully', async () => { +// await login(); +// await allPages.inventoryPage.clickOnAllProductsLink(); +// await allPages.inventoryPage.searchProduct(productName); +// await allPages.inventoryPage.verifyProductTitleVisible(productName); +// await allPages.inventoryPage.clickOnAddToCartIcon(); +// }) + +// await test.step('Add product to cart and checkout', async () => { +// await allPages.cartPage.clickOnCartIcon(); +// await allPages.cartPage.verifyCartItemVisible(productName); +// await allPages.cartPage.clickOnCheckoutButton(); +// }) + +// await test.step('Place order and click on continue shopping', async () => { +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.verifyProductInCheckout(productName); +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCashOnDeliverySelected(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +// await allPages.checkoutPage.verifyOrderItemName(productName); +// await allPages.inventoryPage.clickOnContinueShopping(); +// }) + +// await test.step('Verify order in My Orders', async () => { +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.orderPage.clickOnMyOrdersTab(); +// await allPages.orderPage.verifyMyOrdersTitle(); +// await allPages.orderPage.clickOnPaginationButton(2); +// await allPages.orderPage.verifyProductInOrderList(productName); +// await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); +// await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); +// await allPages.orderPage.clickOnPaginationButton(1); +// await allPages.orderPage.clickViewDetailsButton(1); +// await allPages.orderPage.verifyOrderDetailsTitle(); +// await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); +// }) + +// await test.step('Cancel order and verify status is updated to Canceled', async () => { +// await allPages.orderPage.clickCancelOrderButton(2); +// await allPages.orderPage.confirmCancellation(); +// await allPages.orderPage.verifyCancellationConfirmationMessage(); +// await allPages.orderPage.verifyMyOrdersCount(); +// await allPages.orderPage.clickOnMyOrdersTab(); +// await allPages.orderPage.verifyMyOrdersTitle(); +// await allPages.orderPage.clickOnPaginationButton(2); +// await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); +// }) +// }); test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', async () => { // fresh test data @@ -397,69 +397,69 @@ test('Verify new user views and cancels an order in my orders', async () => { }); }); -test.skip('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - let productName= `Rode NT1-A Condenser Mic`; - - await test.step('Verify that user can register successfully', async () => { - // Signup - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - // Login as new user - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) - - await test.step('Navigate to All Products and add view details of a random product', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); - await allPages.productDetailsPage.clickOnAdditionalInfoTab(); - await allPages.productDetailsPage.assertAdditionalInfoTab(); - }) - - await test.step('Add product to cart, change quantity, add new address and checkout', async () => { - await allPages.productDetailsPage.clickAddToCartButton(); - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.clickIncreaseQuantityButton(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.checkoutPage.verifyOrderConfirmedTitle(); - await allPages.checkoutPage.clickOnContinueShoppingButton(); - }) - - await test.step('Add another product to cart, select existing address and checkout', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickAddToCartButton(); - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) -}); +// test.skip('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { +// const email = `test+${Date.now()}@test.com`; +// const firstName = 'Test'; +// const lastName = 'User'; + +// let productName= `Rode NT1-A Condenser Mic`; + +// await test.step('Verify that user can register successfully', async () => { +// // Signup +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.clickOnSignupLink(); +// await allPages.signupPage.assertSignupPage(); +// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); +// await allPages.signupPage.verifySuccessSignUp(); +// }) + +// await test.step('Verify that user can login successfully', async () => { +// // Login as new user +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.login(email, process.env.PASSWORD); +// await allPages.loginPage.verifySuccessSignIn(); +// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); +// }) + +// await test.step('Navigate to All Products and add view details of a random product', async () => { +// await allPages.homePage.clickOnShopNowButton(); +// await allPages.allProductsPage.assertAllProductsTitle(); +// await allPages.allProductsPage.clickNthProduct(1); +// await allPages.productDetailsPage.clickOnReviewsTab(); +// await allPages.productDetailsPage.assertReviewsTab(); +// await allPages.productDetailsPage.clickOnAdditionalInfoTab(); +// await allPages.productDetailsPage.assertAdditionalInfoTab(); +// }) + +// await test.step('Add product to cart, change quantity, add new address and checkout', async () => { +// await allPages.productDetailsPage.clickAddToCartButton(); +// await allPages.productDetailsPage.clickCartIcon(); +// await allPages.cartPage.clickIncreaseQuantityButton(); +// await allPages.cartPage.clickOnCheckoutButton(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCashOnDeliverySelected(); +// await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); +// await allPages.checkoutPage.clickSaveAddressButton(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +// await allPages.checkoutPage.verifyOrderConfirmedTitle(); +// await allPages.checkoutPage.clickOnContinueShoppingButton(); +// }) + +// await test.step('Add another product to cart, select existing address and checkout', async () => { +// await allPages.homePage.clickOnShopNowButton(); +// await allPages.allProductsPage.assertAllProductsTitle(); +// await allPages.allProductsPage.clickNthProduct(1); +// await allPages.productDetailsPage.clickAddToCartButton(); +// await allPages.productDetailsPage.clickCartIcon(); +// await allPages.cartPage.clickOnCheckoutButton(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCashOnDeliverySelected(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +// }) +// }); From deb600e5bd0e4a97b270421e8e336870e30bbcbf Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:32:37 +0530 Subject: [PATCH 18/67] Updated some of the test cases --- tests/example.spec.js | 273 ++++++++++++++++++++++++++++++++---------- 1 file changed, 208 insertions(+), 65 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 76e4130..89dbdf6 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -28,76 +28,80 @@ async function logout() { await allPages.loginPage.clickOnLogoutButton(); } -test('Verify that user can login and logout successfully', async () => { +test('Verify that user can login and logout successfully @chromium', async () => { await login(); await logout(); }); -// test.skip('Verify that user can update personal information', async () => { -// await login(); -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.updatePersonalInfo(); -// await allPages.userPage.verifyPersonalInfoUpdated(); -// }); - -// test.skip('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { -// await login(); +test('Verify that all the navbar are working properly @chromium', async () => { + await login(); + await allPages.homePage.clickBackToHomeButton(); + // await allPages.homePage.assertHomePage(); + await allPages.homePage.clickAllProductsNav(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.homePage.clickOnContactUsLink(); + await allPages.contactUsPage.assertContactUsTitle(); + await allPages.homePage.clickAboutUsNav(); + await allPages.homePage.assertAboutUsTitle(); +}); -// await test.step('Verify that user is able to add address successfully', async () => { -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnAddressTab(); -// await allPages.userPage.clickOnAddAddressButton(); -// await allPages.userPage.fillAddressForm(); -// await allPages.userPage.verifytheAddressIsAdded(); -// }); +test('Verify that user is able to delete selected product from cart @chroium', async () => { + const productName = 'GoPro HERO10 Black'; + await login(); + await allPages.inventoryPage.clickOnShopNowButton(); + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); -// await test.step('Verify that user is able to edit address successfully', async () => { -// await allPages.userPage.clickOnEditAddressButton(); -// await allPages.userPage.updateAddressForm(); -// await allPages.userPage.verifytheUpdatedAddressIsAdded(); -// }) + await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.verifyCartItemVisible(productName); + await allPages.cartPage.clickOnDeleteProductIcon(); + await allPages.cartPage.verifyCartItemDeleted(productName); + await allPages.cartPage.verifyEmptyCartMessage(); + await allPages.cartPage.clickOnStartShoppingButton(); + await allPages.allProductsPage.assertAllProductsTitle(); +}); -// await test.step('Verify that user is able to delete address successfully', async () => { -// await allPages.userPage.clickOnDeleteAddressButton(); -// }); -// }); +test('Verify that user can edit and delete a product review @chromium', async () => { + await test.step('Login as existing user and navigate to a product', async () => { + await login(); + }) -// test.skip('Verify that user can change password successfully', async () => { -// await test.step('Login with existing password', async () => { -// await login1(); -// }); + await test.step('Navigate to all product section and select a product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + }) -// await test.step('Change password and verify login with new password', async () => { -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnSecurityButton(); -// await allPages.userPage.enterNewPassword(); -// await allPages.userPage.enterConfirmNewPassword(); -// await allPages.userPage.clickOnUpdatePasswordButton(); -// await allPages.userPage.getUpdatePasswordNotification(); -// }); -// await test.step('Verify login with new password and revert back to original password', async () => { -// // Re-login with new password -// await logout(); -// await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); + await test.step('Submit a product review and verify submission', async () => { + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + + await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + await allPages.productDetailsPage.fillReviewForm(); + await allPages.productDetailsPage.assertSubmittedReview({ + name: 'John Doe', + title: 'Great Product', + opinion: 'This product exceeded my expectations. Highly recommend!' + }); + }) -// // Revert back -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnSecurityButton(); -// await allPages.userPage.revertPasswordBackToOriginal(); -// await allPages.userPage.getUpdatePasswordNotification(); -// }) -// }); + await test.step('Edit the submitted review and verify changes', async () => { + await allPages.productDetailsPage.clickOnEditReviewBtn(); + await allPages.productDetailsPage.updateReviewForm(); + await allPages.productDetailsPage.assertUpdatedReview({ + title: 'Updated Review Title', + opinion: 'This is an updated review opinion.' + }) + }); -// test.skip('Verify that the New User is able to add Addresses in the Address section', async () => { -// await login(); -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnAddressTab(); -// await allPages.userPage.clickOnAddAddressButton(); -// await allPages.userPage.checkAddNewAddressMenu(); -// await allPages.userPage.fillAddressForm(); -// }); + await test.step('Delete the submitted review and verify deletion', async () => { + await allPages.productDetailsPage.clickOnDeleteReviewBtn(); + }) +}); -test('Verify that User Can Complete the Journey from Login to Order Placement', async () => { +test('Verify that User Can Complete the Journey from Login to Order Placement @firefox', async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -117,7 +121,7 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -// test.skip('Verify user can place and cancel an order', async () => { +// test('Verify user can place and cancel an order @firefox', async () => { // const productName = 'GoPro HERO10 Black'; // const productPriceAndQuantity = '₹49,999 × 1'; // const productQuantity = '1'; @@ -175,7 +179,7 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', // }) // }); -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', async () => { +test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement @firefox', async () => { // fresh test data const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; @@ -287,7 +291,7 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra }); }); -test('Verify that user add product to cart before logging in and then complete order after logging in', async () => { +test('Verify that user add product to cart before logging in and then complete order after logging in @firefox', async () => { await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); @@ -307,7 +311,7 @@ test('Verify that user add product to cart before logging in and then complete o }) }); -test('Verify that user can filter products by price range', async () => { +test('Verify that user can filter products by price range @firefox', async () => { await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); @@ -315,7 +319,7 @@ test('Verify that user can filter products by price range', async () => { await allPages.homePage.clickOnFilterButton(); }); -test('Verify if user can add product to wishlist, moves it to card and then checks out', async () => { +test('Verify if user can add product to wishlist, moves it to card and then checks out @webkit', async () => { await login(); await test.step('Add product to wishlistand then add to cart', async () => { @@ -339,7 +343,7 @@ test('Verify if user can add product to wishlist, moves it to card and then chec }); -test('Verify new user views and cancels an order in my orders', async () => { +test('Verify new user views and cancels an order in my orders @webkit', async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -397,7 +401,7 @@ test('Verify new user views and cancels an order in my orders', async () => { }); }); -// test.skip('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { +// test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement @webkit', async () => { // const email = `test+${Date.now()}@test.com`; // const firstName = 'Test'; // const lastName = 'User'; @@ -463,3 +467,142 @@ test('Verify new user views and cancels an order in my orders', async () => { // }) // }); +test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully @webkit', async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + await test.step('Verify that user can register successfully', async () => { + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.loginPage.validateSignInPage(); + await allPages.loginPage.clickOnSignupLink(); + await allPages.signupPage.assertSignupPage(); + await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + await allPages.signupPage.verifySuccessSignUp(); + }) + + await test.step('Verify that user can login successfully', async () => { + await allPages.loginPage.validateSignInPage(); + await allPages.loginPage.login(email, process.env.PASSWORD); + await allPages.loginPage.verifySuccessSignIn(); + await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + }) +}) + +test('Verify that user is able to fill Contact Us page successfully @webkit', async () => { + await login(); + await allPages.homePage.clickOnContactUsLink(); + await allPages.contactUsPage.assertContactUsTitle(); + await allPages.contactUsPage.fillContactUsForm(); + await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); +}); + +test('Verify that user is able to submit a product review @chromium', async () => { + await test.step('Login as existing user and navigate to a product', async () => { + await login(); + }) + + await test.step('Navigate to all product section and select a product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + }) + + await test.step('Submit a product review and verify submission', async () => { + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + + await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + await allPages.productDetailsPage.fillReviewForm(); + await allPages.productDetailsPage.assertSubmittedReview({ + name: 'John Doe', + title: 'Great Product', + opinion: 'This product exceeded my expectations. Highly recommend!' + }); + }) +}); + +// test('Verify that user can update personal information @andriod', async () => { +// await login(); +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.updatePersonalInfo(); +// await allPages.userPage.verifyPersonalInfoUpdated(); +// }); + +// test('Verify that User Can Add, Edit, and Delete Addresses after Logging In @andriod', async () => { +// await login(); + +// await test.step('Verify that user is able to add address successfully', async () => { +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.clickOnAddressTab(); +// await allPages.userPage.clickOnAddAddressButton(); +// await allPages.userPage.fillAddressForm(); +// await allPages.userPage.verifytheAddressIsAdded(); +// }); + +// await test.step('Verify that user is able to edit address successfully', async () => { +// await allPages.userPage.clickOnEditAddressButton(); +// await allPages.userPage.updateAddressForm(); +// await allPages.userPage.verifytheUpdatedAddressIsAdded(); +// }) + +// await test.step('Verify that user is able to delete address successfully', async () => { +// await allPages.userPage.clickOnDeleteAddressButton(); +// }); + +// test('Verify that user can change password successfully @ios', async () => { +// await test.step('Login with existing password', async () => { +// await login1(); +// }); + +// await test.step('Change password and verify login with new password', async () => { +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.clickOnSecurityButton(); +// await allPages.userPage.enterNewPassword(); +// await allPages.userPage.enterConfirmNewPassword(); +// await allPages.userPage.clickOnUpdatePasswordButton(); +// await allPages.userPage.getUpdatePasswordNotification(); +// }); +// await test.step('Verify login with new password and revert back to original password', async () => { +// // Re-login with new password +// await logout(); +// await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); + +// // Revert back +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.clickOnSecurityButton(); +// await allPages.userPage.revertPasswordBackToOriginal(); +// await allPages.userPage.getUpdatePasswordNotification(); +// }) +// }); + +// test('Verify that the New User is able to add Addresses in the Address section @ios', async () => { +// await login(); +// await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.clickOnAddressTab(); +// await allPages.userPage.clickOnAddAddressButton(); +// await allPages.userPage.checkAddNewAddressMenu(); +// await allPages.userPage.fillAddressForm(); +// }); + +// test('Verify that user can purchase multiple quantities in a single order @ios', async () => { +// const productName = 'GoPro HERO10 Black'; +// await login(); +// await allPages.inventoryPage.clickOnShopNowButton(); +// await allPages.inventoryPage.clickOnAllProductsLink(); +// await allPages.inventoryPage.searchProduct(productName); +// await allPages.inventoryPage.verifyProductTitleVisible(productName); +// await allPages.inventoryPage.clickOnAddToCartIcon(); + +// await allPages.cartPage.clickOnCartIcon(); +// await allPages.cartPage.verifyCartItemVisible(productName); +// await allPages.cartPage.clickIncreaseQuantityButton(); +// await allPages.cartPage.verifyIncreasedQuantity('3'); +// await allPages.cartPage.clickOnCheckoutButton(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.verifyProductInCheckout(productName); +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCashOnDeliverySelected(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // }); From a9a1c809aef10fb400be6f4d47943a22c4de9139 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:37:03 +0530 Subject: [PATCH 19/67] Updated some of the test cases --- tests/example.spec.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 89dbdf6..6b030bd 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -28,12 +28,12 @@ async function logout() { await allPages.loginPage.clickOnLogoutButton(); } -test('Verify that user can login and logout successfully @chromium', async () => { +test('Verify that user can login and logout successfully', async () => { await login(); await logout(); }); -test('Verify that all the navbar are working properly @chromium', async () => { +test('Verify that all the navbar are working properly, async () => { await login(); await allPages.homePage.clickBackToHomeButton(); // await allPages.homePage.assertHomePage(); @@ -45,7 +45,7 @@ test('Verify that all the navbar are working properly @chromium', async () => { await allPages.homePage.assertAboutUsTitle(); }); -test('Verify that user is able to delete selected product from cart @chroium', async () => { +test('Verify that user is able to delete selected product from cart', async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -63,7 +63,7 @@ test('Verify that user is able to delete selected product from cart @chroium', a await allPages.allProductsPage.assertAllProductsTitle(); }); -test('Verify that user can edit and delete a product review @chromium', async () => { +test('Verify that user can edit and delete a product review', async () => { await test.step('Login as existing user and navigate to a product', async () => { await login(); }) @@ -101,7 +101,7 @@ test('Verify that user can edit and delete a product review @chromium', async () }) }); -test('Verify that User Can Complete the Journey from Login to Order Placement @firefox', async () => { +test('Verify that User Can Complete the Journey from Login to Order Placement', async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -179,7 +179,7 @@ test('Verify that User Can Complete the Journey from Login to Order Placement @f // }) // }); -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement @firefox', async () => { +test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', async () => { // fresh test data const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; @@ -291,7 +291,7 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra }); }); -test('Verify that user add product to cart before logging in and then complete order after logging in @firefox', async () => { +test('Verify that user add product to cart before logging in and then complete order after logging in', async () => { await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); @@ -311,7 +311,7 @@ test('Verify that user add product to cart before logging in and then complete o }) }); -test('Verify that user can filter products by price range @firefox', async () => { +test('Verify that user can filter products by price range', async () => { await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); @@ -319,7 +319,7 @@ test('Verify that user can filter products by price range @firefox', async () => await allPages.homePage.clickOnFilterButton(); }); -test('Verify if user can add product to wishlist, moves it to card and then checks out @webkit', async () => { +test('Verify if user can add product to wishlist, moves it to card and then checks out', async () => { await login(); await test.step('Add product to wishlistand then add to cart', async () => { @@ -343,7 +343,7 @@ test('Verify if user can add product to wishlist, moves it to card and then chec }); -test('Verify new user views and cancels an order in my orders @webkit', async () => { +test('Verify new user views and cancels an order in my orders', async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -467,7 +467,7 @@ test('Verify new user views and cancels an order in my orders @webkit', async () // }) // }); -test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully @webkit', async () => { +test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -489,7 +489,7 @@ test('Verify that the new user is able to Sign Up, Log In, and Navigate to the H }) }) -test('Verify that user is able to fill Contact Us page successfully @webkit', async () => { +test('Verify that user is able to fill Contact Us page successfully', async () => { await login(); await allPages.homePage.clickOnContactUsLink(); await allPages.contactUsPage.assertContactUsTitle(); @@ -497,7 +497,7 @@ test('Verify that user is able to fill Contact Us page successfully @webkit', as await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); }); -test('Verify that user is able to submit a product review @chromium', async () => { +test('Verify that user is able to submit a product review', async () => { await test.step('Login as existing user and navigate to a product', async () => { await login(); }) From 34a2c6b9cc70ccb8fdb77795e672b443dd37897e Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:44:08 +0530 Subject: [PATCH 20/67] Updated some of the test cases --- tests/example.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 6b030bd..af1bfc5 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -33,7 +33,7 @@ test('Verify that user can login and logout successfully', async () => { await logout(); }); -test('Verify that all the navbar are working properly, async () => { +test('Verify that all the navbar are working properly', async () => { await login(); await allPages.homePage.clickBackToHomeButton(); // await allPages.homePage.assertHomePage(); From 9d0753dc1ce105683be9be7f7b908a95be25e576 Mon Sep 17 00:00:00 2001 From: testing Date: Mon, 22 Sep 2025 15:11:31 +0530 Subject: [PATCH 21/67] Updated playwright config --- playwright.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.js b/playwright.config.js index f6d55df..9750e70 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -22,7 +22,7 @@ export default defineConfig({ use: { baseURL: 'https://demo.alphabin.co/', - headless: true, + headless: false, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', From ccf41c2781e8506008e02a9629ea81517e7cbe4a Mon Sep 17 00:00:00 2001 From: testing Date: Wed, 24 Sep 2025 14:56:13 +0530 Subject: [PATCH 22/67] Updated some of the test cases --- tests/example.spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index af1bfc5..22380bc 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -121,7 +121,7 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -// test('Verify user can place and cancel an order @firefox', async () => { +// test('Verify user can place and cancel an order', async () => { // const productName = 'GoPro HERO10 Black'; // const productPriceAndQuantity = '₹49,999 × 1'; // const productQuantity = '1'; @@ -401,7 +401,7 @@ test('Verify new user views and cancels an order in my orders', async () => { }); }); -// test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement @webkit', async () => { +// test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { // const email = `test+${Date.now()}@test.com`; // const firstName = 'Test'; // const lastName = 'User'; @@ -522,14 +522,14 @@ test('Verify that user is able to submit a product review', async () => { }) }); -// test('Verify that user can update personal information @andriod', async () => { +// test('Verify that user can update personal information', async () => { // await login(); // await allPages.userPage.clickOnUserProfileIcon(); // await allPages.userPage.updatePersonalInfo(); // await allPages.userPage.verifyPersonalInfoUpdated(); // }); -// test('Verify that User Can Add, Edit, and Delete Addresses after Logging In @andriod', async () => { +// test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { // await login(); // await test.step('Verify that user is able to add address successfully', async () => { @@ -550,7 +550,7 @@ test('Verify that user is able to submit a product review', async () => { // await allPages.userPage.clickOnDeleteAddressButton(); // }); -// test('Verify that user can change password successfully @ios', async () => { +// test('Verify that user can change password successfully', async () => { // await test.step('Login with existing password', async () => { // await login1(); // }); @@ -576,7 +576,7 @@ test('Verify that user is able to submit a product review', async () => { // }) // }); -// test('Verify that the New User is able to add Addresses in the Address section @ios', async () => { +// test('Verify that the New User is able to add Addresses in the Address section', async () => { // await login(); // await allPages.userPage.clickOnUserProfileIcon(); // await allPages.userPage.clickOnAddressTab(); @@ -585,7 +585,7 @@ test('Verify that user is able to submit a product review', async () => { // await allPages.userPage.fillAddressForm(); // }); -// test('Verify that user can purchase multiple quantities in a single order @ios', async () => { +// test('Verify that user can purchase multiple quantities in a single order', async () => { // const productName = 'GoPro HERO10 Black'; // await login(); // await allPages.inventoryPage.clickOnShopNowButton(); @@ -605,4 +605,4 @@ test('Verify that user is able to submit a product review', async () => { // await allPages.checkoutPage.verifyCashOnDeliverySelected(); // await allPages.checkoutPage.clickOnPlaceOrder(); // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // }); + // }); \ No newline at end of file From da8e79fddb223b0f14c2033a2278ae7b45a48733 Mon Sep 17 00:00:00 2001 From: testing Date: Wed, 24 Sep 2025 15:01:09 +0530 Subject: [PATCH 23/67] Updated some of the test cases and Readme file --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6019cf..ca3f3bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,12 @@ on: push: # runs on every push pull_request: # runs on new PRs or PR updates schedule: + # Original time: 06:30 UTC (11:00 AM IST) - cron: '30 3 * * 1-5' + # Second run: +2 hours → 08:30 UTC (01:00 PM IST) + - cron: '30 5 * * 1-5' + # Third run: +2 more hours → 10:30 UTC (03:00 PM IST) + - cron: '30 7 * * 1-5' workflow_dispatch: jobs: From df10acdff03bc39562636bba877c4eb6cacd47a4 Mon Sep 17 00:00:00 2001 From: testing Date: Wed, 24 Sep 2025 15:07:30 +0530 Subject: [PATCH 24/67] added some of the test cases and fixed issues --- tests/example.spec.js | 413 +++++++++++++++++++++--------------------- 1 file changed, 207 insertions(+), 206 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 22380bc..a278b08 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -121,63 +121,63 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -// test('Verify user can place and cancel an order', async () => { -// const productName = 'GoPro HERO10 Black'; -// const productPriceAndQuantity = '₹49,999 × 1'; -// const productQuantity = '1'; -// const orderStatusProcessing = 'Processing'; -// const orderStatusCanceled = 'Canceled'; - -// await test.step('Verify that user can login successfully', async () => { -// await login(); -// await allPages.inventoryPage.clickOnAllProductsLink(); -// await allPages.inventoryPage.searchProduct(productName); -// await allPages.inventoryPage.verifyProductTitleVisible(productName); -// await allPages.inventoryPage.clickOnAddToCartIcon(); -// }) - -// await test.step('Add product to cart and checkout', async () => { -// await allPages.cartPage.clickOnCartIcon(); -// await allPages.cartPage.verifyCartItemVisible(productName); -// await allPages.cartPage.clickOnCheckoutButton(); -// }) - -// await test.step('Place order and click on continue shopping', async () => { -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.verifyProductInCheckout(productName); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -// await allPages.checkoutPage.verifyOrderItemName(productName); -// await allPages.inventoryPage.clickOnContinueShopping(); -// }) - -// await test.step('Verify order in My Orders', async () => { -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.orderPage.clickOnMyOrdersTab(); -// await allPages.orderPage.verifyMyOrdersTitle(); -// await allPages.orderPage.clickOnPaginationButton(2); -// await allPages.orderPage.verifyProductInOrderList(productName); -// await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); -// await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); -// await allPages.orderPage.clickOnPaginationButton(1); -// await allPages.orderPage.clickViewDetailsButton(1); -// await allPages.orderPage.verifyOrderDetailsTitle(); -// await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); -// }) - -// await test.step('Cancel order and verify status is updated to Canceled', async () => { -// await allPages.orderPage.clickCancelOrderButton(2); -// await allPages.orderPage.confirmCancellation(); -// await allPages.orderPage.verifyCancellationConfirmationMessage(); -// await allPages.orderPage.verifyMyOrdersCount(); -// await allPages.orderPage.clickOnMyOrdersTab(); -// await allPages.orderPage.verifyMyOrdersTitle(); -// await allPages.orderPage.clickOnPaginationButton(2); -// await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); -// }) -// }); +test('Verify user can place and cancel an order', async () => { + const productName = 'GoPro HERO10 Black'; + const productPriceAndQuantity = '₹49,999 × 1'; + const productQuantity = '1'; + const orderStatusProcessing = 'Processing'; + const orderStatusCanceled = 'Canceled'; + + await test.step('Verify that user can login successfully', async () => { + await login(); + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); + }) + + await test.step('Add product to cart and checkout', async () => { + await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.verifyCartItemVisible(productName); + await allPages.cartPage.clickOnCheckoutButton(); + }) + + await test.step('Place order and click on continue shopping', async () => { + await allPages.checkoutPage.verifyCheckoutTitle(); + await allPages.checkoutPage.verifyProductInCheckout(productName); + await allPages.checkoutPage.selectCashOnDelivery(); + await allPages.checkoutPage.verifyCashOnDeliverySelected(); + await allPages.checkoutPage.clickOnPlaceOrder(); + await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + await allPages.checkoutPage.verifyOrderItemName(productName); + await allPages.inventoryPage.clickOnContinueShopping(); + }) + + await test.step('Verify order in My Orders', async () => { + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.orderPage.clickOnMyOrdersTab(); + await allPages.orderPage.verifyMyOrdersTitle(); + await allPages.orderPage.clickOnPaginationButton(2); + await allPages.orderPage.verifyProductInOrderList(productName); + await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); + await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); + await allPages.orderPage.clickOnPaginationButton(1); + await allPages.orderPage.clickViewDetailsButton(1); + await allPages.orderPage.verifyOrderDetailsTitle(); + await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); + }) + + await test.step('Cancel order and verify status is updated to Canceled', async () => { + await allPages.orderPage.clickCancelOrderButton(2); + await allPages.orderPage.confirmCancellation(); + await allPages.orderPage.verifyCancellationConfirmationMessage(); + await allPages.orderPage.verifyMyOrdersCount(); + await allPages.orderPage.clickOnMyOrdersTab(); + await allPages.orderPage.verifyMyOrdersTitle(); + await allPages.orderPage.clickOnPaginationButton(2); + await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); + }) +}); test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', async () => { // fresh test data @@ -401,71 +401,71 @@ test('Verify new user views and cancels an order in my orders', async () => { }); }); -// test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { -// const email = `test+${Date.now()}@test.com`; -// const firstName = 'Test'; -// const lastName = 'User'; - -// let productName= `Rode NT1-A Condenser Mic`; - -// await test.step('Verify that user can register successfully', async () => { -// // Signup -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.clickOnSignupLink(); -// await allPages.signupPage.assertSignupPage(); -// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); -// await allPages.signupPage.verifySuccessSignUp(); -// }) - -// await test.step('Verify that user can login successfully', async () => { -// // Login as new user -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.login(email, process.env.PASSWORD); -// await allPages.loginPage.verifySuccessSignIn(); -// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); -// }) - -// await test.step('Navigate to All Products and add view details of a random product', async () => { -// await allPages.homePage.clickOnShopNowButton(); -// await allPages.allProductsPage.assertAllProductsTitle(); -// await allPages.allProductsPage.clickNthProduct(1); -// await allPages.productDetailsPage.clickOnReviewsTab(); -// await allPages.productDetailsPage.assertReviewsTab(); -// await allPages.productDetailsPage.clickOnAdditionalInfoTab(); -// await allPages.productDetailsPage.assertAdditionalInfoTab(); -// }) - -// await test.step('Add product to cart, change quantity, add new address and checkout', async () => { -// await allPages.productDetailsPage.clickAddToCartButton(); -// await allPages.productDetailsPage.clickCartIcon(); -// await allPages.cartPage.clickIncreaseQuantityButton(); -// await allPages.cartPage.clickOnCheckoutButton(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); -// await allPages.checkoutPage.clickSaveAddressButton(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -// await allPages.checkoutPage.verifyOrderConfirmedTitle(); -// await allPages.checkoutPage.clickOnContinueShoppingButton(); -// }) - -// await test.step('Add another product to cart, select existing address and checkout', async () => { -// await allPages.homePage.clickOnShopNowButton(); -// await allPages.allProductsPage.assertAllProductsTitle(); -// await allPages.allProductsPage.clickNthProduct(1); -// await allPages.productDetailsPage.clickAddToCartButton(); -// await allPages.productDetailsPage.clickCartIcon(); -// await allPages.cartPage.clickOnCheckoutButton(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -// }) -// }); +test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + let productName= `Rode NT1-A Condenser Mic`; + + await test.step('Verify that user can register successfully', async () => { + // Signup + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.loginPage.validateSignInPage(); + await allPages.loginPage.clickOnSignupLink(); + await allPages.signupPage.assertSignupPage(); + await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + await allPages.signupPage.verifySuccessSignUp(); + }) + + await test.step('Verify that user can login successfully', async () => { + // Login as new user + await allPages.loginPage.validateSignInPage(); + await allPages.loginPage.login(email, process.env.PASSWORD); + await allPages.loginPage.verifySuccessSignIn(); + await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + }) + + await test.step('Navigate to All Products and add view details of a random product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + await allPages.productDetailsPage.clickOnAdditionalInfoTab(); + await allPages.productDetailsPage.assertAdditionalInfoTab(); + }) + + await test.step('Add product to cart, change quantity, add new address and checkout', async () => { + await allPages.productDetailsPage.clickAddToCartButton(); + await allPages.productDetailsPage.clickCartIcon(); + await allPages.cartPage.clickIncreaseQuantityButton(); + await allPages.cartPage.clickOnCheckoutButton(); + await allPages.checkoutPage.verifyCheckoutTitle(); + await allPages.checkoutPage.selectCashOnDelivery(); + await allPages.checkoutPage.verifyCashOnDeliverySelected(); + await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); + await allPages.checkoutPage.clickSaveAddressButton(); + await allPages.checkoutPage.clickOnPlaceOrder(); + await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + await allPages.checkoutPage.verifyOrderConfirmedTitle(); + await allPages.checkoutPage.clickOnContinueShoppingButton(); + }) + + await test.step('Add another product to cart, select existing address and checkout', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + await allPages.productDetailsPage.clickAddToCartButton(); + await allPages.productDetailsPage.clickCartIcon(); + await allPages.cartPage.clickOnCheckoutButton(); + await allPages.checkoutPage.verifyCheckoutTitle(); + await allPages.checkoutPage.selectCashOnDelivery(); + await allPages.checkoutPage.verifyCashOnDeliverySelected(); + await allPages.checkoutPage.clickOnPlaceOrder(); + await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + }) +}); test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', async () => { const email = `test+${Date.now()}@test.com`; @@ -522,87 +522,88 @@ test('Verify that user is able to submit a product review', async () => { }) }); -// test('Verify that user can update personal information', async () => { -// await login(); -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.updatePersonalInfo(); -// await allPages.userPage.verifyPersonalInfoUpdated(); -// }); - -// test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { -// await login(); - -// await test.step('Verify that user is able to add address successfully', async () => { -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnAddressTab(); -// await allPages.userPage.clickOnAddAddressButton(); -// await allPages.userPage.fillAddressForm(); -// await allPages.userPage.verifytheAddressIsAdded(); -// }); - -// await test.step('Verify that user is able to edit address successfully', async () => { -// await allPages.userPage.clickOnEditAddressButton(); -// await allPages.userPage.updateAddressForm(); -// await allPages.userPage.verifytheUpdatedAddressIsAdded(); -// }) - -// await test.step('Verify that user is able to delete address successfully', async () => { -// await allPages.userPage.clickOnDeleteAddressButton(); -// }); - -// test('Verify that user can change password successfully', async () => { -// await test.step('Login with existing password', async () => { -// await login1(); -// }); - -// await test.step('Change password and verify login with new password', async () => { -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnSecurityButton(); -// await allPages.userPage.enterNewPassword(); -// await allPages.userPage.enterConfirmNewPassword(); -// await allPages.userPage.clickOnUpdatePasswordButton(); -// await allPages.userPage.getUpdatePasswordNotification(); -// }); -// await test.step('Verify login with new password and revert back to original password', async () => { -// // Re-login with new password -// await logout(); -// await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); - -// // Revert back -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnSecurityButton(); -// await allPages.userPage.revertPasswordBackToOriginal(); -// await allPages.userPage.getUpdatePasswordNotification(); -// }) -// }); - -// test('Verify that the New User is able to add Addresses in the Address section', async () => { -// await login(); -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnAddressTab(); -// await allPages.userPage.clickOnAddAddressButton(); -// await allPages.userPage.checkAddNewAddressMenu(); -// await allPages.userPage.fillAddressForm(); -// }); - -// test('Verify that user can purchase multiple quantities in a single order', async () => { -// const productName = 'GoPro HERO10 Black'; -// await login(); -// await allPages.inventoryPage.clickOnShopNowButton(); -// await allPages.inventoryPage.clickOnAllProductsLink(); -// await allPages.inventoryPage.searchProduct(productName); -// await allPages.inventoryPage.verifyProductTitleVisible(productName); -// await allPages.inventoryPage.clickOnAddToCartIcon(); - -// await allPages.cartPage.clickOnCartIcon(); -// await allPages.cartPage.verifyCartItemVisible(productName); -// await allPages.cartPage.clickIncreaseQuantityButton(); -// await allPages.cartPage.verifyIncreasedQuantity('3'); -// await allPages.cartPage.clickOnCheckoutButton(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.verifyProductInCheckout(productName); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // }); \ No newline at end of file +test('Verify that user can update personal information', async () => { + await login(); + await allPages.userPage.clickOnUserProfileIcon(); + await allPages.userPage.updatePersonalInfo(); + await allPages.userPage.verifyPersonalInfoUpdated(); +}); + +test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { + await login(); + + await test.step('Verify that user is able to add address successfully', async () => { + await allPages.userPage.clickOnUserProfileIcon(); + await allPages.userPage.clickOnAddressTab(); + await allPages.userPage.clickOnAddAddressButton(); + await allPages.userPage.fillAddressForm(); + await allPages.userPage.verifytheAddressIsAdded(); + }); + + await test.step('Verify that user is able to edit address successfully', async () => { + await allPages.userPage.clickOnEditAddressButton(); + await allPages.userPage.updateAddressForm(); + await allPages.userPage.verifytheUpdatedAddressIsAdded(); + }) + + await test.step('Verify that user is able to delete address successfully', async () => { + await allPages.userPage.clickOnDeleteAddressButton(); + }); + + test('Verify that user can change password successfully', async () => { + await test.step('Login with existing password', async () => { + await login1(); + }); + + await test.step('Change password and verify login with new password', async () => { + await allPages.userPage.clickOnUserProfileIcon(); + await allPages.userPage.clickOnSecurityButton(); + await allPages.userPage.enterNewPassword(); + await allPages.userPage.enterConfirmNewPassword(); + await allPages.userPage.clickOnUpdatePasswordButton(); + await allPages.userPage.getUpdatePasswordNotification(); + }); + await test.step('Verify login with new password and revert back to original password', async () => { + // Re-login with new password + await logout(); + await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); + + // Revert back + await allPages.userPage.clickOnUserProfileIcon(); + await allPages.userPage.clickOnSecurityButton(); + await allPages.userPage.revertPasswordBackToOriginal(); + await allPages.userPage.getUpdatePasswordNotification(); + }) +}); + +test('Verify that the New User is able to add Addresses in the Address section', async () => { + await login(); + await allPages.userPage.clickOnUserProfileIcon(); + await allPages.userPage.clickOnAddressTab(); + await allPages.userPage.clickOnAddAddressButton(); + await allPages.userPage.checkAddNewAddressMenu(); + await allPages.userPage.fillAddressForm(); + }); + +test('Verify that user can purchase multiple quantities in a single order', async () => { + const productName = 'GoPro HERO10 Black'; + await login(); + await allPages.inventoryPage.clickOnShopNowButton(); + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); + + await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.verifyCartItemVisible(productName); + await allPages.cartPage.clickIncreaseQuantityButton(); + await allPages.cartPage.verifyIncreasedQuantity('3'); + await allPages.cartPage.clickOnCheckoutButton(); + await allPages.checkoutPage.verifyCheckoutTitle(); + await allPages.checkoutPage.verifyProductInCheckout(productName); + await allPages.checkoutPage.selectCashOnDelivery(); + await allPages.checkoutPage.verifyCashOnDeliverySelected(); + await allPages.checkoutPage.clickOnPlaceOrder(); + await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + }); +}); \ No newline at end of file From 7f10e3201795c673f7c5dc39f0bbfe9975900c62 Mon Sep 17 00:00:00 2001 From: testing Date: Wed, 24 Sep 2025 15:13:27 +0530 Subject: [PATCH 25/67] Updated some of the test cases --- tests/example.spec.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 22380bc..716d92f 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -45,23 +45,23 @@ test('Verify that all the navbar are working properly', async () => { await allPages.homePage.assertAboutUsTitle(); }); -test('Verify that user is able to delete selected product from cart', async () => { - const productName = 'GoPro HERO10 Black'; - await login(); - await allPages.inventoryPage.clickOnShopNowButton(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); +// test('Verify that user is able to delete selected product from cart', async () => { +// const productName = 'GoPro HERO10 Black'; +// await login(); +// await allPages.inventoryPage.clickOnShopNowButton(); +// await allPages.inventoryPage.clickOnAllProductsLink(); +// await allPages.inventoryPage.searchProduct(productName); +// await allPages.inventoryPage.verifyProductTitleVisible(productName); +// await allPages.inventoryPage.clickOnAddToCartIcon(); - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnDeleteProductIcon(); - await allPages.cartPage.verifyCartItemDeleted(productName); - await allPages.cartPage.verifyEmptyCartMessage(); - await allPages.cartPage.clickOnStartShoppingButton(); - await allPages.allProductsPage.assertAllProductsTitle(); -}); +// await allPages.cartPage.clickOnCartIcon(); +// await allPages.cartPage.verifyCartItemVisible(productName); +// await allPages.cartPage.clickOnDeleteProductIcon(); +// await allPages.cartPage.verifyCartItemDeleted(productName); +// await allPages.cartPage.verifyEmptyCartMessage(); +// await allPages.cartPage.clickOnStartShoppingButton(); +// await allPages.allProductsPage.assertAllProductsTitle(); +// }); test('Verify that user can edit and delete a product review', async () => { await test.step('Login as existing user and navigate to a product', async () => { From c1e67bb27fc778668607e8854f0399c0f18dc466 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 11:02:27 +0530 Subject: [PATCH 26/67] Refactor user flow tests by removing unnecessary steps --- .github/workflows/test.yml | 94 ++-- pages/LoginPage.js | 12 +- tests/example.spec.js | 882 +++++++++++++++---------------------- 3 files changed, 414 insertions(+), 574 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca3f3bb..d549aca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,44 +1,36 @@ -# .github/workflows/playwright-daily.yml -# Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) -# plus anytime you trigger it manually from the Actions tab. - name: Run Playwright tests on: - push: # runs on every push - pull_request: # runs on new PRs or PR updates + push: + pull_request: schedule: - # Original time: 06:30 UTC (11:00 AM IST) - - cron: '30 3 * * 1-5' - # Second run: +2 hours → 08:30 UTC (01:00 PM IST) - - cron: '30 5 * * 1-5' - # Third run: +2 more hours → 10:30 UTC (03:00 PM IST) - - cron: '30 7 * * 1-5' + - cron: '30 3 * * 1-5' # 11:00 AM IST workflow_dispatch: jobs: run-tests: - name: Run Playwright shards + name: Run Playwright tests ${{ matrix.shardIndex }}/3 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - shardIndex: [1,2,3,4,5] - shardTotal: [5] + shardIndex: [1, 2, 3] + shardTotal: [3] steps: - uses: actions/checkout@v4 - - name: Setup Node.js 18.x + # ✅ REQUIRED: Node 20 + - name: Setup Node.js 20.x uses: actions/setup-node@v3 with: - node-version: '18' + node-version: '20' + - name: Create .env file run: | echo "USERNAME=${{ secrets.USERNAME }}" >> .env echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env - echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env @@ -46,7 +38,6 @@ jobs: echo "STATE=${{ secrets.STATE }}" >> .env echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -54,15 +45,41 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - - name: Install deps + browsers run: | npm ci - npx playwright install --with-deps - - - name: Run shard ${{ matrix.shardIndex }} - run: npx playwright test --project=chromium --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - + npx playwright install --with-deps chromium firefox webkit + # ✅ FULL + RERUN LOGIC + - name: Run Playwright (rerun failed tests if applicable) + env: + TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} + SHARD_INDEX: ${{ matrix.shardIndex }} + SHARD_TOTAL: ${{ matrix.shardTotal }} + run: | + echo "GitHub run attempt: ${{ github.run_attempt }}" + if [[ "${{ github.run_attempt }}" -gt 1 ]]; then + echo "Detected re-run. Checking failed test metadata from TestDino." + npx tdpw last-failed \ + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ + > last-failed-flags.txt + EXTRA_PW_FLAGS="$(cat last-failed-flags.txt)" + if [[ -n "$EXTRA_PW_FLAGS" ]]; then + echo "Running only failed tests for this shard:" + echo "$EXTRA_PW_FLAGS" + # IMPORTANT: JSON + BLOB BOTH REQUIRED + # Ensure playwright-report directory exists + mkdir -p ./playwright-report + eval "npx playwright test $EXTRA_PW_FLAGS" + exit 0 + fi + echo "No failed test metadata found. Falling back to full shard." + fi + # First run (full shard) + # Ensure playwright-report directory exists + mkdir -p ./playwright-report + npx playwright test \ + --grep="@chromium|@firefox|@webkit" \ + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 @@ -71,19 +88,29 @@ jobs: path: ./blob-report retention-days: 1 + # ✅ THIS WILL SHOW "Metadata cached successfully ✔" WHEN JSON EXISTS + - name: Cache tdpw last failed metadata + if: always() + env: + TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} + SHARD_INDEX: ${{ matrix.shardIndex }} + SHARD_TOTAL: ${{ matrix.shardTotal }} + run: | + npx tdpw cache --verbose merge-reports: name: Merge Reports needs: run-tests - if: always() # run even if some shards fail + if: always() runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup Node.js 18.x + # ✅ Node 20 here as well + - name: Setup Node.js 20.x uses: actions/setup-node@v3 with: - node-version: '18' + node-version: '20' - name: Cache npm dependencies uses: actions/cache@v3 @@ -92,12 +119,10 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - - name: Install deps + browsers run: | npm ci npx playwright install --with-deps - - name: Download all blob reports uses: actions/download-artifact@v4 with: @@ -117,7 +142,8 @@ jobs: - name: Send TestDino report run: | - npx --yes tdpw ./playwright-report \ - --token="${{ secrets.TESTDINO_TOKEN }}" \ - --upload-html \ - --verbose + npx --yes tdpw ./playwright-report \ + --token="${{ secrets.TESTDINO_TOKEN }}" \ + --upload-html \ + --upload-traces \ + --verbose \ No newline at end of file diff --git a/pages/LoginPage.js b/pages/LoginPage.js index 22e010d..b27d956 100644 --- a/pages/LoginPage.js +++ b/pages/LoginPage.js @@ -2,7 +2,7 @@ import BasePage from './BasePage.js'; import { expect } from '@playwright/test'; class LoginPage extends BasePage{ - + /** * @param {import('@playwright/test').Page} page */ @@ -23,6 +23,7 @@ class LoginPage extends BasePage{ successSignInMessage: `Logged in successfully`, } + async navigateToLoginPage() { await this.navigateTo('/'); } @@ -49,6 +50,11 @@ class LoginPage extends BasePage{ async clickOnUserProfileIcon() { await this.page.locator(this.locators.userIcon).click(); + // Wait for dropdown menu to appear + // await this.page.waitForSelector(this.locators.logoutButton, { + // state: 'visible', + // timeout: 5000 + // }); } async assertLoginPage() { @@ -66,7 +72,9 @@ class LoginPage extends BasePage{ } async clickOnLogoutButton() { - await this.page.locator(this.locators.logoutButton).click(); + const logoutBtn = this.page.locator(this.locators.logoutButton); + await logoutBtn.waitFor({ state: 'visible', timeout: 10000 }); + await logoutBtn.click(); } async validateSignInPage() { await expect(this.getLoginPageTitle()).toBeVisible(); diff --git a/tests/example.spec.js b/tests/example.spec.js index a278b08..8b9b9b2 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -14,13 +14,13 @@ test.beforeEach(async ({ page }) => { async function login(username = process.env.USERNAME, password = process.env.PASSWORD) { await allPages.loginPage.clickOnUserProfileIcon(); await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(username, password); + // await allPages.loginPage.login(username, password); } async function login1(username = process.env.USERNAME1, password = process.env.PASSWORD) { await allPages.loginPage.clickOnUserProfileIcon(); await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(username, password); + // await allPages.loginPage.login(username, password); } async function logout() { @@ -28,270 +28,214 @@ async function logout() { await allPages.loginPage.clickOnLogoutButton(); } -test('Verify that user can login and logout successfully', async () => { +test('Verify that user can login and logout successfully', {tag: '@chromium'}, async () => { await login(); - await logout(); + // Flaky: Random failure to simulate intermittent issues + if (Math.random() < 0.4) { + throw new Error('Random flaky failure: Login validation failed intermittently'); + } + // await logout(); }); -test('Verify that all the navbar are working properly', async () => { +test('Verify that all the navbar are working properly', {tag: '@webkit'}, async () => { await login(); - await allPages.homePage.clickBackToHomeButton(); - // await allPages.homePage.assertHomePage(); - await allPages.homePage.clickAllProductsNav(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.homePage.clickOnContactUsLink(); - await allPages.contactUsPage.assertContactUsTitle(); - await allPages.homePage.clickAboutUsNav(); - await allPages.homePage.assertAboutUsTitle(); -}); - -test('Verify that user is able to delete selected product from cart', async () => { - const productName = 'GoPro HERO10 Black'; - await login(); - await allPages.inventoryPage.clickOnShopNowButton(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnDeleteProductIcon(); - await allPages.cartPage.verifyCartItemDeleted(productName); - await allPages.cartPage.verifyEmptyCartMessage(); - await allPages.cartPage.clickOnStartShoppingButton(); - await allPages.allProductsPage.assertAllProductsTitle(); -}); - -test('Verify that user can edit and delete a product review', async () => { + // Flaky: Random timing issue that sometimes causes race condition + const randomDelay = Math.random() * 200; + await new Promise(resolve => setTimeout(resolve, randomDelay)); + if (Math.random() < 0.35) { + throw new Error('Flaky timing issue: Element not found due to race condition'); + } + // await allPages.homePage.clickBackToHomeButton(); + // // await allPages.homePage.assertHomePage(); + // await allPages.homePage.clickAllProductsNav(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.homePage.clickOnContactUsLink(); + // await allPages.contactUsPage.assertContactUsTitle(); + // await allPages.homePage.clickAboutUsNav(); + // await allPages.homePage.assertAboutUsTitle(); +}); + +test('Verify that user can edit and delete a product review', {tag: '@chromium'}, async () => { await test.step('Login as existing user and navigate to a product', async () => { await login(); + // Flaky: Intermittent failure during navigation + if (Math.random() < 0.3) { + await new Promise(resolve => setTimeout(resolve, 100)); + throw new Error('Flaky error: Page navigation timeout occurred'); + } }) - await test.step('Navigate to all product section and select a product', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - }) + // await test.step('Navigate to all product section and select a product', async () => { + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.allProductsPage.clickNthProduct(1); + // }) - await test.step('Submit a product review and verify submission', async () => { - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); - - await allPages.productDetailsPage.clickOnWriteAReviewBtn(); - await allPages.productDetailsPage.fillReviewForm(); - await allPages.productDetailsPage.assertSubmittedReview({ - name: 'John Doe', - title: 'Great Product', - opinion: 'This product exceeded my expectations. Highly recommend!' - }); - }) - await test.step('Edit the submitted review and verify changes', async () => { - await allPages.productDetailsPage.clickOnEditReviewBtn(); - await allPages.productDetailsPage.updateReviewForm(); - await allPages.productDetailsPage.assertUpdatedReview({ - title: 'Updated Review Title', - opinion: 'This is an updated review opinion.' - }) - }); - - await test.step('Delete the submitted review and verify deletion', async () => { - await allPages.productDetailsPage.clickOnDeleteReviewBtn(); - }) + // await test.step('Submit a product review and verify submission', async () => { + // await allPages.productDetailsPage.clickOnReviewsTab(); + // await allPages.productDetailsPage.assertReviewsTab(); + + // await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + // await allPages.productDetailsPage.fillReviewForm(); + // await allPages.productDetailsPage.assertSubmittedReview({ + // name: 'John Doe', + // title: 'Great Product', + // opinion: 'This product exceeded my expectations. Highly recommend!' + // }); + // }) + + // await test.step('Edit the submitted review and verify changes', async () => { + // await allPages.productDetailsPage.clickOnEditReviewBtn(); + // await allPages.productDetailsPage.updateReviewForm(); + // await allPages.productDetailsPage.assertUpdatedReview({ + // title: 'Updated Review Title', + // opinion: 'This is an updated review opinion.' + // }) + // }); + + // await test.step('Delete the submitted review and verify deletion', async () => { + // await allPages.productDetailsPage.clickOnDeleteReviewBtn(); + // }) }); -test('Verify that User Can Complete the Journey from Login to Order Placement', async () => { +test('Verify that User Can Complete the Journey from Login to Order Placement', {tag: '@chromium'}, async () => { const productName = 'GoPro HERO10 Black'; await login(); - await allPages.inventoryPage.clickOnShopNowButton(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -}); - -test('Verify user can place and cancel an order', async () => { - const productName = 'GoPro HERO10 Black'; - const productPriceAndQuantity = '₹49,999 × 1'; - const productQuantity = '1'; - const orderStatusProcessing = 'Processing'; - const orderStatusCanceled = 'Canceled'; - - await test.step('Verify that user can login successfully', async () => { - await login(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - }) - - await test.step('Add product to cart and checkout', async () => { - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnCheckoutButton(); - }) - - await test.step('Place order and click on continue shopping', async () => { - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.checkoutPage.verifyOrderItemName(productName); - await allPages.inventoryPage.clickOnContinueShopping(); - }) - - await test.step('Verify order in My Orders', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.verifyMyOrdersTitle(); - await allPages.orderPage.clickOnPaginationButton(2); - await allPages.orderPage.verifyProductInOrderList(productName); - await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); - await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); - await allPages.orderPage.clickOnPaginationButton(1); - await allPages.orderPage.clickViewDetailsButton(1); - await allPages.orderPage.verifyOrderDetailsTitle(); - await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); - }) - - await test.step('Cancel order and verify status is updated to Canceled', async () => { - await allPages.orderPage.clickCancelOrderButton(2); - await allPages.orderPage.confirmCancellation(); - await allPages.orderPage.verifyCancellationConfirmationMessage(); - await allPages.orderPage.verifyMyOrdersCount(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.verifyMyOrdersTitle(); - await allPages.orderPage.clickOnPaginationButton(2); - await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); - }) + // Flaky: Random assertion failure + if (Math.random() < 0.25) { + throw new Error('Flaky assertion: Product validation failed intermittently'); + } + // await allPages.inventoryPage.clickOnShopNowButton(); + // await allPages.inventoryPage.clickOnAllProductsLink(); + // await allPages.inventoryPage.searchProduct(productName); + // await allPages.inventoryPage.verifyProductTitleVisible(productName); + // await allPages.inventoryPage.clickOnAddToCartIcon(); + + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.verifyCartItemVisible(productName); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.verifyProductInCheckout(productName); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', async () => { - // fresh test data - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - let productName; - let productPrice; - let productReviewCount; - - await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) - - await test.step('Navigate to all product and add to wishlist section', async () => { - await allPages.homePage.clickAllProductsNav(); - await allPages.allProductsPage.assertAllProductsTitle(); - - productName = await allPages.allProductsPage.getNthProductName(1); - productPrice = await allPages.allProductsPage.getNthProductPrice(1); - productReviewCount = await allPages.allProductsPage.getNthProductReviewCount(1); - - await allPages.allProductsPage.clickNthProductWishlistIcon(1); - await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); - await allPages.allProductsPage.clickNthProduct(1); - - await allPages.productDetailsPage.assertProductNameTitle(productName); - await allPages.productDetailsPage.assertProductPrice(productName, productPrice); - await allPages.productDetailsPage.assertProductReviewCount(productName, productReviewCount); - await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); - }) - - await test.step('Add product to cart, add new address and checkout', async () => { - await allPages.productDetailsPage.clickAddToCartButton(); - - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.assertYourCartTitle(); - await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); - await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); - await allPages.cartPage.clickIncreaseQuantityButton(); - await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); - - const cleanPrice = productPrice.replace(/[₹,]/g, ''); - const priceValue = parseFloat(cleanPrice) * 2; - await expect(allPages.cartPage.getTotalValue()).toContainText( - priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') - ); - await allPages.cartPage.clickOnCheckoutButton(); - - // Fill shipping address and save - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.fillShippingAddress( - firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - ); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.assertAddressAddedToast(); - - // COD, verify summary, place order - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.assertOrderSummaryTitle(); - await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); - await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); - await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); - - const subtotalValue = parseFloat(cleanPrice) * 2; - const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); - await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); - await allPages.checkoutPage.clickOnPlaceOrder(); - - // Order details and return to home - await allPages.orderDetailsPage.assertOrderDetailsTitle(); - await allPages.orderDetailsPage.assertOrderPlacedName(); - await allPages.orderDetailsPage.assertOrderPlacedMessage(); - await allPages.orderDetailsPage.assertOrderPlacedDate(); - await allPages.orderDetailsPage.assertOrderInformationTitle(); - await allPages.orderDetailsPage.assertOrderConfirmedTitle(); - await allPages.orderDetailsPage.assertOrderConfirmedMessage(); - await allPages.orderDetailsPage.assertShippingDetailsTitle(); - await allPages.orderDetailsPage.assertShippingEmailValue(email); - await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); - await allPages.orderDetailsPage.assertDeliveryAddressLabel(); - await allPages.orderDetailsPage.assertDeliveryAddressValue(); - await allPages.orderDetailsPage.assertContinueShoppingButton(); - - await allPages.orderDetailsPage.assertOrderSummaryTitle(); - await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); - await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); - await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); - await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); - await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); - await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); - await allPages.orderDetailsPage.clickBackToHomeButton(); - }); -}); - -test('Verify that user add product to cart before logging in and then complete order after logging in', async () => { +// test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', {tag: '@chromium'}, async () => { +// // fresh test data +// const email = `test+${Date.now()}@test.com`; +// const firstName = 'Test'; +// const lastName = 'User'; + +// let productName; +// let productPrice; +// let productReviewCount; + +// await test.step('Verify that user can register successfully', async () => { +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.clickOnSignupLink(); +// await allPages.signupPage.assertSignupPage(); +// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); +// await allPages.signupPage.verifySuccessSignUp(); +// }) + +// await test.step('Verify that user can login successfully', async () => { +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.login(email, process.env.PASSWORD); +// await allPages.loginPage.verifySuccessSignIn(); +// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); +// }) + +// await test.step('Navigate to all product and add to wishlist section', async () => { +// await allPages.homePage.clickAllProductsNav(); +// await allPages.allProductsPage.assertAllProductsTitle(); + +// productName = await allPages.allProductsPage.getNthProductName(1); +// productPrice = await allPages.allProductsPage.getNthProductPrice(1); +// productReviewCount = await allPages.allProductsPage.getNthProductReviewCount(1); + +// await allPages.allProductsPage.clickNthProductWishlistIcon(1); +// await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); +// await allPages.allProductsPage.clickNthProduct(1); + +// await allPages.productDetailsPage.assertProductNameTitle(productName); +// await allPages.productDetailsPage.assertProductPrice(productName, productPrice); +// await allPages.productDetailsPage.assertProductReviewCount(productName, productReviewCount); +// await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); +// }) + +// await test.step('Add product to cart, add new address and checkout', async () => { +// await allPages.productDetailsPage.clickAddToCartButton(); + +// await allPages.productDetailsPage.clickCartIcon(); +// await allPages.cartPage.assertYourCartTitle(); +// await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); +// await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); +// await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); +// await allPages.cartPage.clickIncreaseQuantityButton(); +// await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); + +// const cleanPrice = productPrice.replace(/[₹,]/g, ''); +// const priceValue = parseFloat(cleanPrice) * 2; +// await expect(allPages.cartPage.getTotalValue()).toContainText( +// priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') +// ); +// await allPages.cartPage.clickOnCheckoutButton(); + +// // Fill shipping address and save +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.fillShippingAddress( +// firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' +// ); +// await allPages.checkoutPage.clickSaveAddressButton(); +// await allPages.checkoutPage.assertAddressAddedToast(); + +// // COD, verify summary, place order +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.assertOrderSummaryTitle(); +// await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); +// await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); +// await allPages.checkoutPage.verifyProductInCheckout(productName); +// await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); +// await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); + +// const subtotalValue = parseFloat(cleanPrice) * 2; +// const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +// await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); +// await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); +// await allPages.checkoutPage.clickOnPlaceOrder(); + +// // Order details and return to home +// await allPages.orderDetailsPage.assertOrderDetailsTitle(); +// await allPages.orderDetailsPage.assertOrderPlacedName(); +// await allPages.orderDetailsPage.assertOrderPlacedMessage(); +// await allPages.orderDetailsPage.assertOrderPlacedDate(); +// await allPages.orderDetailsPage.assertOrderInformationTitle(); +// await allPages.orderDetailsPage.assertOrderConfirmedTitle(); +// await allPages.orderDetailsPage.assertOrderConfirmedMessage(); +// await allPages.orderDetailsPage.assertShippingDetailsTitle(); +// await allPages.orderDetailsPage.assertShippingEmailValue(email); +// await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); +// await allPages.orderDetailsPage.assertDeliveryAddressLabel(); +// await allPages.orderDetailsPage.assertDeliveryAddressValue(); +// await allPages.orderDetailsPage.assertContinueShoppingButton(); + +// await allPages.orderDetailsPage.assertOrderSummaryTitle(); +// await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); +// await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); +// await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); +// await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); +// await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); +// await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); +// await allPages.orderDetailsPage.clickBackToHomeButton(); +// }); +// }); + +test('Verify that user add product to cart before logging in and then complete order after logging in', {tag: '@firefox'}, async () => { await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); @@ -299,311 +243,173 @@ test('Verify that user add product to cart before logging in and then complete o await allPages.homePage.validateAddCartNotification(); await allPages.loginPage.clickOnUserProfileIcon(); }) - await test.step('Login and complete order', async () => { - await login(); - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -}) +// await test.step('Login and complete order', async () => { +// await login(); +// await allPages.cartPage.clickOnCartIcon(); +// await allPages.cartPage.clickOnCheckoutButton(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCashOnDeliverySelected(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +// }) }); -test('Verify that user can filter products by price range', async () => { - await login(); +test('Verify that user can filter products by price range', {tag: '@firefox'}, async () => { + // await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); + // Flaky: Timing issue with slider interaction + const randomWait = Math.random() * 150; + await new Promise(resolve => setTimeout(resolve, randomWait)); await allPages.homePage.AdjustPriceRangeSlider('10000', '20000'); + if (Math.random() < 0.4) { + throw new Error('Flaky error: Filter slider interaction failed due to timing'); + } await allPages.homePage.clickOnFilterButton(); }); -test('Verify if user can add product to wishlist, moves it to card and then checks out', async () => { +test('Verify if user can add product to wishlist, moves it to card and then checks out', {tag: '@firefox'}, async () => { await login(); + // Flaky: Random failure during wishlist operation + if (Math.random() < 0.35) { + await new Promise(resolve => setTimeout(resolve, 50)); + throw new Error('Flaky failure: Wishlist operation failed intermittently'); + } - await test.step('Add product to wishlistand then add to cart', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.inventoryPage.addToWishlist(); - await allPages.inventoryPage.assertWishlistIcon(); - await allPages.inventoryPage.clickOnWishlistIconHeader(); - await allPages.inventoryPage.assertWishlistPage(); - await allPages.inventoryPage.clickOnWishlistAddToCard(); - }) + // await test.step('Add product to wishlistand then add to cart', async () => { + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.inventoryPage.addToWishlist(); + // await allPages.inventoryPage.assertWishlistIcon(); + // await allPages.inventoryPage.clickOnWishlistIconHeader(); + // await allPages.inventoryPage.assertWishlistPage(); + // await allPages.inventoryPage.clickOnWishlistAddToCard(); + // }) - await test.step('Checkout product added to cart', async () => { - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) + // await test.step('Checkout product added to cart', async () => { + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // }) }); -test('Verify new user views and cancels an order in my orders', async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - let productName= `Rode NT1-A Condenser Mic`; - - await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) - - await test.step('Navigate to All Products and add view details of a random product', async () => { - await allPages.homePage.clickAllProductsNav(); - await allPages.allProductsPage.assertAllProductsTitle(); - productName = await allPages.allProductsPage.getNthProductName(1); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickAddToCartButton(); - }) - - await test.step('Add product to cart, add new address and checkout', async () => { - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.assertYourCartTitle(); - await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.fillShippingAddress( - firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - ); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.assertAddressAddedToast(); - }) - - await test.step('Complete order and verify in my orders', async () => { - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.inventoryPage.clickOnContinueShopping(); - - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.clickCancelOrderButton(); - await allPages.orderPage.confirmCancellation(); - }); -}); - -test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - let productName= `Rode NT1-A Condenser Mic`; - - await test.step('Verify that user can register successfully', async () => { - // Signup - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - // Login as new user - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) - - await test.step('Navigate to All Products and add view details of a random product', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); - await allPages.productDetailsPage.clickOnAdditionalInfoTab(); - await allPages.productDetailsPage.assertAdditionalInfoTab(); - }) - - await test.step('Add product to cart, change quantity, add new address and checkout', async () => { - await allPages.productDetailsPage.clickAddToCartButton(); - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.clickIncreaseQuantityButton(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.checkoutPage.verifyOrderConfirmedTitle(); - await allPages.checkoutPage.clickOnContinueShoppingButton(); - }) - - await test.step('Add another product to cart, select existing address and checkout', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickAddToCartButton(); - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) -}); - -test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) -}) - -test('Verify that user is able to fill Contact Us page successfully', async () => { - await login(); - await allPages.homePage.clickOnContactUsLink(); - await allPages.contactUsPage.assertContactUsTitle(); - await allPages.contactUsPage.fillContactUsForm(); - await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); -}); - -test('Verify that user is able to submit a product review', async () => { +// test('Verify new user views and cancels an order in my orders', {tag: '@firefox'}, async () => { +// const email = `test+${Date.now()}@test.com`; +// const firstName = 'Test'; +// const lastName = 'User'; + +// let productName= `Rode NT1-A Condenser Mic`; + +// await test.step('Verify that user can register successfully', async () => { +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.clickOnSignupLink(); +// await allPages.signupPage.assertSignupPage(); +// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); +// await allPages.signupPage.verifySuccessSignUp(); +// }) + +// await test.step('Verify that user can login successfully', async () => { +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.login(email, process.env.PASSWORD); +// await allPages.loginPage.verifySuccessSignIn(); +// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); +// }) + +// await test.step('Navigate to All Products and add view details of a random product', async () => { +// await allPages.homePage.clickAllProductsNav(); +// await allPages.allProductsPage.assertAllProductsTitle(); +// productName = await allPages.allProductsPage.getNthProductName(1); +// await allPages.allProductsPage.clickNthProduct(1); +// await allPages.productDetailsPage.clickAddToCartButton(); +// }) + +// await test.step('Add product to cart, add new address and checkout', async () => { +// await allPages.productDetailsPage.clickCartIcon(); +// await allPages.cartPage.assertYourCartTitle(); +// await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); +// await allPages.cartPage.clickOnCheckoutButton(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.fillShippingAddress( +// firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' +// ); +// await allPages.checkoutPage.clickSaveAddressButton(); +// await allPages.checkoutPage.assertAddressAddedToast(); +// }) + +// await test.step('Complete order and verify in my orders', async () => { +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +// await allPages.inventoryPage.clickOnContinueShopping(); + +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.orderPage.clickOnMyOrdersTab(); +// await allPages.orderPage.clickCancelOrderButton(); +// await allPages.orderPage.confirmCancellation(); +// }); +// }); + +// test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', {tag: '@webkit'}, async () => { +// const email = `test+${Date.now()}@test.com`; +// const firstName = 'Test'; +// const lastName = 'User'; + +// await test.step('Verify that user can register successfully', async () => { +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.clickOnSignupLink(); +// await allPages.signupPage.assertSignupPage(); +// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); +// await allPages.signupPage.verifySuccessSignUp(); +// }) + +// await test.step('Verify that user can login successfully', async () => { +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.login(email, process.env.PASSWORD); +// await allPages.loginPage.verifySuccessSignIn(); +// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); +// }) +// }) + +// test('Verify that user is able to fill Contact Us page successfully', {tag: '@webkit'}, async () => { +// await login(); +// await allPages.homePage.clickOnContactUsLink(); +// await allPages.contactUsPage.assertContactUsTitle(); +// await allPages.contactUsPage.fillContactUsForm(); +// await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); +// }); + +test('Verify that user is able to submit a product review', {tag: '@webkit'}, async () => { await test.step('Login as existing user and navigate to a product', async () => { await login(); + // Flaky: Intermittent validation failure + if (Math.random() < 0.3) { + throw new Error('Flaky error: Review submission validation failed'); + } }) - await test.step('Navigate to all product section and select a product', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - }) + // await test.step('Navigate to all product section and select a product', async () => { + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.allProductsPage.clickNthProduct(1); + // }) - await test.step('Submit a product review and verify submission', async () => { - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); + // await test.step('Submit a product review and verify submission', async () => { + // await allPages.productDetailsPage.clickOnReviewsTab(); + // await allPages.productDetailsPage.assertReviewsTab(); - await allPages.productDetailsPage.clickOnWriteAReviewBtn(); - await allPages.productDetailsPage.fillReviewForm(); - await allPages.productDetailsPage.assertSubmittedReview({ - name: 'John Doe', - title: 'Great Product', - opinion: 'This product exceeded my expectations. Highly recommend!' - }); - }) -}); - -test('Verify that user can update personal information', async () => { - await login(); - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.updatePersonalInfo(); - await allPages.userPage.verifyPersonalInfoUpdated(); -}); - -test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { - await login(); - - await test.step('Verify that user is able to add address successfully', async () => { - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnAddressTab(); - await allPages.userPage.clickOnAddAddressButton(); - await allPages.userPage.fillAddressForm(); - await allPages.userPage.verifytheAddressIsAdded(); - }); - - await test.step('Verify that user is able to edit address successfully', async () => { - await allPages.userPage.clickOnEditAddressButton(); - await allPages.userPage.updateAddressForm(); - await allPages.userPage.verifytheUpdatedAddressIsAdded(); - }) - - await test.step('Verify that user is able to delete address successfully', async () => { - await allPages.userPage.clickOnDeleteAddressButton(); - }); - - test('Verify that user can change password successfully', async () => { - await test.step('Login with existing password', async () => { - await login1(); - }); - - await test.step('Change password and verify login with new password', async () => { - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnSecurityButton(); - await allPages.userPage.enterNewPassword(); - await allPages.userPage.enterConfirmNewPassword(); - await allPages.userPage.clickOnUpdatePasswordButton(); - await allPages.userPage.getUpdatePasswordNotification(); - }); - await test.step('Verify login with new password and revert back to original password', async () => { - // Re-login with new password - await logout(); - await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); - - // Revert back - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnSecurityButton(); - await allPages.userPage.revertPasswordBackToOriginal(); - await allPages.userPage.getUpdatePasswordNotification(); - }) -}); - -test('Verify that the New User is able to add Addresses in the Address section', async () => { - await login(); - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnAddressTab(); - await allPages.userPage.clickOnAddAddressButton(); - await allPages.userPage.checkAddNewAddressMenu(); - await allPages.userPage.fillAddressForm(); - }); - -test('Verify that user can purchase multiple quantities in a single order', async () => { - const productName = 'GoPro HERO10 Black'; - await login(); - await allPages.inventoryPage.clickOnShopNowButton(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickIncreaseQuantityButton(); - await allPages.cartPage.verifyIncreasedQuantity('3'); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }); -}); \ No newline at end of file + // await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + // await allPages.productDetailsPage.fillReviewForm(); + // await allPages.productDetailsPage.assertSubmittedReview({ + // name: 'John Doe', + // title: 'Great Product', + // opinion: 'This product exceeded my expectations. Highly recommend!' + // }); + }) \ No newline at end of file From 29c9747a521dee0f203afc49eced092f2c11f6ce Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 11:21:47 +0530 Subject: [PATCH 27/67] Refactor user flow tests by removing unnecessary steps --- .github/workflows/test.yml | 2 +- tests/example.spec.js | 447 ++++++------------------------------- 2 files changed, 70 insertions(+), 379 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d549aca..cc478d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: mkdir -p ./playwright-report npx playwright test \ --grep="@chromium|@firefox|@webkit" \ - --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} || exit 1 - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 diff --git a/tests/example.spec.js b/tests/example.spec.js index 8b9b9b2..d0e8c3e 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test'; import AllPages from '../pages/AllPages.js'; import dotenv from 'dotenv'; + dotenv.config({ override: true }); let allPages; @@ -11,405 +12,95 @@ test.beforeEach(async ({ page }) => { await page.goto('/'); }); -async function login(username = process.env.USERNAME, password = process.env.PASSWORD) { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(username, password); -} +/* ---------- Helpers ---------- */ -async function login1(username = process.env.USERNAME1, password = process.env.PASSWORD) { +async function login( + username = process.env.USERNAME, + password = process.env.PASSWORD +) { await allPages.loginPage.clickOnUserProfileIcon(); await allPages.loginPage.validateSignInPage(); // await allPages.loginPage.login(username, password); } -async function logout() { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.clickOnLogoutButton(); +function failOnlyOnFirstAttempt(testInfo, message) { + if (testInfo.retry === 0) { + throw new Error(message); + } } -test('Verify that user can login and logout successfully', {tag: '@chromium'}, async () => { - await login(); - // Flaky: Random failure to simulate intermittent issues - if (Math.random() < 0.4) { - throw new Error('Random flaky failure: Login validation failed intermittently'); - } - // await logout(); -}); +/* ---------- DEMO FLAKY TEST ---------- */ -test('Verify that all the navbar are working properly', {tag: '@webkit'}, async () => { +test( + 'DEMO: Verify that user can login and logout successfully', + { tag: '@chromium' }, + async ({}, testInfo) => { await login(); - // Flaky: Random timing issue that sometimes causes race condition - const randomDelay = Math.random() * 200; - await new Promise(resolve => setTimeout(resolve, randomDelay)); - if (Math.random() < 0.35) { - throw new Error('Flaky timing issue: Element not found due to race condition'); - } - // await allPages.homePage.clickBackToHomeButton(); - // // await allPages.homePage.assertHomePage(); - // await allPages.homePage.clickAllProductsNav(); - // await allPages.allProductsPage.assertAllProductsTitle(); - // await allPages.homePage.clickOnContactUsLink(); - // await allPages.contactUsPage.assertContactUsTitle(); - // await allPages.homePage.clickAboutUsNav(); - // await allPages.homePage.assertAboutUsTitle(); -}); -test('Verify that user can edit and delete a product review', {tag: '@chromium'}, async () => { - await test.step('Login as existing user and navigate to a product', async () => { - await login(); - // Flaky: Intermittent failure during navigation - if (Math.random() < 0.3) { - await new Promise(resolve => setTimeout(resolve, 100)); - throw new Error('Flaky error: Page navigation timeout occurred'); + if (testInfo.retry > 0) { + console.log(`🔁 Re-running failed test (retry #${testInfo.retry})`); } - }) - - // await test.step('Navigate to all product section and select a product', async () => { - // await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.assertAllProductsTitle(); - // await allPages.allProductsPage.clickNthProduct(1); - // }) - - - // await test.step('Submit a product review and verify submission', async () => { - // await allPages.productDetailsPage.clickOnReviewsTab(); - // await allPages.productDetailsPage.assertReviewsTab(); - - // await allPages.productDetailsPage.clickOnWriteAReviewBtn(); - // await allPages.productDetailsPage.fillReviewForm(); - // await allPages.productDetailsPage.assertSubmittedReview({ - // name: 'John Doe', - // title: 'Great Product', - // opinion: 'This product exceeded my expectations. Highly recommend!' - // }); - // }) - - // await test.step('Edit the submitted review and verify changes', async () => { - // await allPages.productDetailsPage.clickOnEditReviewBtn(); - // await allPages.productDetailsPage.updateReviewForm(); - // await allPages.productDetailsPage.assertUpdatedReview({ - // title: 'Updated Review Title', - // opinion: 'This is an updated review opinion.' - // }) - // }); - // await test.step('Delete the submitted review and verify deletion', async () => { - // await allPages.productDetailsPage.clickOnDeleteReviewBtn(); - // }) -}); + failOnlyOnFirstAttempt( + testInfo, + 'Demo failure: intentionally failing first attempt' + ); -test('Verify that User Can Complete the Journey from Login to Order Placement', {tag: '@chromium'}, async () => { - const productName = 'GoPro HERO10 Black'; - await login(); - // Flaky: Random assertion failure - if (Math.random() < 0.25) { - throw new Error('Flaky assertion: Product validation failed intermittently'); + // await logout(); } - // await allPages.inventoryPage.clickOnShopNowButton(); - // await allPages.inventoryPage.clickOnAllProductsLink(); - // await allPages.inventoryPage.searchProduct(productName); - // await allPages.inventoryPage.verifyProductTitleVisible(productName); - // await allPages.inventoryPage.clickOnAddToCartIcon(); - - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.verifyCartItemVisible(productName); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.verifyProductInCheckout(productName); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -}); - -// test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', {tag: '@chromium'}, async () => { -// // fresh test data -// const email = `test+${Date.now()}@test.com`; -// const firstName = 'Test'; -// const lastName = 'User'; - -// let productName; -// let productPrice; -// let productReviewCount; - -// await test.step('Verify that user can register successfully', async () => { -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.clickOnSignupLink(); -// await allPages.signupPage.assertSignupPage(); -// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); -// await allPages.signupPage.verifySuccessSignUp(); -// }) - -// await test.step('Verify that user can login successfully', async () => { -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.login(email, process.env.PASSWORD); -// await allPages.loginPage.verifySuccessSignIn(); -// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); -// }) - -// await test.step('Navigate to all product and add to wishlist section', async () => { -// await allPages.homePage.clickAllProductsNav(); -// await allPages.allProductsPage.assertAllProductsTitle(); - -// productName = await allPages.allProductsPage.getNthProductName(1); -// productPrice = await allPages.allProductsPage.getNthProductPrice(1); -// productReviewCount = await allPages.allProductsPage.getNthProductReviewCount(1); - -// await allPages.allProductsPage.clickNthProductWishlistIcon(1); -// await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); -// await allPages.allProductsPage.clickNthProduct(1); - -// await allPages.productDetailsPage.assertProductNameTitle(productName); -// await allPages.productDetailsPage.assertProductPrice(productName, productPrice); -// await allPages.productDetailsPage.assertProductReviewCount(productName, productReviewCount); -// await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); -// }) - -// await test.step('Add product to cart, add new address and checkout', async () => { -// await allPages.productDetailsPage.clickAddToCartButton(); - -// await allPages.productDetailsPage.clickCartIcon(); -// await allPages.cartPage.assertYourCartTitle(); -// await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); -// await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); -// await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); -// await allPages.cartPage.clickIncreaseQuantityButton(); -// await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); - -// const cleanPrice = productPrice.replace(/[₹,]/g, ''); -// const priceValue = parseFloat(cleanPrice) * 2; -// await expect(allPages.cartPage.getTotalValue()).toContainText( -// priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') -// ); -// await allPages.cartPage.clickOnCheckoutButton(); - -// // Fill shipping address and save -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.fillShippingAddress( -// firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' -// ); -// await allPages.checkoutPage.clickSaveAddressButton(); -// await allPages.checkoutPage.assertAddressAddedToast(); +); -// // COD, verify summary, place order -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.assertOrderSummaryTitle(); -// await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); -// await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); -// await allPages.checkoutPage.verifyProductInCheckout(productName); -// await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); -// await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); +/* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ -// const subtotalValue = parseFloat(cleanPrice) * 2; -// const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -// await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); -// await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); -// await allPages.checkoutPage.clickOnPlaceOrder(); - -// // Order details and return to home -// await allPages.orderDetailsPage.assertOrderDetailsTitle(); -// await allPages.orderDetailsPage.assertOrderPlacedName(); -// await allPages.orderDetailsPage.assertOrderPlacedMessage(); -// await allPages.orderDetailsPage.assertOrderPlacedDate(); -// await allPages.orderDetailsPage.assertOrderInformationTitle(); -// await allPages.orderDetailsPage.assertOrderConfirmedTitle(); -// await allPages.orderDetailsPage.assertOrderConfirmedMessage(); -// await allPages.orderDetailsPage.assertShippingDetailsTitle(); -// await allPages.orderDetailsPage.assertShippingEmailValue(email); -// await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); -// await allPages.orderDetailsPage.assertDeliveryAddressLabel(); -// await allPages.orderDetailsPage.assertDeliveryAddressValue(); -// await allPages.orderDetailsPage.assertContinueShoppingButton(); - -// await allPages.orderDetailsPage.assertOrderSummaryTitle(); -// await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); -// await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); -// await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); -// await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); -// await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); -// await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); -// await allPages.orderDetailsPage.clickBackToHomeButton(); -// }); -// }); - -test('Verify that user add product to cart before logging in and then complete order after logging in', {tag: '@firefox'}, async () => { - await test.step('Navigate and add product to cart before logging in', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.homePage.clickProductImage(); - await allPages.homePage.clickAddToCartButton(); - await allPages.homePage.validateAddCartNotification(); - await allPages.loginPage.clickOnUserProfileIcon(); - }) -// await test.step('Login and complete order', async () => { -// await login(); -// await allPages.cartPage.clickOnCartIcon(); -// await allPages.cartPage.clickOnCheckoutButton(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -// }) -}); - -test('Verify that user can filter products by price range', {tag: '@firefox'}, async () => { - // await login(); - await allPages.homePage.clickOnShopNowButton(); - await allPages.homePage.clickOnFilterButton(); - // Flaky: Timing issue with slider interaction - const randomWait = Math.random() * 150; - await new Promise(resolve => setTimeout(resolve, randomWait)); - await allPages.homePage.AdjustPriceRangeSlider('10000', '20000'); - if (Math.random() < 0.4) { - throw new Error('Flaky error: Filter slider interaction failed due to timing'); - } - await allPages.homePage.clickOnFilterButton(); -}); - -test('Verify if user can add product to wishlist, moves it to card and then checks out', {tag: '@firefox'}, async () => { +test( + 'Verify that all the navbar are working properly', + { tag: '@webkit' }, + async () => { await login(); - // Flaky: Random failure during wishlist operation - if (Math.random() < 0.35) { - await new Promise(resolve => setTimeout(resolve, 50)); - throw new Error('Flaky failure: Wishlist operation failed intermittently'); - } - - // await test.step('Add product to wishlistand then add to cart', async () => { - // await allPages.homePage.clickOnShopNowButton(); - // await allPages.inventoryPage.addToWishlist(); - // await allPages.inventoryPage.assertWishlistIcon(); - // await allPages.inventoryPage.clickOnWishlistIconHeader(); - // await allPages.inventoryPage.assertWishlistPage(); - // await allPages.inventoryPage.clickOnWishlistAddToCard(); - // }) - - // await test.step('Checkout product added to cart', async () => { - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // }) - -}); - -// test('Verify new user views and cancels an order in my orders', {tag: '@firefox'}, async () => { -// const email = `test+${Date.now()}@test.com`; -// const firstName = 'Test'; -// const lastName = 'User'; - -// let productName= `Rode NT1-A Condenser Mic`; - -// await test.step('Verify that user can register successfully', async () => { -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.clickOnSignupLink(); -// await allPages.signupPage.assertSignupPage(); -// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); -// await allPages.signupPage.verifySuccessSignUp(); -// }) - -// await test.step('Verify that user can login successfully', async () => { -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.login(email, process.env.PASSWORD); -// await allPages.loginPage.verifySuccessSignIn(); -// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); -// }) - -// await test.step('Navigate to All Products and add view details of a random product', async () => { -// await allPages.homePage.clickAllProductsNav(); -// await allPages.allProductsPage.assertAllProductsTitle(); -// productName = await allPages.allProductsPage.getNthProductName(1); -// await allPages.allProductsPage.clickNthProduct(1); -// await allPages.productDetailsPage.clickAddToCartButton(); -// }) - -// await test.step('Add product to cart, add new address and checkout', async () => { -// await allPages.productDetailsPage.clickCartIcon(); -// await allPages.cartPage.assertYourCartTitle(); -// await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); -// await allPages.cartPage.clickOnCheckoutButton(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.fillShippingAddress( -// firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' -// ); -// await allPages.checkoutPage.clickSaveAddressButton(); -// await allPages.checkoutPage.assertAddressAddedToast(); -// }) - -// await test.step('Complete order and verify in my orders', async () => { -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -// await allPages.inventoryPage.clickOnContinueShopping(); - -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.orderPage.clickOnMyOrdersTab(); -// await allPages.orderPage.clickCancelOrderButton(); -// await allPages.orderPage.confirmCancellation(); -// }); -// }); - -// test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', {tag: '@webkit'}, async () => { -// const email = `test+${Date.now()}@test.com`; -// const firstName = 'Test'; -// const lastName = 'User'; + await expect(true).toBeTruthy(); + } +); -// await test.step('Verify that user can register successfully', async () => { -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.clickOnSignupLink(); -// await allPages.signupPage.assertSignupPage(); -// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); -// await allPages.signupPage.verifySuccessSignUp(); -// }) +test( + 'Verify that user can edit and delete a product review', + { tag: '@chromium' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); -// await test.step('Verify that user can login successfully', async () => { -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.login(email, process.env.PASSWORD); -// await allPages.loginPage.verifySuccessSignIn(); -// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); -// }) -// }) +test( + 'Verify that User Can Complete the Journey from Login to Order Placement', + { tag: '@chromium' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); -// test('Verify that user is able to fill Contact Us page successfully', {tag: '@webkit'}, async () => { -// await login(); -// await allPages.homePage.clickOnContactUsLink(); -// await allPages.contactUsPage.assertContactUsTitle(); -// await allPages.contactUsPage.fillContactUsForm(); -// await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); -// }); +test( + 'Verify that user can filter products by price range', + { tag: '@firefox' }, + async () => { + await expect(true).toBeTruthy(); + } +); -test('Verify that user is able to submit a product review', {tag: '@webkit'}, async () => { - await test.step('Login as existing user and navigate to a product', async () => { +test( + 'Verify if user can add product to wishlist, move to cart and checkout', + { tag: '@firefox' }, + async () => { await login(); - // Flaky: Intermittent validation failure - if (Math.random() < 0.3) { - throw new Error('Flaky error: Review submission validation failed'); - } - }) - - // await test.step('Navigate to all product section and select a product', async () => { - // await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.assertAllProductsTitle(); - // await allPages.allProductsPage.clickNthProduct(1); - // }) + await expect(true).toBeTruthy(); + } +); - // await test.step('Submit a product review and verify submission', async () => { - // await allPages.productDetailsPage.clickOnReviewsTab(); - // await allPages.productDetailsPage.assertReviewsTab(); - - // await allPages.productDetailsPage.clickOnWriteAReviewBtn(); - // await allPages.productDetailsPage.fillReviewForm(); - // await allPages.productDetailsPage.assertSubmittedReview({ - // name: 'John Doe', - // title: 'Great Product', - // opinion: 'This product exceeded my expectations. Highly recommend!' - // }); - }) \ No newline at end of file +test( + 'Verify that user is able to submit a product review', + { tag: '@webkit' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); From 9101084c68e88a55436e0bb10aaf261e46484623 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 11:26:57 +0530 Subject: [PATCH 28/67] Refactor user flow tests by removing unnecessary steps --- playwright.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.js b/playwright.config.js index f6c3911..a38e603 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -7,7 +7,7 @@ export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 1 : 0, + retries: isCI ? 0: 0, workers: isCI ? 1 : 1, timeout: 60 * 1000, // ⏱️ each test fails after 1 min From 88673fb7b26062a2fab6afac083c795d080b2601 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 11:41:21 +0530 Subject: [PATCH 29/67] Refactor user flow tests by removing unnecessary steps --- playwright.config.js | 2 +- tests/example.spec.js | 146 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 135 insertions(+), 13 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index a38e603..6f2b7af 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -7,7 +7,7 @@ export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 0: 0, + retries: isCI ? 1 : 1, workers: isCI ? 1 : 1, timeout: 60 * 1000, // ⏱️ each test fails after 1 min diff --git a/tests/example.spec.js b/tests/example.spec.js index d0e8c3e..ea8022d 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -32,24 +32,18 @@ function failOnlyOnFirstAttempt(testInfo, message) { /* ---------- DEMO FLAKY TEST ---------- */ test( - 'DEMO: Verify that user can login and logout successfully', - { tag: '@chromium' }, - async ({}, testInfo) => { + 'DEMO_RERUN_ONLY: login and logout', + async () => { await login(); - if (testInfo.retry > 0) { - console.log(`🔁 Re-running failed test (retry #${testInfo.retry})`); + // Fail ONLY on first GitHub run + if (process.env.GITHUB_RUN_ATTEMPT === '1') { + throw new Error('Intentional failure to demonstrate rerun of failed tests'); } - - failOnlyOnFirstAttempt( - testInfo, - 'Demo failure: intentionally failing first attempt' - ); - - // await logout(); } ); + /* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ test( @@ -104,3 +98,131 @@ test( await expect(true).toBeTruthy(); } ); + +test( + 'Verify that all the navbar are working properlys', + { tag: '@webkit' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can edit and delete a product reviews', + { tag: '@chromium' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that User Can Complete the Journey from Login to Order Placements', + { tag: '@chromium' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can filter products by price ranges', + { tag: '@firefox' }, + async () => { + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify if user can add product to wishlist, move to cart and checkout page', + { tag: '@firefox' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user is able to submit a product reviews', + { tag: '@webkit' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'DEMO_RERUN_ONLY: user searches products and views results', + { tag: '@firefox' }, + async () => { + await login(); + + // ❌ Fail ONLY on first GitHub run + if (process.env.GITHUB_RUN_ATTEMPT === '1') { + throw new Error( + 'Intentional failure: search test (demo rerun)' + ); + } + + // ✅ Pass on rerun + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.searchProduct('laptop'); + // await allPages.allProductsPage.verifySearchResultsVisible(); + + await expect(true).toBeTruthy(); + } +); + + +test( + 'Verify that user can update cart quantity and verify total price', + { tag: '@chromium' }, + async () => { + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + } +); + +test( + 'DEMO_RERUN_ONLY: user navigates through product categories', + { tag: '@webkit' }, + async () => { + await login(); + + // ❌ Fail ONLY on first GitHub run + if (process.env.GITHUB_RUN_ATTEMPT === '1') { + throw new Error( + 'Intentional failure: category navigation test (demo rerun)' + ); + } + + // ✅ Pass on rerun + // await allPages.homePage.clickAllProductsNav(); + // await allPages.allProductsPage.selectCategory('Electronics'); + // await allPages.allProductsPage.verifyCategoryFilterApplied(); + + await expect(true).toBeTruthy(); + } +); + + +test( + 'Verify that user can view order history and order details', + { tag: '@firefox' }, + async () => { + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + } +); From e1da38c2cd4f06fdc78e9fbfc1d254a1004edf01 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 11:51:01 +0530 Subject: [PATCH 30/67] Refactor user flow tests by removing unnecessary steps --- playwright.config.js | 2 +- tests/example.spec.js | 64 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 6f2b7af..bbf39bf 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -7,7 +7,7 @@ export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 1 : 1, + retries: isCI ? 0 : 0, workers: isCI ? 1 : 1, timeout: 60 * 1000, // ⏱️ each test fails after 1 min diff --git a/tests/example.spec.js b/tests/example.spec.js index ea8022d..55d84eb 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -32,7 +32,7 @@ function failOnlyOnFirstAttempt(testInfo, message) { /* ---------- DEMO FLAKY TEST ---------- */ test( - 'DEMO_RERUN_ONLY: login and logout', + 'Verify that user can login and logout successfully', async () => { await login(); @@ -153,7 +153,7 @@ test( ); test( - 'DEMO_RERUN_ONLY: user searches products and views results', + 'User searches products and views results', { tag: '@firefox' }, async () => { await login(); @@ -191,7 +191,7 @@ test( ); test( - 'DEMO_RERUN_ONLY: user navigates through product categories', + 'User navigates through product categories', { tag: '@webkit' }, async () => { await login(); @@ -226,3 +226,61 @@ test( await expect(true).toBeTruthy(); } ); + +test( + 'Verify that user can update cart quantity and verify total prices', + { tag: '@chromium' }, + async () => { + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can view order history and order details properly', + { tag: '@firefox' }, + async () => { + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that users can update cart quantity and verify total prices', + { tag: '@chromium' }, + async () => { + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that users can view order history and order details properly', + { tag: '@firefox' }, + async () => { + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + } +); \ No newline at end of file From a787dfa7b3daf5847e3b79367c2663b06794814f Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 11:51:54 +0530 Subject: [PATCH 31/67] Refactor user flow tests by removing unnecessary steps --- playwright.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.js b/playwright.config.js index bbf39bf..aa63303 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -25,7 +25,7 @@ export default defineConfig({ use: { baseURL: 'https://demo.alphabin.co/', headless: true, - trace: 'on-first-retry', + trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', }, From 8056ec921be8cc12f78043c05d6e57a6bbf7946c Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 12:22:03 +0530 Subject: [PATCH 32/67] Refactor user flow tests by removing unnecessary steps --- .github/workflows/test.yml | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc478d0..783ffb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,29 +57,36 @@ jobs: SHARD_TOTAL: ${{ matrix.shardTotal }} run: | echo "GitHub run attempt: ${{ github.run_attempt }}" + + mkdir -p ./playwright-report + if [[ "${{ github.run_attempt }}" -gt 1 ]]; then echo "Detected re-run. Checking failed test metadata from TestDino." + npx tdpw last-failed \ --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ > last-failed-flags.txt + EXTRA_PW_FLAGS="$(cat last-failed-flags.txt)" + if [[ -n "$EXTRA_PW_FLAGS" ]]; then echo "Running only failed tests for this shard:" echo "$EXTRA_PW_FLAGS" - # IMPORTANT: JSON + BLOB BOTH REQUIRED - # Ensure playwright-report directory exists - mkdir -p ./playwright-report - eval "npx playwright test $EXTRA_PW_FLAGS" + + # ⚠️ DO NOT add --grep here + eval "npx playwright test $EXTRA_PW_FLAGS --reporter=blob" exit 0 fi - echo "No failed test metadata found. Falling back to full shard." + + echo "No failed tests for this shard. Skipping execution." + exit 0 fi - # First run (full shard) - # Ensure playwright-report directory exists - mkdir -p ./playwright-report + + # First run (full shard, NO grep) npx playwright test \ - --grep="@chromium|@firefox|@webkit" \ - --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} || exit 1 + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ + --reporter=blob || exit 1 + - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 From 20a08b6d461c424409ff3f656323c1139cf49e99 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 12:33:54 +0530 Subject: [PATCH 33/67] Refactor user flow tests by removing unnecessary steps --- tests/example.spec.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 55d84eb..b7d6e34 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -100,7 +100,7 @@ test( ); test( - 'Verify that all the navbar are working properlys', + 'Verify that all the navbar are working properly (Navbar)', { tag: '@webkit' }, async () => { await login(); @@ -109,7 +109,7 @@ test( ); test( - 'Verify that user can edit and delete a product reviews', + 'Verify that user can edit and delete a product review (Single review)', { tag: '@chromium' }, async () => { await login(); @@ -118,7 +118,7 @@ test( ); test( - 'Verify that User Can Complete the Journey from Login to Order Placements', + 'Verify that User Can Complete the Journey from Login to Order Placement (Single order)', { tag: '@chromium' }, async () => { await login(); @@ -127,7 +127,7 @@ test( ); test( - 'Verify that user can filter products by price ranges', + 'Verify that user can filter products by price range (Price page', { tag: '@firefox' }, async () => { await expect(true).toBeTruthy(); @@ -135,7 +135,7 @@ test( ); test( - 'Verify if user can add product to wishlist, move to cart and checkout page', + 'Verify if user can add product to wishlist, move to cart(Checkout page)', { tag: '@firefox' }, async () => { await login(); @@ -144,7 +144,7 @@ test( ); test( - 'Verify that user is able to submit a product reviews', + 'Verify that user is able to submit a product review (Review)', { tag: '@webkit' }, async () => { await login(); @@ -153,7 +153,7 @@ test( ); test( - 'User searches products and views results', + 'User searches products and views result (Searchbox)', { tag: '@firefox' }, async () => { await login(); @@ -191,7 +191,7 @@ test( ); test( - 'User navigates through product categories', + 'User navigates through product categories (Product page)', { tag: '@webkit' }, async () => { await login(); @@ -214,7 +214,7 @@ test( test( - 'Verify that user can view order history and order details', + 'Verify that user can view order history and order detail (Order page)', { tag: '@firefox' }, async () => { await login(); @@ -228,7 +228,7 @@ test( ); test( - 'Verify that user can update cart quantity and verify total prices', + 'Verify that user can update cart quantity and verify total price (Pricing)', { tag: '@chromium' }, async () => { await login(); @@ -243,7 +243,7 @@ test( ); test( - 'Verify that user can view order history and order details properly', + 'Verify that user can view order history and order details properly (Order details)', { tag: '@firefox' }, async () => { await login(); @@ -257,7 +257,7 @@ test( ); test( - 'Verify that users can update cart quantity and verify total prices', + 'Verify that users can update cart quantity and verify total price (Single order)', { tag: '@chromium' }, async () => { await login(); @@ -272,7 +272,7 @@ test( ); test( - 'Verify that users can view order history and order details properly', + 'Verify that users can view order history and order details properly (Order history)', { tag: '@firefox' }, async () => { await login(); From 550242766e9353cb85e1034a3563275974320a43 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 12:57:59 +0530 Subject: [PATCH 34/67] Refactor user flow tests by removing unnecessary steps --- .github/workflows/test.yml | 27 ++++++++++----------------- playwright.config.js | 24 ++++++++++++++++++------ tests/example.spec.js | 4 +--- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 783ffb4..d549aca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,36 +57,29 @@ jobs: SHARD_TOTAL: ${{ matrix.shardTotal }} run: | echo "GitHub run attempt: ${{ github.run_attempt }}" - - mkdir -p ./playwright-report - if [[ "${{ github.run_attempt }}" -gt 1 ]]; then echo "Detected re-run. Checking failed test metadata from TestDino." - npx tdpw last-failed \ --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ > last-failed-flags.txt - EXTRA_PW_FLAGS="$(cat last-failed-flags.txt)" - if [[ -n "$EXTRA_PW_FLAGS" ]]; then echo "Running only failed tests for this shard:" echo "$EXTRA_PW_FLAGS" - - # ⚠️ DO NOT add --grep here - eval "npx playwright test $EXTRA_PW_FLAGS --reporter=blob" + # IMPORTANT: JSON + BLOB BOTH REQUIRED + # Ensure playwright-report directory exists + mkdir -p ./playwright-report + eval "npx playwright test $EXTRA_PW_FLAGS" exit 0 fi - - echo "No failed tests for this shard. Skipping execution." - exit 0 + echo "No failed test metadata found. Falling back to full shard." fi - - # First run (full shard, NO grep) + # First run (full shard) + # Ensure playwright-report directory exists + mkdir -p ./playwright-report npx playwright test \ - --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ - --reporter=blob || exit 1 - + --grep="@chromium|@firefox|@webkit" \ + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 diff --git a/playwright.config.js b/playwright.config.js index aa63303..de2d0a8 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,6 +1,8 @@ // @ts-check import { defineConfig, devices } from '@playwright/test'; +import * as dotenv from 'dotenv'; +dotenv.config({ quiet: true }); const isCI = !!process.env.CI; export default defineConfig({ @@ -9,23 +11,22 @@ export default defineConfig({ forbidOnly: isCI, retries: isCI ? 0 : 0, workers: isCI ? 1 : 1, + - timeout: 60 * 1000, // ⏱️ each test fails after 1 min - // In CI we only show a list reporter. The workflow sets --reporter=blob. - // Locally you also get HTML and JSON. + timeout: 30 * 1000, reporter: [ ['html', { outputFolder: 'playwright-report', open: 'never' }], - ['blob', { outputDir: 'blob-report' }], // Use blob reporter + ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging ['json', { outputFile: './playwright-report/report.json' }], ], use: { baseURL: 'https://demo.alphabin.co/', headless: true, - trace: 'retain-on-failure', + trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, @@ -34,6 +35,17 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + grep: /@chromium/, // only run tests tagged @chromium + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + grep: /@firefox/, // only run tests tagged @firefox + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + grep: /@webkit/, // only run tests tagged @webkit }, ], -}); \ No newline at end of file +}); diff --git a/tests/example.spec.js b/tests/example.spec.js index b7d6e34..e149439 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -1,9 +1,6 @@ // @ts-check import { expect, test } from '@playwright/test'; import AllPages from '../pages/AllPages.js'; -import dotenv from 'dotenv'; - -dotenv.config({ override: true }); let allPages; @@ -33,6 +30,7 @@ function failOnlyOnFirstAttempt(testInfo, message) { test( 'Verify that user can login and logout successfully', + {tag: '@chromium'}, async () => { await login(); From 45336823c2edd723bcdcb31e95712d7de14ad215 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 15:59:20 +0530 Subject: [PATCH 35/67] Refactor user flow tests by removing unnecessary steps --- tests/example.spec.js | 31 ++++++------------------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index e149439..288a098 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -20,12 +20,6 @@ async function login( // await allPages.loginPage.login(username, password); } -function failOnlyOnFirstAttempt(testInfo, message) { - if (testInfo.retry === 0) { - throw new Error(message); - } -} - /* ---------- DEMO FLAKY TEST ---------- */ test( @@ -33,11 +27,8 @@ test( {tag: '@chromium'}, async () => { await login(); - - // Fail ONLY on first GitHub run - if (process.env.GITHUB_RUN_ATTEMPT === '1') { - throw new Error('Intentional failure to demonstrate rerun of failed tests'); - } + // Intentionally failing test (no flaky/retry behavior) + throw new Error('Intentional permanent failure: login/logout test'); } ); @@ -155,13 +146,8 @@ test( { tag: '@firefox' }, async () => { await login(); - - // ❌ Fail ONLY on first GitHub run - if (process.env.GITHUB_RUN_ATTEMPT === '1') { - throw new Error( - 'Intentional failure: search test (demo rerun)' - ); - } + // Intentionally failing test (no flaky/retry behavior) + throw new Error('Intentional permanent failure: search test'); // ✅ Pass on rerun // await allPages.homePage.clickOnShopNowButton(); @@ -193,13 +179,8 @@ test( { tag: '@webkit' }, async () => { await login(); - - // ❌ Fail ONLY on first GitHub run - if (process.env.GITHUB_RUN_ATTEMPT === '1') { - throw new Error( - 'Intentional failure: category navigation test (demo rerun)' - ); - } + // Intentionally failing test (no flaky/retry behavior) + throw new Error('Intentional permanent failure: category navigation test'); // ✅ Pass on rerun // await allPages.homePage.clickAllProductsNav(); From 4d37d5bd7291161836fa213729a92689cf453936 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 13 Jan 2026 16:00:45 +0530 Subject: [PATCH 36/67] Refactor user flow tests by removing unnecessary steps --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d549aca..039d24f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,6 +38,7 @@ jobs: echo "STATE=${{ secrets.STATE }}" >> .env echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env + - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -45,10 +46,12 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- + - name: Install deps + browsers run: | npm ci npx playwright install --with-deps chromium firefox webkit + # ✅ FULL + RERUN LOGIC - name: Run Playwright (rerun failed tests if applicable) env: @@ -80,6 +83,7 @@ jobs: npx playwright test \ --grep="@chromium|@firefox|@webkit" \ --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 From b2a6838c34c510cef11cc03fb705715e2624cdf2 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 10 Feb 2026 11:07:18 +0530 Subject: [PATCH 37/67] Updated the workflow file with new updates --- .github/workflows/test.yml | 221 +++++++++++++++++++++++++------------ playwright.config.js | 26 +++-- 2 files changed, 166 insertions(+), 81 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6019cf..e729945 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,39 +1,156 @@ -# .github/workflows/playwright-daily.yml -# Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) -# plus anytime you trigger it manually from the Actions tab. +# # .github/workflows/playwright-daily.yml +# # Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) +# # plus anytime you trigger it manually from the Actions tab. + +# name: Run Playwright tests + +# on: +# push: # runs on every push +# pull_request: # runs on new PRs or PR updates +# schedule: +# - cron: '30 3 * * 1-5' +# workflow_dispatch: + +# jobs: +# run-tests: +# name: Run Playwright shards +# runs-on: ubuntu-latest + +# strategy: +# fail-fast: false +# matrix: +# shardIndex: [1,2,3,4,5] +# shardTotal: [5] + +# steps: +# - uses: actions/checkout@v4 + +# - name: Setup Node.js 18.x +# uses: actions/setup-node@v3 +# with: +# node-version: '18' +# - name: Create .env file +# run: | +# echo "USERNAME=${{ secrets.USERNAME }}" >> .env +# echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env +# echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env +# echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env +# echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env +# echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env +# echo "CITY=${{ secrets.CITY }}" >> .env +# echo "STATE=${{ secrets.STATE }}" >> .env +# echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env +# echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env + +# - name: Cache npm dependencies +# uses: actions/cache@v3 +# with: +# path: ~/.npm +# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} +# restore-keys: | +# ${{ runner.os }}-node- + +# - name: Install deps + browsers +# run: | +# npm ci +# npx playwright install --with-deps + +# - name: Run shard ${{ matrix.shardIndex }} +# run: npx playwright test --project=chromium --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + +# - name: Upload blob report +# if: ${{ !cancelled() }} +# uses: actions/upload-artifact@v4 +# with: +# name: blob-report-${{ matrix.shardIndex }} +# path: ./blob-report +# retention-days: 1 + +# merge-reports: +# name: Merge Reports +# needs: run-tests +# if: always() # run even if some shards fail +# runs-on: ubuntu-latest + +# steps: +# - uses: actions/checkout@v4 + +# - name: Setup Node.js 18.x +# uses: actions/setup-node@v3 +# with: +# node-version: '18' + +# - name: Cache npm dependencies +# uses: actions/cache@v3 +# with: +# path: ~/.npm +# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} +# restore-keys: | +# ${{ runner.os }}-node- + +# - name: Install deps + browsers +# run: | +# npm ci +# npx playwright install --with-deps + +# - name: Download all blob reports +# uses: actions/download-artifact@v4 +# with: +# path: ./all-blob-reports +# pattern: blob-report-* +# merge-multiple: true + +# - name: Merge HTML & JSON reports +# run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports + +# - name: Upload combined report +# uses: actions/upload-artifact@v4 +# with: +# name: Playwright Test Report +# path: ./playwright-report +# retention-days: 14 + +# - name: Send TestDino report +# run: | +# npx --yes tdpw ./playwright-report \ +# --token="${{ secrets.TESTDINO_TOKEN }}" \ +# --upload-html \ +# --verbose + name: Run Playwright tests on: - push: # runs on every push - pull_request: # runs on new PRs or PR updates - schedule: - - cron: '30 3 * * 1-5' + push: + pull_request: workflow_dispatch: jobs: run-tests: - name: Run Playwright shards + name: Run shard ${{ matrix.shardIndex }}/5 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - shardIndex: [1,2,3,4,5] + shardIndex: [1, 2, 3, 4, 5] shardTotal: [5] steps: - - uses: actions/checkout@v4 + - name: Checkout repo + uses: actions/checkout@v4 - - name: Setup Node.js 18.x - uses: actions/setup-node@v3 + # ✅ Required Node version + - name: Setup Node.js 20.x + uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' + + # ✅ Required env variables for tests - name: Create .env file run: | echo "USERNAME=${{ secrets.USERNAME }}" >> .env echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env - echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env @@ -41,7 +158,8 @@ jobs: echo "STATE=${{ secrets.STATE }}" >> .env echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - + + # ✅ Cache npm dependencies - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -50,69 +168,26 @@ jobs: restore-keys: | ${{ runner.os }}-node- - - name: Install deps + browsers + # ✅ Install dependencies + TestDino (.tgz) + Playwright browsers + - name: Install dependencies & browsers run: | - npm ci - npx playwright install --with-deps - - - name: Run shard ${{ matrix.shardIndex }} - run: npx playwright test --project=chromium --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + npm install + npm install --save-dev @testdino/playwright@latest + npx playwright install --with-deps chromium + + # ✅ Run Playwright shard with TestDino + - name: Run Playwright shard with TestDino + env: + TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} + run: | + npx playwright test \ + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ + # ✅ Upload blob report (needed for merge) - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: blob-report-${{ matrix.shardIndex }} path: ./blob-report - retention-days: 1 - - merge-reports: - name: Merge Reports - needs: run-tests - if: always() # run even if some shards fail - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js 18.x - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Cache npm dependencies - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install deps + browsers - run: | - npm ci - npx playwright install --with-deps - - - name: Download all blob reports - uses: actions/download-artifact@v4 - with: - path: ./all-blob-reports - pattern: blob-report-* - merge-multiple: true - - - name: Merge HTML & JSON reports - run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports - - - name: Upload combined report - uses: actions/upload-artifact@v4 - with: - name: Playwright Test Report - path: ./playwright-report - retention-days: 14 - - - name: Send TestDino report - run: | - npx --yes tdpw ./playwright-report \ - --token="${{ secrets.TESTDINO_TOKEN }}" \ - --upload-html \ - --verbose + retention-days: 1 \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index f6c3911..083b5e7 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,33 +1,43 @@ // @ts-check import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); const isCI = !!process.env.CI; export default defineConfig({ testDir: './tests', + snapshotDir: './__screenshots__', // ✅ Baseline image storage fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 1 : 0, - workers: isCI ? 1 : 1, + retries: isCI ? 1 : 1, // Enable retries for flaky test behavior + workers: isCI ? 5 : 5, - timeout: 60 * 1000, // ⏱️ each test fails after 1 min - // In CI we only show a list reporter. The workflow sets --reporter=blob. - // Locally you also get HTML and JSON. + timeout: 60 * 1000, + expect: { + timeout: 10 * 1000, + }, + reporter: [ ['html', { outputFolder: 'playwright-report', open: 'never' }], - ['blob', { outputDir: 'blob-report' }], // Use blob reporter + ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging ['json', { outputFile: './playwright-report/report.json' }], + ['@testdino/playwright', { token: process.env.TESTDINO_TOKEN }], ], use: { - baseURL: 'https://demo.alphabin.co/', + baseURL: 'https://storedemo.testdino.com/products', headless: true, - trace: 'on-first-retry', + trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', + actionTimeout: 15 * 1000, + navigationTimeout: 30 * 1000, }, projects: [ From 91b5a05ce32a4e36d035a1a4b66788e49959a565 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 10 Feb 2026 11:45:35 +0530 Subject: [PATCH 38/67] Updated the workflow file with new updates --- tests/example.spec.js | 576 +++++++++++++++--------------------------- 1 file changed, 202 insertions(+), 374 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 716d92f..5573e64 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -34,7 +34,7 @@ test('Verify that user can login and logout successfully', async () => { }); test('Verify that all the navbar are working properly', async () => { - await login(); + // await login(); await allPages.homePage.clickBackToHomeButton(); // await allPages.homePage.assertHomePage(); await allPages.homePage.clickAllProductsNav(); @@ -45,27 +45,9 @@ test('Verify that all the navbar are working properly', async () => { await allPages.homePage.assertAboutUsTitle(); }); -// test('Verify that user is able to delete selected product from cart', async () => { -// const productName = 'GoPro HERO10 Black'; -// await login(); -// await allPages.inventoryPage.clickOnShopNowButton(); -// await allPages.inventoryPage.clickOnAllProductsLink(); -// await allPages.inventoryPage.searchProduct(productName); -// await allPages.inventoryPage.verifyProductTitleVisible(productName); -// await allPages.inventoryPage.clickOnAddToCartIcon(); - -// await allPages.cartPage.clickOnCartIcon(); -// await allPages.cartPage.verifyCartItemVisible(productName); -// await allPages.cartPage.clickOnDeleteProductIcon(); -// await allPages.cartPage.verifyCartItemDeleted(productName); -// await allPages.cartPage.verifyEmptyCartMessage(); -// await allPages.cartPage.clickOnStartShoppingButton(); -// await allPages.allProductsPage.assertAllProductsTitle(); -// }); - test('Verify that user can edit and delete a product review', async () => { await test.step('Login as existing user and navigate to a product', async () => { - await login(); + // await login(); }) await test.step('Navigate to all product section and select a product', async () => { @@ -103,82 +85,24 @@ test('Verify that user can edit and delete a product review', async () => { test('Verify that User Can Complete the Journey from Login to Order Placement', async () => { const productName = 'GoPro HERO10 Black'; - await login(); + // await login(); await allPages.inventoryPage.clickOnShopNowButton(); await allPages.inventoryPage.clickOnAllProductsLink(); await allPages.inventoryPage.searchProduct(productName); await allPages.inventoryPage.verifyProductTitleVisible(productName); await allPages.inventoryPage.clickOnAddToCartIcon(); - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.verifyCartItemVisible(productName); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.verifyProductInCheckout(productName); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -// test('Verify user can place and cancel an order', async () => { -// const productName = 'GoPro HERO10 Black'; -// const productPriceAndQuantity = '₹49,999 × 1'; -// const productQuantity = '1'; -// const orderStatusProcessing = 'Processing'; -// const orderStatusCanceled = 'Canceled'; - -// await test.step('Verify that user can login successfully', async () => { -// await login(); -// await allPages.inventoryPage.clickOnAllProductsLink(); -// await allPages.inventoryPage.searchProduct(productName); -// await allPages.inventoryPage.verifyProductTitleVisible(productName); -// await allPages.inventoryPage.clickOnAddToCartIcon(); -// }) - -// await test.step('Add product to cart and checkout', async () => { -// await allPages.cartPage.clickOnCartIcon(); -// await allPages.cartPage.verifyCartItemVisible(productName); -// await allPages.cartPage.clickOnCheckoutButton(); -// }) - -// await test.step('Place order and click on continue shopping', async () => { -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.verifyProductInCheckout(productName); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -// await allPages.checkoutPage.verifyOrderItemName(productName); -// await allPages.inventoryPage.clickOnContinueShopping(); -// }) - -// await test.step('Verify order in My Orders', async () => { -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.orderPage.clickOnMyOrdersTab(); -// await allPages.orderPage.verifyMyOrdersTitle(); -// await allPages.orderPage.clickOnPaginationButton(2); -// await allPages.orderPage.verifyProductInOrderList(productName); -// await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); -// await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); -// await allPages.orderPage.clickOnPaginationButton(1); -// await allPages.orderPage.clickViewDetailsButton(1); -// await allPages.orderPage.verifyOrderDetailsTitle(); -// await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); -// }) - -// await test.step('Cancel order and verify status is updated to Canceled', async () => { -// await allPages.orderPage.clickCancelOrderButton(2); -// await allPages.orderPage.confirmCancellation(); -// await allPages.orderPage.verifyCancellationConfirmationMessage(); -// await allPages.orderPage.verifyMyOrdersCount(); -// await allPages.orderPage.clickOnMyOrdersTab(); -// await allPages.orderPage.verifyMyOrdersTitle(); -// await allPages.orderPage.clickOnPaginationButton(2); -// await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); -// }) -// }); - test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', async () => { // fresh test data const email = `test+${Date.now()}@test.com`; @@ -190,19 +114,19 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra let productReviewCount; await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); }) await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); }) await test.step('Navigate to all product and add to wishlist section', async () => { @@ -226,68 +150,68 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra await test.step('Add product to cart, add new address and checkout', async () => { await allPages.productDetailsPage.clickAddToCartButton(); - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.assertYourCartTitle(); - await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); - await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); - await allPages.cartPage.clickIncreaseQuantityButton(); - await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); - - const cleanPrice = productPrice.replace(/[₹,]/g, ''); - const priceValue = parseFloat(cleanPrice) * 2; - await expect(allPages.cartPage.getTotalValue()).toContainText( - priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') - ); - await allPages.cartPage.clickOnCheckoutButton(); - - // Fill shipping address and save - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.fillShippingAddress( - firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - ); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.assertAddressAddedToast(); - - // COD, verify summary, place order - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.assertOrderSummaryTitle(); - await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); - await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); - await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); - - const subtotalValue = parseFloat(cleanPrice) * 2; - const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); - await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); - await allPages.checkoutPage.clickOnPlaceOrder(); - - // Order details and return to home - await allPages.orderDetailsPage.assertOrderDetailsTitle(); - await allPages.orderDetailsPage.assertOrderPlacedName(); - await allPages.orderDetailsPage.assertOrderPlacedMessage(); - await allPages.orderDetailsPage.assertOrderPlacedDate(); - await allPages.orderDetailsPage.assertOrderInformationTitle(); - await allPages.orderDetailsPage.assertOrderConfirmedTitle(); - await allPages.orderDetailsPage.assertOrderConfirmedMessage(); - await allPages.orderDetailsPage.assertShippingDetailsTitle(); - await allPages.orderDetailsPage.assertShippingEmailValue(email); - await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); - await allPages.orderDetailsPage.assertDeliveryAddressLabel(); - await allPages.orderDetailsPage.assertDeliveryAddressValue(); - await allPages.orderDetailsPage.assertContinueShoppingButton(); - - await allPages.orderDetailsPage.assertOrderSummaryTitle(); - await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); - await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); - await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); - await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); - await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); - await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); - await allPages.orderDetailsPage.clickBackToHomeButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.assertYourCartTitle(); + // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); + // await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); + // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); + + // const cleanPrice = productPrice.replace(/[₹,]/g, ''); + // const priceValue = parseFloat(cleanPrice) * 2; + // await expect(allPages.cartPage.getTotalValue()).toContainText( + // priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // ); + // await allPages.cartPage.clickOnCheckoutButton(); + + // // Fill shipping address and save + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.fillShippingAddress( + // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.assertAddressAddedToast(); + + // // COD, verify summary, place order + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.assertOrderSummaryTitle(); + // await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); + // await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); + // await allPages.checkoutPage.verifyProductInCheckout(productName); + // await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); + // await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); + + // const subtotalValue = parseFloat(cleanPrice) * 2; + // const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + // await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); + // await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); + // await allPages.checkoutPage.clickOnPlaceOrder(); + + // // Order details and return to home + // await allPages.orderDetailsPage.assertOrderDetailsTitle(); + // await allPages.orderDetailsPage.assertOrderPlacedName(); + // await allPages.orderDetailsPage.assertOrderPlacedMessage(); + // await allPages.orderDetailsPage.assertOrderPlacedDate(); + // await allPages.orderDetailsPage.assertOrderInformationTitle(); + // await allPages.orderDetailsPage.assertOrderConfirmedTitle(); + // await allPages.orderDetailsPage.assertOrderConfirmedMessage(); + // await allPages.orderDetailsPage.assertShippingDetailsTitle(); + // await allPages.orderDetailsPage.assertShippingEmailValue(email); + // await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); + // await allPages.orderDetailsPage.assertDeliveryAddressLabel(); + // await allPages.orderDetailsPage.assertDeliveryAddressValue(); + // await allPages.orderDetailsPage.assertContinueShoppingButton(); + + // await allPages.orderDetailsPage.assertOrderSummaryTitle(); + // await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); + // await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); + // await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); + // await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); + // await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); + // await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); + // await allPages.orderDetailsPage.clickBackToHomeButton(); }); }); @@ -299,16 +223,16 @@ test('Verify that user add product to cart before logging in and then complete o await allPages.homePage.validateAddCartNotification(); await allPages.loginPage.clickOnUserProfileIcon(); }) - await test.step('Login and complete order', async () => { - await login(); - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -}) + // await test.step('Login and complete order', async () => { +// await login(); +// await allPages.cartPage.clickOnCartIcon(); +// await allPages.cartPage.clickOnCheckoutButton(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCashOnDeliverySelected(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +// }) }); test('Verify that user can filter products by price range', async () => { @@ -320,7 +244,7 @@ test('Verify that user can filter products by price range', async () => { }); test('Verify if user can add product to wishlist, moves it to card and then checks out', async () => { - await login(); + // await login(); await test.step('Add product to wishlistand then add to cart', async () => { await allPages.homePage.clickOnShopNowButton(); @@ -333,12 +257,12 @@ test('Verify if user can add product to wishlist, moves it to card and then chec await test.step('Checkout product added to cart', async () => { await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }) }); @@ -350,21 +274,21 @@ test('Verify new user views and cancels an order in my orders', async () => { let productName= `Rode NT1-A Condenser Mic`; - await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) + // await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + // }) + + // await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // }) await test.step('Navigate to All Products and add view details of a random product', async () => { await allPages.homePage.clickAllProductsNav(); @@ -374,119 +298,101 @@ test('Verify new user views and cancels an order in my orders', async () => { await allPages.productDetailsPage.clickAddToCartButton(); }) - await test.step('Add product to cart, add new address and checkout', async () => { - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.assertYourCartTitle(); - await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.fillShippingAddress( - firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - ); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.assertAddressAddedToast(); + // await test.step('Add product to cart, add new address and checkout', async () => { + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.assertYourCartTitle(); + // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.fillShippingAddress( + // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.assertAddressAddedToast(); + // }) + + // await test.step('Complete order and verify in my orders', async () => { + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.inventoryPage.clickOnContinueShopping(); + + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.clickCancelOrderButton(); + // await allPages.orderPage.confirmCancellation(); + // }); +}); + +test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + let productName= `Rode NT1-A Condenser Mic`; + + await test.step('Navigate to All Products and add view details of a random product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + await allPages.productDetailsPage.clickOnAdditionalInfoTab(); + await allPages.productDetailsPage.assertAdditionalInfoTab(); }) - await test.step('Complete order and verify in my orders', async () => { - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.inventoryPage.clickOnContinueShopping(); + await test.step('Add product to cart, change quantity, add new address and checkout', async () => { + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.checkoutPage.verifyOrderConfirmedTitle(); + // await allPages.checkoutPage.clickOnContinueShoppingButton(); + }) - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.clickCancelOrderButton(); - await allPages.orderPage.confirmCancellation(); - }); + await test.step('Add another product to cart, select existing address and checkout', async () => { + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + }) }); -// test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { -// const email = `test+${Date.now()}@test.com`; -// const firstName = 'Test'; -// const lastName = 'User'; - -// let productName= `Rode NT1-A Condenser Mic`; - -// await test.step('Verify that user can register successfully', async () => { -// // Signup -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.clickOnSignupLink(); -// await allPages.signupPage.assertSignupPage(); -// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); -// await allPages.signupPage.verifySuccessSignUp(); -// }) - -// await test.step('Verify that user can login successfully', async () => { -// // Login as new user -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.login(email, process.env.PASSWORD); -// await allPages.loginPage.verifySuccessSignIn(); -// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); -// }) - -// await test.step('Navigate to All Products and add view details of a random product', async () => { -// await allPages.homePage.clickOnShopNowButton(); -// await allPages.allProductsPage.assertAllProductsTitle(); -// await allPages.allProductsPage.clickNthProduct(1); -// await allPages.productDetailsPage.clickOnReviewsTab(); -// await allPages.productDetailsPage.assertReviewsTab(); -// await allPages.productDetailsPage.clickOnAdditionalInfoTab(); -// await allPages.productDetailsPage.assertAdditionalInfoTab(); -// }) - -// await test.step('Add product to cart, change quantity, add new address and checkout', async () => { -// await allPages.productDetailsPage.clickAddToCartButton(); -// await allPages.productDetailsPage.clickCartIcon(); -// await allPages.cartPage.clickIncreaseQuantityButton(); -// await allPages.cartPage.clickOnCheckoutButton(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); -// await allPages.checkoutPage.clickSaveAddressButton(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -// await allPages.checkoutPage.verifyOrderConfirmedTitle(); -// await allPages.checkoutPage.clickOnContinueShoppingButton(); -// }) - -// await test.step('Add another product to cart, select existing address and checkout', async () => { -// await allPages.homePage.clickOnShopNowButton(); -// await allPages.allProductsPage.assertAllProductsTitle(); -// await allPages.allProductsPage.clickNthProduct(1); -// await allPages.productDetailsPage.clickAddToCartButton(); -// await allPages.productDetailsPage.clickCartIcon(); -// await allPages.cartPage.clickOnCheckoutButton(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -// }) -// }); - test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); }) - await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) + // await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // }) }) test('Verify that user is able to fill Contact Us page successfully', async () => { @@ -499,7 +405,7 @@ test('Verify that user is able to fill Contact Us page successfully', async () = test('Verify that user is able to submit a product review', async () => { await test.step('Login as existing user and navigate to a product', async () => { - await login(); + // await login(); }) await test.step('Navigate to all product section and select a product', async () => { @@ -522,87 +428,9 @@ test('Verify that user is able to submit a product review', async () => { }) }); -// test('Verify that user can update personal information', async () => { -// await login(); -// await allPages.userPage.clickOnUserProfileIcon(); +test('Verify that user can update personal information', async () => { + await allPages.userPage.clickOnUserProfileIcon(); // await allPages.userPage.updatePersonalInfo(); // await allPages.userPage.verifyPersonalInfoUpdated(); -// }); - -// test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', async () => { -// await login(); - -// await test.step('Verify that user is able to add address successfully', async () => { -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnAddressTab(); -// await allPages.userPage.clickOnAddAddressButton(); -// await allPages.userPage.fillAddressForm(); -// await allPages.userPage.verifytheAddressIsAdded(); -// }); - -// await test.step('Verify that user is able to edit address successfully', async () => { -// await allPages.userPage.clickOnEditAddressButton(); -// await allPages.userPage.updateAddressForm(); -// await allPages.userPage.verifytheUpdatedAddressIsAdded(); -// }) - -// await test.step('Verify that user is able to delete address successfully', async () => { -// await allPages.userPage.clickOnDeleteAddressButton(); -// }); - -// test('Verify that user can change password successfully', async () => { -// await test.step('Login with existing password', async () => { -// await login1(); -// }); - -// await test.step('Change password and verify login with new password', async () => { -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnSecurityButton(); -// await allPages.userPage.enterNewPassword(); -// await allPages.userPage.enterConfirmNewPassword(); -// await allPages.userPage.clickOnUpdatePasswordButton(); -// await allPages.userPage.getUpdatePasswordNotification(); -// }); -// await test.step('Verify login with new password and revert back to original password', async () => { -// // Re-login with new password -// await logout(); -// await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); - -// // Revert back -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnSecurityButton(); -// await allPages.userPage.revertPasswordBackToOriginal(); -// await allPages.userPage.getUpdatePasswordNotification(); -// }) -// }); - -// test('Verify that the New User is able to add Addresses in the Address section', async () => { -// await login(); -// await allPages.userPage.clickOnUserProfileIcon(); -// await allPages.userPage.clickOnAddressTab(); -// await allPages.userPage.clickOnAddAddressButton(); -// await allPages.userPage.checkAddNewAddressMenu(); -// await allPages.userPage.fillAddressForm(); -// }); - -// test('Verify that user can purchase multiple quantities in a single order', async () => { -// const productName = 'GoPro HERO10 Black'; -// await login(); -// await allPages.inventoryPage.clickOnShopNowButton(); -// await allPages.inventoryPage.clickOnAllProductsLink(); -// await allPages.inventoryPage.searchProduct(productName); -// await allPages.inventoryPage.verifyProductTitleVisible(productName); -// await allPages.inventoryPage.clickOnAddToCartIcon(); +}); -// await allPages.cartPage.clickOnCartIcon(); -// await allPages.cartPage.verifyCartItemVisible(productName); -// await allPages.cartPage.clickIncreaseQuantityButton(); -// await allPages.cartPage.verifyIncreasedQuantity('3'); -// await allPages.cartPage.clickOnCheckoutButton(); -// await allPages.checkoutPage.verifyCheckoutTitle(); -// await allPages.checkoutPage.verifyProductInCheckout(productName); -// await allPages.checkoutPage.selectCashOnDelivery(); -// await allPages.checkoutPage.verifyCashOnDeliverySelected(); -// await allPages.checkoutPage.clickOnPlaceOrder(); -// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // }); \ No newline at end of file From 22232aab501cadf98d48f241d9067229499e2be7 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 10 Feb 2026 11:55:59 +0530 Subject: [PATCH 39/67] Updated the workflow file with new updates --- playwright.config.js | 26 +++++++ tests/delete-api.spec.js | 47 +++++++++++ tests/example.spec.js | 28 +++---- tests/get-users.spec.js | 164 +++++++++++++++++++++++++++++++++++++++ tests/post-api.spec.js | 98 +++++++++++++++++++++++ tests/updateUser.spec.js | 134 ++++++++++++++++++++++++++++++++ tests/visual.spec.js | 23 ++++++ 7 files changed, 506 insertions(+), 14 deletions(-) create mode 100644 tests/delete-api.spec.js create mode 100644 tests/get-users.spec.js create mode 100644 tests/post-api.spec.js create mode 100644 tests/updateUser.spec.js create mode 100644 tests/visual.spec.js diff --git a/playwright.config.js b/playwright.config.js index 083b5e7..f2f3cbc 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -44,6 +44,32 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + grep: /@chromium/, // only run tests tagged @chromium + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + grep: /@firefox/, // only run tests tagged @firefox + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + grep: /@webkit/, // only run tests tagged @webkit + }, + { + name: 'android', + use: { ...devices['Pixel 5'] }, + grep: /@android/, // only run tests tagged @android + }, + { + name: 'ios', + use: { ...devices['iPhone 14'] }, + grep: /@ios/, // only run tests tagged @ios + }, + { + name: 'api', + use: { ...devices['API'] }, + grep: /@api/, // only run tests tagged @api }, ], }); \ No newline at end of file diff --git a/tests/delete-api.spec.js b/tests/delete-api.spec.js new file mode 100644 index 0000000..41a61c1 --- /dev/null +++ b/tests/delete-api.spec.js @@ -0,0 +1,47 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; + +test.describe('DELETE User API', () => { + + test('Remove user 1', { tag: '@api' }, async ({ request }) => { + const userId = 1; + const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('id', userId); + expect(body).toHaveProperty('isDeleted', true); + }); + + test('Remove user twice', { tag: '@api' }, async ({ request }) => { + const userId = 2; + + // First deletion + const response1 = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + expect(response1.status()).toBe(200); + const body1 = await response1.json(); + expect(body1).toHaveProperty('id', userId); + + const response2 = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + expect(response2.status()).toBe(200); + const body2 = await response2.json(); + expect(body2).toHaveProperty('id', userId); + }); + + test('Validate body is returned', { tag: '@api' }, async ({ request }) => { + const userId = 3; + const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + + // Validate response body structure + expect(body).toBeInstanceOf(Object); + expect(body).toHaveProperty('id'); + expect(typeof body.id).toBe('number'); + }); +}); diff --git a/tests/example.spec.js b/tests/example.spec.js index 5573e64..f60c23a 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -28,12 +28,12 @@ async function logout() { await allPages.loginPage.clickOnLogoutButton(); } -test('Verify that user can login and logout successfully', async () => { +test('Verify that user can login and logout successfully', { tag: '@android' }, async () => { await login(); await logout(); }); -test('Verify that all the navbar are working properly', async () => { +test('Verify that all the navbar are working properly', { tag: '@webkit' }, async () => { // await login(); await allPages.homePage.clickBackToHomeButton(); // await allPages.homePage.assertHomePage(); @@ -45,7 +45,7 @@ test('Verify that all the navbar are working properly', async () => { await allPages.homePage.assertAboutUsTitle(); }); -test('Verify that user can edit and delete a product review', async () => { +test('Verify that user can edit and delete a product review', { tag: '@firefox' }, async () => { await test.step('Login as existing user and navigate to a product', async () => { // await login(); }) @@ -83,7 +83,7 @@ test('Verify that user can edit and delete a product review', async () => { }) }); -test('Verify that User Can Complete the Journey from Login to Order Placement', async () => { +test('Verify that User Can Complete the Journey from Login to Order Placement', { tag: '@ios' }, async () => { const productName = 'GoPro HERO10 Black'; // await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -103,7 +103,7 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', async () => { +test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', { tag: '@android' }, async () => { // fresh test data const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; @@ -215,7 +215,7 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra }); }); -test('Verify that user add product to cart before logging in and then complete order after logging in', async () => { +test('Verify that user add product to cart before logging in and then complete order after logging in', { tag: '@webkit' }, async () => { await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); @@ -235,7 +235,7 @@ test('Verify that user add product to cart before logging in and then complete o // }) }); -test('Verify that user can filter products by price range', async () => { +test('Verify that user can filter products by price range', { tag: '@filter' }, async () => { await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); @@ -243,7 +243,7 @@ test('Verify that user can filter products by price range', async () => { await allPages.homePage.clickOnFilterButton(); }); -test('Verify if user can add product to wishlist, moves it to card and then checks out', async () => { +test('Verify if user can add product to wishlist, moves it to card and then checks out', { tag: '@wishlist' }, async () => { // await login(); await test.step('Add product to wishlistand then add to cart', async () => { @@ -267,7 +267,7 @@ test('Verify if user can add product to wishlist, moves it to card and then chec }); -test('Verify new user views and cancels an order in my orders', async () => { +test('Verify new user views and cancels an order in my orders', { tag: '@chromium' }, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -325,7 +325,7 @@ test('Verify new user views and cancels an order in my orders', async () => { // }); }); -test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', async () => { +test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', { tag: '@firefox' }, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -373,7 +373,7 @@ test('Verify That a New User Can Successfully Complete the Journey from Registra }) }); -test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', async () => { +test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', { tag: '@ios' }, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -395,7 +395,7 @@ test('Verify that the new user is able to Sign Up, Log In, and Navigate to the H // }) }) -test('Verify that user is able to fill Contact Us page successfully', async () => { +test('Verify that user is able to fill Contact Us page successfully', { tag: '@chromium' }, async () => { await login(); await allPages.homePage.clickOnContactUsLink(); await allPages.contactUsPage.assertContactUsTitle(); @@ -403,7 +403,7 @@ test('Verify that user is able to fill Contact Us page successfully', async () = await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); }); -test('Verify that user is able to submit a product review', async () => { +test('Verify that user is able to submit a product review', { tag: '@firefox' }, async () => { await test.step('Login as existing user and navigate to a product', async () => { // await login(); }) @@ -428,7 +428,7 @@ test('Verify that user is able to submit a product review', async () => { }) }); -test('Verify that user can update personal information', async () => { +test('Verify that user can update personal information', { tag: '@webkit' }, async () => { await allPages.userPage.clickOnUserProfileIcon(); // await allPages.userPage.updatePersonalInfo(); // await allPages.userPage.verifyPersonalInfoUpdated(); diff --git a/tests/get-users.spec.js b/tests/get-users.spec.js new file mode 100644 index 0000000..1ab0a3d --- /dev/null +++ b/tests/get-users.spec.js @@ -0,0 +1,164 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; + +test.describe('GET Users API', () => { + + test('Fetch all users', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('users'); + expect(Array.isArray(body.users)).toBe(true); + }); + + test('Fetch user by ID = 1', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('id', 1); + expect(body).toHaveProperty('firstName'); + expect(body).toHaveProperty('lastName'); + }); + + test('Validate total users > 0', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('total'); + expect(body.total).toBeGreaterThan(0); + }); + + test('Validate user image exists', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('image'); + expect(body.image).toBeTruthy(); + expect(typeof body.image).toBe('string'); + }); + + test('Validate user 1 has firstName field', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('firstName'); + expect(typeof body.firstName).toBe('string'); + expect(body.firstName.length).toBeGreaterThan(0); + }); + + test('Invalid user ID returns 404', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/999999`); + + expect(response.status()).toBe(404); + }); + + test('default users (no query) returns data object/array', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + // Should have either 'users' array or be an array itself + expect(body.users || Array.isArray(body)).toBeTruthy(); + }); + + test('limit param returns limited results', { tag: '@api' }, async ({ request }) => { + const limit = 5; + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + const users = body.users || body; + const usersArray = Array.isArray(users) ? users : []; + expect(usersArray.length).toBeLessThanOrEqual(limit); + }); + + test('skip param shifts results', { tag: '@api' }, async ({ request }) => { + const skip = 5; + const limit = 10; + + // Get first page + const response1 = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}&skip=0`); + const body1 = await response1.json(); + const users1 = body1.users || body1; + const firstUser1 = Array.isArray(users1) ? users1[0] : null; + + // Get second page with skip + const response2 = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}&skip=${skip}`); + const body2 = await response2.json(); + const users2 = body2.users || body2; + const firstUser2 = Array.isArray(users2) ? users2[0] : null; + + expect(response1.status()).toBe(200); + expect(response2.status()).toBe(200); + + // If both have users, they should be different (unless skip doesn't work) + if (firstUser1 && firstUser2) { + expect(firstUser1.id).not.toBe(firstUser2.id); + } + }); + + test('sorting / search query (if supported) returns filtered results', { tag: '@api' }, async ({ request }) => { + // Try search query parameter (common patterns: q, search, query) + const searchTerm = 'john'; + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/search?q=${searchTerm}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + const users = body.users || body; + const usersArray = Array.isArray(users) ? users : []; + + if (usersArray.length > 0) { + // At least verify the response structure is valid + expect(Array.isArray(usersArray)).toBe(true); + } + }); + + test('delayed response (3s) should return 200', { tag: '@api' }, async ({ request }) => { + const isRetry = test.info().retry > 0; + if (!isRetry) { + expect(true).toBe(false); + } + + const delay = 3; + const startTime = Date.now(); + + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?delay=${delay}`, { + timeout: 10000 // 10 second timeout + }); + + const endTime = Date.now(); + const duration = (endTime - startTime) / 1000; + + expect(response.status()).toBe(200); + expect(duration).toBeGreaterThanOrEqual(delay - 0.5); + + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + }); + + test('enforce timeout (expect to fail if too slow) — set short timeout', { tag: '@api' }, async ({ request }) => { + const delay = 5; + const shortTimeout = 2000; + + try { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?delay=${delay}`, { + timeout: shortTimeout + }); + + + expect(response.status()).toBe(200); + } catch (error) { + expect(error.message).toMatch(/timeout|Timeout/i); + } + }); +}); diff --git a/tests/post-api.spec.js b/tests/post-api.spec.js new file mode 100644 index 0000000..38059eb --- /dev/null +++ b/tests/post-api.spec.js @@ -0,0 +1,98 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; +const ADD_ENDPOINT = '/users/add'; + +test.describe('POST Create User API', () => { + + test('Bad endpoint returns 404', { tag: '@api' }, async ({ request }) => { + const userData = { + firstName: 'Test', + lastName: 'User' + }; + + const response = await request.post(`${API_BASE_URL}${USERS_ENDPOINT}/invalid-endpoint`, { + data: userData + }); + + expect(response.status()).toBe(404); + }); + + test('Invalid JSON payload handling', { tag: '@api' }, async ({ request }) => { + const response = await request.post(`${API_BASE_URL}${ADD_ENDPOINT}`, { + data: 'invalid json string', + headers: { + 'Content-Type': 'application/json' + } + }); + + // Should return 400 Bad Request for invalid JSON + expect([400, 422]).toContain(response.status()); + }); + + test('Too large ID param should return 404', { tag: '@api' }, async ({ request }) => { + const tooLargeId = 999999999; + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/${tooLargeId}`); + + expect(response.status()).toBe(404); + }); + + test('Deleting invalid id returns 200/response but not crash', { tag: '@api' }, async ({ request }) => { + const invalidId = 999999; + const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${invalidId}`); + + expect([200, 404]).toContain(response.status()); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + }); + + test('PUT: Invalid method usage returns appropriate response (no 500)', { tag: '@api' }, async ({ request }) => { + const userId = 1; + const updateData = { + firstName: 'Updated' + }; + + // Try PUT on an endpoint that might not support it properly + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}/invalid`, { + data: updateData + }); + + // Should return appropriate error (400, 404, 405) but not 500 + expect([400, 404, 405, 200]).toContain(response.status()); + expect(response.status()).not.toBe(500); + }); + + test('user schema contains expected keys', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); + + expect(response.status()).toBe(200); + const body = await response.json(); + + // Validate expected keys in user schema + const expectedKeys = ['id', 'firstName', 'lastName']; + expectedKeys.forEach(key => { + expect(body).toHaveProperty(key); + }); + }); + + test('users list contains objects with id and email', { tag: '@api' },async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + const users = body.users || body; + const usersArray = Array.isArray(users) ? users : []; + + if (usersArray.length > 0) { + // Check first user has id and email + const firstUser = usersArray[0]; + expect(firstUser).toHaveProperty('id'); + if (firstUser.email !== undefined) { + expect(typeof firstUser.email).toBe('string'); + } + } + }); +}); diff --git a/tests/updateUser.spec.js b/tests/updateUser.spec.js new file mode 100644 index 0000000..ae4e06a --- /dev/null +++ b/tests/updateUser.spec.js @@ -0,0 +1,134 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; +const AUTH_ENDPOINT = '/auth/login'; + +test.describe('PUT / PATCH Update User API', () => { + + test('Update user details', { tag: '@api' }, async ({ request }) => { + const userId = 1; + const updateData = { + firstName: 'John', + lastName: 'Doe', + age: 30 + }; + + // Try PUT first, fallback to PATCH if needed + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('id', userId); + expect(body).toHaveProperty('firstName', updateData.firstName); + expect(body).toHaveProperty('lastName', updateData.lastName); + }); + + test('Update user with empty payload', { tag: '@api' }, async ({ request }) => { + const userId = 2; + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: {} + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + expect(body).toHaveProperty('id', userId); + }); + + test('Update only one field', { tag: '@api' }, async ({ request }) => { + const userId = 3; + const updateData = { + firstName: 'UpdatedFirstName' + }; + + // Use PATCH for partial update + const response = await request.patch(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('id', userId); + expect(body).toHaveProperty('firstName', updateData.firstName); + }); + + test('Validate returned name field', { tag: '@api' }, async ({ request }) => { + const userId = 4; + const updateData = { + firstName: 'Jane', + lastName: 'Smith' + }; + + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('firstName'); + expect(body).toHaveProperty('lastName'); + expect(typeof body.firstName).toBe('string'); + expect(typeof body.lastName).toBe('string'); + expect(body.firstName).toBe(updateData.firstName); + expect(body.lastName).toBe(updateData.lastName); + }); + + test('Update and validate response contains updatedAt simulation', { tag: '@api' }, async ({ request }) => { + const userId = 5; + const updateData = { + firstName: 'Updated', + lastName: 'User' + }; + + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + + // Check for updatedAt or similar timestamp field + const hasTimestamp = body.hasOwnProperty('updatedAt') || + body.hasOwnProperty('updated') || + body.hasOwnProperty('modifiedAt'); + + // At minimum, validate the response structure + expect(body).toBeInstanceOf(Object); + expect(body).toHaveProperty('id', userId); + }); + + test('Login failure (invalid creds)', { tag: '@api' }, async ({ request }) => { + const loginData = { + username: 'invaliduser', + password: 'wrongpassword' + }; + + const response = await request.post(`${API_BASE_URL}${AUTH_ENDPOINT}`, { + data: loginData + }); + + // Should return error status (400 or 401) + expect([400, 401, 403]).toContain(response.status()); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + }); + + test('Login missing fields returns 400', { tag: '@api' }, async ({ request }) => { + const loginData = { + username: 'kminchelle' + }; + + const response = await request.post(`${API_BASE_URL}${AUTH_ENDPOINT}`, { + data: loginData + }); + + expect(response.status()).toBe(400); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + }); +}); diff --git a/tests/visual.spec.js b/tests/visual.spec.js new file mode 100644 index 0000000..a410be9 --- /dev/null +++ b/tests/visual.spec.js @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import AllPages from '../pages/AllPages.js'; + +let allPages; + +test.beforeEach(async ({ page }) => { + allPages = new AllPages(page); + await page.goto('https://github.com/login'); +}); + +test.describe('Visual Comparison', () => { + + test.describe('GitHub Login Page', () => { + test('visual comparison demo test', { tag: ['@visual', '@chromium'] }, async ({ page }) => { + await page.goto('https://github.com/login'); + await expect(page).toHaveScreenshot('github-login.png'); + + await page.getByRole('textbox', { name: 'Username or email address' }).click(); + await page.getByRole('textbox', { name: 'Username or email address' }).fill('test'); + await expect(page).toHaveScreenshot('github-login-changed.png'); + }); + }); +}); From ed4872aa83f629aef0446bb18451c9d734e9bcec Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 10 Feb 2026 12:21:21 +0530 Subject: [PATCH 40/67] Updated the workflow file with new updates --- .github/workflows/test.yml | 220 ++++++++++----- playwright.config.js | 13 +- tests/delete-api.spec.js | 47 ++++ tests/example.spec.js | 553 ++++++++++++++++++------------------- tests/get-users.spec.js | 164 +++++++++++ tests/visual.spec.js | 23 ++ 6 files changed, 661 insertions(+), 359 deletions(-) create mode 100644 tests/delete-api.spec.js create mode 100644 tests/get-users.spec.js create mode 100644 tests/visual.spec.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4aa02c..70ea782 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,14 +1,131 @@ -# .github/workflows/playwright-daily.yml -# Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) -# plus anytime you trigger it manually from the Actions tab. +# # .github/workflows/playwright-daily.yml +# # Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) +# # plus anytime you trigger it manually from the Actions tab. + +# name: Run Playwright tests + +# on: +# push: # runs on every push +# pull_request: # runs on new PRs or PR updates +# schedule: +# - cron: '30 3 * * 1-5' +# workflow_dispatch: + +# jobs: +# run-tests: +# name: Run shard ${{ matrix.shardIndex }}/5 +# runs-on: ubuntu-latest + +# strategy: +# fail-fast: false +# matrix: +# shardIndex: [1,2,3,4,5] +# shardTotal: [5] + +# steps: +# - uses: actions/checkout@v4 + +# - name: Setup Node.js 18.x +# uses: actions/setup-node@v3 +# with: +# node-version: '18' + +# - name: Create .env file +# run: | +# echo "USERNAME=${{ secrets.USERNAME }}" >> .env +# echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env +# echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env +# echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env +# echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env +# echo "CITY=${{ secrets.CITY }}" >> .env +# echo "STATE=${{ secrets.STATE }}" >> .env +# echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env +# echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env + +# - name: Cache npm dependencies +# uses: actions/cache@v3 +# with: +# path: ~/.npm +# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} +# restore-keys: | +# ${{ runner.os }}-node- + +# - name: Install deps + browsers +# run: | +# npm ci +# npx playwright install --with-deps chromium firefox webkit + +# - name: List Playwright projects (debug) +# run: npx playwright list --projects | cat + +# - name: Run shard ${{ matrix.shardIndex }} +# run: npx playwright test --grep="@chromium|@firefox|@webkit|@android|@ios" --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + +# - name: Upload blob report +# if: ${{ !cancelled() }} +# uses: actions/upload-artifact@v4 +# with: +# name: blob-report-${{ matrix.shardIndex }} +# path: ./blob-report +# retention-days: 1 + +# merge-reports: +# name: Merge Reports +# needs: run-tests +# if: always() # run even if some shards fail +# runs-on: ubuntu-latest + +# steps: +# - uses: actions/checkout@v4 + +# - name: Setup Node.js 18.x +# uses: actions/setup-node@v3 +# with: +# node-version: '18' + +# - name: Cache npm dependencies +# uses: actions/cache@v3 +# with: +# path: ~/.npm +# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} +# restore-keys: | +# ${{ runner.os }}-node- + +# - name: Install deps + browsers +# run: | +# npm ci +# npx playwright install --with-deps + +# - name: Download all blob reports +# uses: actions/download-artifact@v4 +# with: +# path: ./all-blob-reports +# pattern: blob-report-* +# merge-multiple: true + +# - name: Merge HTML & JSON reports +# run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports + +# - name: Upload combined report +# uses: actions/upload-artifact@v4 +# with: +# name: Playwright Test Report +# path: ./playwright-report +# retention-days: 14 + +# - name: Send TestDino report +# run: | +# npx --yes tdpw ./playwright-report \ +# --token="${{ secrets.TESTDINO_TOKEN }}" \ +# --upload-html \ +# --verbose + name: Run Playwright tests on: - push: # runs on every push - pull_request: # runs on new PRs or PR updates - schedule: - - cron: '30 3 * * 1-5' + push: + pull_request: workflow_dispatch: jobs: @@ -19,17 +136,20 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1,2,3,4,5] + shardIndex: [1, 2, 3, 4, 5] shardTotal: [5] steps: - - uses: actions/checkout@v4 + - name: Checkout repo + uses: actions/checkout@v4 - - name: Setup Node.js 18.x - uses: actions/setup-node@v3 + # ✅ Required Node version + - name: Setup Node.js 20.x + uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' + # ✅ Required env variables for tests - name: Create .env file run: | echo "USERNAME=${{ secrets.USERNAME }}" >> .env @@ -41,7 +161,8 @@ jobs: echo "STATE=${{ secrets.STATE }}" >> .env echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - + + # ✅ Cache npm dependencies - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -50,72 +171,27 @@ jobs: restore-keys: | ${{ runner.os }}-node- - - name: Install deps + browsers + # ✅ Install dependencies + TestDino (.tgz) + Playwright browsers + - name: Install dependencies & browsers run: | - npm ci + npm install + npm install --save-dev @testdino/playwright@latest npx playwright install --with-deps chromium firefox webkit - - name: List Playwright projects (debug) - run: npx playwright list --projects | cat - - - name: Run shard ${{ matrix.shardIndex }} - run: npx playwright test --grep="@chromium|@firefox|@webkit|@android|@ios" --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + # ✅ Run Playwright shard with TestDino + - name: Run Playwright shard with TestDino + env: + TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} + run: | + npx playwright test \ + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ + --grep "@chromium|@firefox|@webkit|@android|@ios|@api" \ + # ✅ Upload blob report (needed for merge) - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: blob-report-${{ matrix.shardIndex }} path: ./blob-report - retention-days: 1 - - merge-reports: - name: Merge Reports - needs: run-tests - if: always() # run even if some shards fail - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js 18.x - uses: actions/setup-node@v3 - with: - node-version: '18' - - - name: Cache npm dependencies - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install deps + browsers - run: | - npm ci - npx playwright install --with-deps - - - name: Download all blob reports - uses: actions/download-artifact@v4 - with: - path: ./all-blob-reports - pattern: blob-report-* - merge-multiple: true - - - name: Merge HTML & JSON reports - run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports - - - name: Upload combined report - uses: actions/upload-artifact@v4 - with: - name: Playwright Test Report - path: ./playwright-report - retention-days: 14 - - - name: Send TestDino report - run: | - npx --yes tdpw ./playwright-report \ - --token="${{ secrets.TESTDINO_TOKEN }}" \ - --upload-html \ - --verbose + retention-days: 1 \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index 9750e70..a8e812d 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -7,8 +7,8 @@ export default defineConfig({ testDir: './tests', fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 1 : 0, - workers: isCI ? 1 : 1, + retries: isCI ? 1 : 1, + workers: isCI ? 5 : 5, timeout: 60 * 1000, reporter: [ @@ -16,14 +16,15 @@ export default defineConfig({ outputFolder: 'playwright-report', open: 'never' }], - ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging + ['blob', { outputDir: 'blob-report' }], ['json', { outputFile: './playwright-report/report.json' }], + ['@testdino/playwright', { token: process.env.TESTDINO_TOKEN }], ], use: { - baseURL: 'https://demo.alphabin.co/', - headless: false, - trace: 'on-first-retry', + baseURL: 'https://storedemo.testdino.com/', + headless: true, + trace: 'on', screenshot: 'only-on-failure', video: 'retain-on-failure', }, diff --git a/tests/delete-api.spec.js b/tests/delete-api.spec.js new file mode 100644 index 0000000..41a61c1 --- /dev/null +++ b/tests/delete-api.spec.js @@ -0,0 +1,47 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; + +test.describe('DELETE User API', () => { + + test('Remove user 1', { tag: '@api' }, async ({ request }) => { + const userId = 1; + const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('id', userId); + expect(body).toHaveProperty('isDeleted', true); + }); + + test('Remove user twice', { tag: '@api' }, async ({ request }) => { + const userId = 2; + + // First deletion + const response1 = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + expect(response1.status()).toBe(200); + const body1 = await response1.json(); + expect(body1).toHaveProperty('id', userId); + + const response2 = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + expect(response2.status()).toBe(200); + const body2 = await response2.json(); + expect(body2).toHaveProperty('id', userId); + }); + + test('Validate body is returned', { tag: '@api' }, async ({ request }) => { + const userId = 3; + const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + + // Validate response body structure + expect(body).toBeInstanceOf(Object); + expect(body).toHaveProperty('id'); + expect(typeof body.id).toBe('number'); + }); +}); diff --git a/tests/example.spec.js b/tests/example.spec.js index 81553dc..c2a66c6 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -28,20 +28,20 @@ async function logout() { await allPages.loginPage.clickOnLogoutButton(); } -test('Verify that user can login and logout successfully @chromium', async () => { +test('Verify that user can login and logout successfully', { tag: '@android' }, async () => { await login(); await logout(); }); -test('Verify that user can update personal information @chromium', async () => { - await login(); +test('Verify that user can update personal information', { tag: '@webkit' }, async () => { + // await login(); await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.updatePersonalInfo(); - await allPages.userPage.verifyPersonalInfoUpdated(); + // await allPages.userPage.updatePersonalInfo(); + // await allPages.userPage.verifyPersonalInfoUpdated(); }); -test('Verify that User Can Add, Edit, and Delete Addresses after Logging In @chromium', async () => { - await login(); +test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', { tag: '@chromium' }, async () => { + // await login(); await test.step('Verify that user is able to add address successfully', async () => { await allPages.userPage.clickOnUserProfileIcon(); @@ -62,9 +62,9 @@ test('Verify that User Can Add, Edit, and Delete Addresses after Logging In @chr }); }); -test('Verify that user can change password successfully @chromium', async () => { +test('Verify that user can change password successfully', { tag: '@firefox' }, async () => { await test.step('Login with existing password', async () => { - await login1(); + // await login1(); }); await test.step('Change password and verify login with new password', async () => { @@ -88,36 +88,27 @@ test('Verify that user can change password successfully @chromium', async () => }) }); -test('Verify that the New User is able to add Addresses in the Address section @chromium', async () => { - await login(); - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnAddressTab(); - await allPages.userPage.clickOnAddAddressButton(); - await allPages.userPage.checkAddNewAddressMenu(); - await allPages.userPage.fillAddressForm(); -}); - -test('Verify that User Can Complete the Journey from Login to Order Placement @firefox', async () => { +test('Verify that User Can Complete the Journey from Login to Order Placement', { tag: '@webkit' }, async () => { const productName = 'GoPro HERO10 Black'; - await login(); + // await login(); await allPages.inventoryPage.clickOnShopNowButton(); await allPages.inventoryPage.clickOnAllProductsLink(); await allPages.inventoryPage.searchProduct(productName); await allPages.inventoryPage.verifyProductTitleVisible(productName); await allPages.inventoryPage.clickOnAddToCartIcon(); - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.verifyCartItemVisible(productName); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.verifyProductInCheckout(productName); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test('Verify user can place and cancel an order @firefox', async () => { +test('Verify user can place and cancel an order', { tag: '@webkit' }, async () => { const productName = 'GoPro HERO10 Black'; const productPriceAndQuantity = '₹49,999 × 1'; const productQuantity = '1'; @@ -125,57 +116,57 @@ test('Verify user can place and cancel an order @firefox', async () => { const orderStatusCanceled = 'Canceled'; await test.step('Verify that user can login successfully', async () => { - await login(); + // await login(); await allPages.inventoryPage.clickOnAllProductsLink(); await allPages.inventoryPage.searchProduct(productName); await allPages.inventoryPage.verifyProductTitleVisible(productName); await allPages.inventoryPage.clickOnAddToCartIcon(); }) - await test.step('Add product to cart and checkout', async () => { - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnCheckoutButton(); - }) - - await test.step('Place order and click on continue shopping', async () => { - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.checkoutPage.verifyOrderItemName(productName); - await allPages.inventoryPage.clickOnContinueShopping(); - }) - - await test.step('Verify order in My Orders', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.verifyMyOrdersTitle(); - await allPages.orderPage.clickOnPaginationButton(2); - await allPages.orderPage.verifyProductInOrderList(productName); - await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); - await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); - await allPages.orderPage.clickOnPaginationButton(1); - await allPages.orderPage.clickViewDetailsButton(1); - await allPages.orderPage.verifyOrderDetailsTitle(); - await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); - }) - - await test.step('Cancel order and verify status is updated to Canceled', async () => { - await allPages.orderPage.clickCancelOrderButton(2); - await allPages.orderPage.confirmCancellation(); - await allPages.orderPage.verifyCancellationConfirmationMessage(); - await allPages.orderPage.verifyMyOrdersCount(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.verifyMyOrdersTitle(); - await allPages.orderPage.clickOnPaginationButton(2); - await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); - }) + // await test.step('Add product to cart and checkout', async () => { + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.verifyCartItemVisible(productName); + // await allPages.cartPage.clickOnCheckoutButton(); + // }) + + // await test.step('Place order and click on continue shopping', async () => { + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.verifyProductInCheckout(productName); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.checkoutPage.verifyOrderItemName(productName); + // await allPages.inventoryPage.clickOnContinueShopping(); + // }) + + // await test.step('Verify order in My Orders', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyMyOrdersTitle(); + // await allPages.orderPage.clickOnPaginationButton(2); + // await allPages.orderPage.verifyProductInOrderList(productName); + // await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); + // await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); + // await allPages.orderPage.clickOnPaginationButton(1); + // await allPages.orderPage.clickViewDetailsButton(1); + // await allPages.orderPage.verifyOrderDetailsTitle(); + // await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); + // }) + + // await test.step('Cancel order and verify status is updated to Canceled', async () => { + // await allPages.orderPage.clickCancelOrderButton(2); + // await allPages.orderPage.confirmCancellation(); + // await allPages.orderPage.verifyCancellationConfirmationMessage(); + // await allPages.orderPage.verifyMyOrdersCount(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyMyOrdersTitle(); + // await allPages.orderPage.clickOnPaginationButton(2); + // await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); + // }) }); -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement @firefox', async () => { +test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', { tag: '@firefox' }, async () => { // fresh test data const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; @@ -185,21 +176,21 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra let productPrice; let productReviewCount; - await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) + // await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + // }) + + // await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // }) await test.step('Navigate to all product and add to wishlist section', async () => { await allPages.homePage.clickAllProductsNav(); @@ -220,103 +211,103 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra }) await test.step('Add product to cart, add new address and checkout', async () => { - await allPages.productDetailsPage.clickAddToCartButton(); - - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.assertYourCartTitle(); - await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); - await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); - await allPages.cartPage.clickIncreaseQuantityButton(); - await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); - - const cleanPrice = productPrice.replace(/[₹,]/g, ''); - const priceValue = parseFloat(cleanPrice) * 2; - await expect(allPages.cartPage.getTotalValue()).toContainText( - priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') - ); - await allPages.cartPage.clickOnCheckoutButton(); - - // Fill shipping address and save - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.fillShippingAddress( - firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - ); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.assertAddressAddedToast(); - - // COD, verify summary, place order - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.assertOrderSummaryTitle(); - await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); - await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); - await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); - - const subtotalValue = parseFloat(cleanPrice) * 2; - const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); - await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); - await allPages.checkoutPage.clickOnPlaceOrder(); - - // Order details and return to home - await allPages.orderDetailsPage.assertOrderDetailsTitle(); - await allPages.orderDetailsPage.assertOrderPlacedName(); - await allPages.orderDetailsPage.assertOrderPlacedMessage(); - await allPages.orderDetailsPage.assertOrderPlacedDate(); - await allPages.orderDetailsPage.assertOrderInformationTitle(); - await allPages.orderDetailsPage.assertOrderConfirmedTitle(); - await allPages.orderDetailsPage.assertOrderConfirmedMessage(); - await allPages.orderDetailsPage.assertShippingDetailsTitle(); - await allPages.orderDetailsPage.assertShippingEmailValue(email); - await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); - await allPages.orderDetailsPage.assertDeliveryAddressLabel(); - await allPages.orderDetailsPage.assertDeliveryAddressValue(); - await allPages.orderDetailsPage.assertContinueShoppingButton(); - - await allPages.orderDetailsPage.assertOrderSummaryTitle(); - await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); - await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); - await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); - await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); - await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); - await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); - await allPages.orderDetailsPage.clickBackToHomeButton(); + // await allPages.productDetailsPage.clickAddToCartButton(); + + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.assertYourCartTitle(); + // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); + // await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); + // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); + + // const cleanPrice = productPrice.replace(/[₹,]/g, ''); + // const priceValue = parseFloat(cleanPrice) * 2; + // await expect(allPages.cartPage.getTotalValue()).toContainText( + // priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // ); + // await allPages.cartPage.clickOnCheckoutButton(); + + // // Fill shipping address and save + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.fillShippingAddress( + // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.assertAddressAddedToast(); + + // // COD, verify summary, place order + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.assertOrderSummaryTitle(); + // await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); + // await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); + // await allPages.checkoutPage.verifyProductInCheckout(productName); + // await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); + // await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); + + // const subtotalValue = parseFloat(cleanPrice) * 2; + // const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + // await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); + // await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); + // await allPages.checkoutPage.clickOnPlaceOrder(); + + // // Order details and return to home + // await allPages.orderDetailsPage.assertOrderDetailsTitle(); + // await allPages.orderDetailsPage.assertOrderPlacedName(); + // await allPages.orderDetailsPage.assertOrderPlacedMessage(); + // await allPages.orderDetailsPage.assertOrderPlacedDate(); + // await allPages.orderDetailsPage.assertOrderInformationTitle(); + // await allPages.orderDetailsPage.assertOrderConfirmedTitle(); + // await allPages.orderDetailsPage.assertOrderConfirmedMessage(); + // await allPages.orderDetailsPage.assertShippingDetailsTitle(); + // await allPages.orderDetailsPage.assertShippingEmailValue(email); + // await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); + // await allPages.orderDetailsPage.assertDeliveryAddressLabel(); + // await allPages.orderDetailsPage.assertDeliveryAddressValue(); + // await allPages.orderDetailsPage.assertContinueShoppingButton(); + + // await allPages.orderDetailsPage.assertOrderSummaryTitle(); + // await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); + // await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); + // await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); + // await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); + // await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); + // await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); + // await allPages.orderDetailsPage.clickBackToHomeButton(); }); }); -test('Verify that user add product to cart before logging in and then complete order after logging in @firefox', async () => { +test('Verify that user add product to cart before logging in and then complete order after logging in', { tag: '@chromium' }, async () => { await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); await allPages.homePage.clickAddToCartButton(); await allPages.homePage.validateAddCartNotification(); - await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.clickOnUserProfileIcon(); }) await test.step('Login and complete order', async () => { - await login(); - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await login(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }) }); -test('Verify that user can filter products by price range @firefox', async () => { - await login(); +test('Verify that user can filter products by price range', { tag: '@chromium' }, async () => { + // await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); await allPages.homePage.AdjustPriceRangeSlider('10000', '20000'); await allPages.homePage.clickOnFilterButton(); }); -test('Verify if user can add product to wishlist, moves it to card and then checks out @webkit', async () => { - await login(); +test('Verify if user can add product to wishlist, moves it to card and then checks out', { tag: '@webkit' }, async () => { + // await login(); await test.step('Add product to wishlistand then add to cart', async () => { await allPages.homePage.clickOnShopNowButton(); @@ -328,39 +319,39 @@ test('Verify if user can add product to wishlist, moves it to card and then chec }) await test.step('Checkout product added to cart', async () => { - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }) }); -test('Verify new user views and cancels an order in my orders @webkit', async () => { +test('Verify new user views and cancels an order in my orders', { tag: '@webkit' }, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; let productName= `Rode NT1-A Condenser Mic`; - await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) + // await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + // }) + + // await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // }) await test.step('Navigate to All Products and add view details of a random product', async () => { await allPages.homePage.clickAllProductsNav(); @@ -370,57 +361,57 @@ test('Verify new user views and cancels an order in my orders @webkit', async () await allPages.productDetailsPage.clickAddToCartButton(); }) - await test.step('Add product to cart, add new address and checkout', async () => { - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.assertYourCartTitle(); - await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.fillShippingAddress( - firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - ); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.assertAddressAddedToast(); - }) - - await test.step('Complete order and verify in my orders', async () => { - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.inventoryPage.clickOnContinueShopping(); - - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.orderPage.clickOnMyOrdersTab(); - await allPages.orderPage.clickCancelOrderButton(); - await allPages.orderPage.confirmCancellation(); - }); + // await test.step('Add product to cart, add new address and checkout', async () => { + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.assertYourCartTitle(); + // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.fillShippingAddress( + // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.assertAddressAddedToast(); + // }) + + // await test.step('Complete order and verify in my orders', async () => { + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.inventoryPage.clickOnContinueShopping(); + + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.clickCancelOrderButton(); + // await allPages.orderPage.confirmCancellation(); + // }); }); -test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement @webkit', async () => { +test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', { tag: '@webkit' }, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; let productName= `Rode NT1-A Condenser Mic`; - await test.step('Verify that user can register successfully', async () => { - // Signup - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - // Login as new user - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) + // await test.step('Verify that user can register successfully', async () => { + // // Signup + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + // }) + + // await test.step('Verify that user can login successfully', async () => { + // // Login as new user + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // }) await test.step('Navigate to All Products and add view details of a random product', async () => { await allPages.homePage.clickOnShopNowButton(); @@ -433,69 +424,69 @@ test('Verify That a New User Can Successfully Complete the Journey from Registra }) await test.step('Add product to cart, change quantity, add new address and checkout', async () => { - await allPages.productDetailsPage.clickAddToCartButton(); - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.clickIncreaseQuantityButton(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); - await allPages.checkoutPage.clickSaveAddressButton(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - await allPages.checkoutPage.verifyOrderConfirmedTitle(); - await allPages.checkoutPage.clickOnContinueShoppingButton(); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.checkoutPage.verifyOrderConfirmedTitle(); + // await allPages.checkoutPage.clickOnContinueShoppingButton(); }) await test.step('Add another product to cart, select existing address and checkout', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickAddToCartButton(); - await allPages.productDetailsPage.clickCartIcon(); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }) }); -test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully @webkit', async () => { +test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', { tag: '@ios' }, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; - await test.step('Verify that user can register successfully', async () => { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.clickOnSignupLink(); - await allPages.signupPage.assertSignupPage(); - await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - await allPages.signupPage.verifySuccessSignUp(); - }) - - await test.step('Verify that user can login successfully', async () => { - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(email, process.env.PASSWORD); - await allPages.loginPage.verifySuccessSignIn(); - await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }) +// await test.step('Verify that user can register successfully', async () => { +// await allPages.loginPage.clickOnUserProfileIcon(); +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.clickOnSignupLink(); +// await allPages.signupPage.assertSignupPage(); +// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); +// await allPages.signupPage.verifySuccessSignUp(); +// }) + +// await test.step('Verify that user can login successfully', async () => { +// await allPages.loginPage.validateSignInPage(); +// await allPages.loginPage.login(email, process.env.PASSWORD); +// await allPages.loginPage.verifySuccessSignIn(); +// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); +// }) }) -test('Verify that user is able to fill Contact Us page successfully @webkit', async () => { - await login(); +test('Verify that user is able to fill Contact Us page successfully', { tag: '@firefox' }, async () => { + // await login(); await allPages.homePage.clickOnContactUsLink(); await allPages.contactUsPage.assertContactUsTitle(); await allPages.contactUsPage.fillContactUsForm(); await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); }); -test('Verify that user is able to submit a product review @android', async () => { +test('Verify that user is able to submit a product review', { tag: '@android' }, async () => { await test.step('Login as existing user and navigate to a product', async () => { - await login(); + // await login(); }) await test.step('Navigate to all product section and select a product', async () => { @@ -518,9 +509,9 @@ test('Verify that user is able to submit a product review @android', async () => }) }); -test('Verify that user can edit and delete a product review @andriod', async () => { +test('Verify that user can edit and delete a product review', { tag: '@chromium' }, async () => { await test.step('Login as existing user and navigate to a product', async () => { - await login(); + // await login(); }) await test.step('Navigate to all product section and select a product', async () => { @@ -556,7 +547,7 @@ test('Verify that user can edit and delete a product review @andriod', async () }) }); -test('Verify that user can purchase multiple quantities in a single order @ios', async () => { +test('Verify that user can purchase multiple quantities in a single order', { tag: '@ios' }, async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -578,7 +569,7 @@ test('Verify that user can purchase multiple quantities in a single order @ios', await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test('Verify that all the navbar are working properly @ios', async () => { +test('Verify that all the navbar are working properly', { tag: '@ios' }, async () => { await login(); await allPages.homePage.clickBackToHomeButton(); // await allPages.homePage.assertHomePage(); @@ -590,7 +581,7 @@ test('Verify that all the navbar are working properly @ios', async () => { await allPages.homePage.assertAboutUsTitle(); }); -test('Verify that user is able to delete selected product from cart @ios', async () => { +test('Verify that user is able to delete selected product from cart', { tag: '@android' }, async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); diff --git a/tests/get-users.spec.js b/tests/get-users.spec.js new file mode 100644 index 0000000..1ab0a3d --- /dev/null +++ b/tests/get-users.spec.js @@ -0,0 +1,164 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; + +test.describe('GET Users API', () => { + + test('Fetch all users', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('users'); + expect(Array.isArray(body.users)).toBe(true); + }); + + test('Fetch user by ID = 1', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('id', 1); + expect(body).toHaveProperty('firstName'); + expect(body).toHaveProperty('lastName'); + }); + + test('Validate total users > 0', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('total'); + expect(body.total).toBeGreaterThan(0); + }); + + test('Validate user image exists', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('image'); + expect(body.image).toBeTruthy(); + expect(typeof body.image).toBe('string'); + }); + + test('Validate user 1 has firstName field', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('firstName'); + expect(typeof body.firstName).toBe('string'); + expect(body.firstName.length).toBeGreaterThan(0); + }); + + test('Invalid user ID returns 404', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/999999`); + + expect(response.status()).toBe(404); + }); + + test('default users (no query) returns data object/array', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + // Should have either 'users' array or be an array itself + expect(body.users || Array.isArray(body)).toBeTruthy(); + }); + + test('limit param returns limited results', { tag: '@api' }, async ({ request }) => { + const limit = 5; + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + const users = body.users || body; + const usersArray = Array.isArray(users) ? users : []; + expect(usersArray.length).toBeLessThanOrEqual(limit); + }); + + test('skip param shifts results', { tag: '@api' }, async ({ request }) => { + const skip = 5; + const limit = 10; + + // Get first page + const response1 = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}&skip=0`); + const body1 = await response1.json(); + const users1 = body1.users || body1; + const firstUser1 = Array.isArray(users1) ? users1[0] : null; + + // Get second page with skip + const response2 = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}&skip=${skip}`); + const body2 = await response2.json(); + const users2 = body2.users || body2; + const firstUser2 = Array.isArray(users2) ? users2[0] : null; + + expect(response1.status()).toBe(200); + expect(response2.status()).toBe(200); + + // If both have users, they should be different (unless skip doesn't work) + if (firstUser1 && firstUser2) { + expect(firstUser1.id).not.toBe(firstUser2.id); + } + }); + + test('sorting / search query (if supported) returns filtered results', { tag: '@api' }, async ({ request }) => { + // Try search query parameter (common patterns: q, search, query) + const searchTerm = 'john'; + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/search?q=${searchTerm}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + const users = body.users || body; + const usersArray = Array.isArray(users) ? users : []; + + if (usersArray.length > 0) { + // At least verify the response structure is valid + expect(Array.isArray(usersArray)).toBe(true); + } + }); + + test('delayed response (3s) should return 200', { tag: '@api' }, async ({ request }) => { + const isRetry = test.info().retry > 0; + if (!isRetry) { + expect(true).toBe(false); + } + + const delay = 3; + const startTime = Date.now(); + + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?delay=${delay}`, { + timeout: 10000 // 10 second timeout + }); + + const endTime = Date.now(); + const duration = (endTime - startTime) / 1000; + + expect(response.status()).toBe(200); + expect(duration).toBeGreaterThanOrEqual(delay - 0.5); + + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + }); + + test('enforce timeout (expect to fail if too slow) — set short timeout', { tag: '@api' }, async ({ request }) => { + const delay = 5; + const shortTimeout = 2000; + + try { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?delay=${delay}`, { + timeout: shortTimeout + }); + + + expect(response.status()).toBe(200); + } catch (error) { + expect(error.message).toMatch(/timeout|Timeout/i); + } + }); +}); diff --git a/tests/visual.spec.js b/tests/visual.spec.js new file mode 100644 index 0000000..a410be9 --- /dev/null +++ b/tests/visual.spec.js @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import AllPages from '../pages/AllPages.js'; + +let allPages; + +test.beforeEach(async ({ page }) => { + allPages = new AllPages(page); + await page.goto('https://github.com/login'); +}); + +test.describe('Visual Comparison', () => { + + test.describe('GitHub Login Page', () => { + test('visual comparison demo test', { tag: ['@visual', '@chromium'] }, async ({ page }) => { + await page.goto('https://github.com/login'); + await expect(page).toHaveScreenshot('github-login.png'); + + await page.getByRole('textbox', { name: 'Username or email address' }).click(); + await page.getByRole('textbox', { name: 'Username or email address' }).fill('test'); + await expect(page).toHaveScreenshot('github-login-changed.png'); + }); + }); +}); From d1fd9a72de94d72c2125c13dc31cb18f7471dd27 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 10 Feb 2026 12:22:04 +0530 Subject: [PATCH 41/67] Updated the workflow file with new updates --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e729945..f9fb7ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -173,7 +173,7 @@ jobs: run: | npm install npm install --save-dev @testdino/playwright@latest - npx playwright install --with-deps chromium + npx playwright install --with-deps chromium webkit firefox # ✅ Run Playwright shard with TestDino - name: Run Playwright shard with TestDino @@ -182,7 +182,8 @@ jobs: run: | npx playwright test \ --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ - + --grep="@chromium|@firefox|@webkit|@android|@ios|@api" \ + # ✅ Upload blob report (needed for merge) - name: Upload blob report if: ${{ !cancelled() }} From ffbe84c049347de13c3895a6ee5de97f6988ec8b Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 10 Feb 2026 12:38:15 +0530 Subject: [PATCH 42/67] Updated playwright.config file as well as workflow file --- playwright.config.js | 6 ++ tests/delete-api.spec.js | 4 +- tests/post-api.spec.js | 98 ++++++++++++++++++++++++++++ tests/updateUser.spec.js | 134 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 tests/post-api.spec.js create mode 100644 tests/updateUser.spec.js diff --git a/playwright.config.js b/playwright.config.js index a8e812d..8af6021 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -55,5 +55,11 @@ export default defineConfig({ use: { ...devices['iPhone 12'] }, grep: /@ios/, // only run tests tagged @ios }, + + { + name: 'api', + use: { ...devices['API'] }, + grep: /@api/, // only run tests tagged @api + }, ], }); diff --git a/tests/delete-api.spec.js b/tests/delete-api.spec.js index 41a61c1..e85a3da 100644 --- a/tests/delete-api.spec.js +++ b/tests/delete-api.spec.js @@ -17,7 +17,7 @@ test.describe('DELETE User API', () => { expect(body).toHaveProperty('isDeleted', true); }); - test('Remove user twice', { tag: '@api' }, async ({ request }) => { + test.skip('Remove user twice', { tag: '@api' }, async ({ request }) => { const userId = 2; // First deletion @@ -32,7 +32,7 @@ test.describe('DELETE User API', () => { expect(body2).toHaveProperty('id', userId); }); - test('Validate body is returned', { tag: '@api' }, async ({ request }) => { + test.skip('Validate body is returned', { tag: '@api' }, async ({ request }) => { const userId = 3; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); diff --git a/tests/post-api.spec.js b/tests/post-api.spec.js new file mode 100644 index 0000000..38059eb --- /dev/null +++ b/tests/post-api.spec.js @@ -0,0 +1,98 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; +const ADD_ENDPOINT = '/users/add'; + +test.describe('POST Create User API', () => { + + test('Bad endpoint returns 404', { tag: '@api' }, async ({ request }) => { + const userData = { + firstName: 'Test', + lastName: 'User' + }; + + const response = await request.post(`${API_BASE_URL}${USERS_ENDPOINT}/invalid-endpoint`, { + data: userData + }); + + expect(response.status()).toBe(404); + }); + + test('Invalid JSON payload handling', { tag: '@api' }, async ({ request }) => { + const response = await request.post(`${API_BASE_URL}${ADD_ENDPOINT}`, { + data: 'invalid json string', + headers: { + 'Content-Type': 'application/json' + } + }); + + // Should return 400 Bad Request for invalid JSON + expect([400, 422]).toContain(response.status()); + }); + + test('Too large ID param should return 404', { tag: '@api' }, async ({ request }) => { + const tooLargeId = 999999999; + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/${tooLargeId}`); + + expect(response.status()).toBe(404); + }); + + test('Deleting invalid id returns 200/response but not crash', { tag: '@api' }, async ({ request }) => { + const invalidId = 999999; + const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${invalidId}`); + + expect([200, 404]).toContain(response.status()); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + }); + + test('PUT: Invalid method usage returns appropriate response (no 500)', { tag: '@api' }, async ({ request }) => { + const userId = 1; + const updateData = { + firstName: 'Updated' + }; + + // Try PUT on an endpoint that might not support it properly + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}/invalid`, { + data: updateData + }); + + // Should return appropriate error (400, 404, 405) but not 500 + expect([400, 404, 405, 200]).toContain(response.status()); + expect(response.status()).not.toBe(500); + }); + + test('user schema contains expected keys', { tag: '@api' }, async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); + + expect(response.status()).toBe(200); + const body = await response.json(); + + // Validate expected keys in user schema + const expectedKeys = ['id', 'firstName', 'lastName']; + expectedKeys.forEach(key => { + expect(body).toHaveProperty(key); + }); + }); + + test('users list contains objects with id and email', { tag: '@api' },async ({ request }) => { + const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); + + expect(response.status()).toBe(200); + const body = await response.json(); + const users = body.users || body; + const usersArray = Array.isArray(users) ? users : []; + + if (usersArray.length > 0) { + // Check first user has id and email + const firstUser = usersArray[0]; + expect(firstUser).toHaveProperty('id'); + if (firstUser.email !== undefined) { + expect(typeof firstUser.email).toBe('string'); + } + } + }); +}); diff --git a/tests/updateUser.spec.js b/tests/updateUser.spec.js new file mode 100644 index 0000000..ae4e06a --- /dev/null +++ b/tests/updateUser.spec.js @@ -0,0 +1,134 @@ +// @ts-check +import { expect, test } from '@playwright/test'; + +// Base API URL - adjust this to match your actual API endpoint +const API_BASE_URL = process.env.API_BASE_URL || 'https://dummyjson.com'; +const USERS_ENDPOINT = '/users'; +const AUTH_ENDPOINT = '/auth/login'; + +test.describe('PUT / PATCH Update User API', () => { + + test('Update user details', { tag: '@api' }, async ({ request }) => { + const userId = 1; + const updateData = { + firstName: 'John', + lastName: 'Doe', + age: 30 + }; + + // Try PUT first, fallback to PATCH if needed + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('id', userId); + expect(body).toHaveProperty('firstName', updateData.firstName); + expect(body).toHaveProperty('lastName', updateData.lastName); + }); + + test('Update user with empty payload', { tag: '@api' }, async ({ request }) => { + const userId = 2; + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: {} + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + expect(body).toHaveProperty('id', userId); + }); + + test('Update only one field', { tag: '@api' }, async ({ request }) => { + const userId = 3; + const updateData = { + firstName: 'UpdatedFirstName' + }; + + // Use PATCH for partial update + const response = await request.patch(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('id', userId); + expect(body).toHaveProperty('firstName', updateData.firstName); + }); + + test('Validate returned name field', { tag: '@api' }, async ({ request }) => { + const userId = 4; + const updateData = { + firstName: 'Jane', + lastName: 'Smith' + }; + + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + expect(body).toHaveProperty('firstName'); + expect(body).toHaveProperty('lastName'); + expect(typeof body.firstName).toBe('string'); + expect(typeof body.lastName).toBe('string'); + expect(body.firstName).toBe(updateData.firstName); + expect(body.lastName).toBe(updateData.lastName); + }); + + test('Update and validate response contains updatedAt simulation', { tag: '@api' }, async ({ request }) => { + const userId = 5; + const updateData = { + firstName: 'Updated', + lastName: 'User' + }; + + const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { + data: updateData + }); + + expect(response.status()).toBe(200); + const body = await response.json(); + + // Check for updatedAt or similar timestamp field + const hasTimestamp = body.hasOwnProperty('updatedAt') || + body.hasOwnProperty('updated') || + body.hasOwnProperty('modifiedAt'); + + // At minimum, validate the response structure + expect(body).toBeInstanceOf(Object); + expect(body).toHaveProperty('id', userId); + }); + + test('Login failure (invalid creds)', { tag: '@api' }, async ({ request }) => { + const loginData = { + username: 'invaliduser', + password: 'wrongpassword' + }; + + const response = await request.post(`${API_BASE_URL}${AUTH_ENDPOINT}`, { + data: loginData + }); + + // Should return error status (400 or 401) + expect([400, 401, 403]).toContain(response.status()); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + }); + + test('Login missing fields returns 400', { tag: '@api' }, async ({ request }) => { + const loginData = { + username: 'kminchelle' + }; + + const response = await request.post(`${API_BASE_URL}${AUTH_ENDPOINT}`, { + data: loginData + }); + + expect(response.status()).toBe(400); + const body = await response.json(); + expect(body).toBeInstanceOf(Object); + }); +}); From 2253a480651642eec2c7a33241ed12f48514bda9 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 10 Feb 2026 16:30:04 +0530 Subject: [PATCH 43/67] Updated the playwright config file --- tests/cart_checkout.spec.js | 177 ++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 tests/cart_checkout.spec.js diff --git a/tests/cart_checkout.spec.js b/tests/cart_checkout.spec.js new file mode 100644 index 0000000..123f880 --- /dev/null +++ b/tests/cart_checkout.spec.js @@ -0,0 +1,177 @@ +// @ts-check +import { expect, test } from '@playwright/test'; +import AllPages from '../pages/AllPages.js'; + +let allPages; + +test.beforeEach(async ({ page }) => { + allPages = new AllPages(page); + await page.goto('/'); +}); + +test.describe('Cart Module', () => { + test.describe('Product Removal', () => { + test('Verify that user is able to delete selected product from cart ',{tag: '@ios'}, async () => { + const productName = 'GoPro HERO10 Black'; + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); + + await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.verifyCartItemVisible(productName); + await allPages.cartPage.clickOnDeleteProductIcon(); + await allPages.cartPage.verifyCartItemDeleted(productName); + + }); + }); +}); + +test.describe('Orders Module', () => { + test.describe('Order Cancellation', () => { + test('Verify new user views and cancels an order in my orders ',{tag: '@chromium'}, async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + let productName = `Rode NT1-A Condenser Mic`; + + await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + }); + + await test.step('Navigate to All Products and add view details of a random product', async () => { + await allPages.homePage.clickAllProductsNav(); + productName = await allPages.allProductsPage.getNthProductName(1); + await allPages.allProductsPage.clickNthProduct(1); + await allPages.productDetailsPage.clickAddToCartButton(); + }); + + await test.step('Add product to cart, add new address and checkout', async () => { + await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.assertYourCartTitle(); + // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.fillShippingAddress( + // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.assertAddressAddedToast(); + // }); + + // await test.step('Complete order and verify in my orders', async () => { + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.inventoryPage.clickOnContinueShopping(); + + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.clickCancelOrderButton(); + // await allPages.orderPage.confirmCancellation(); + }); + }); + }); +}); + +test.describe('User Journey', () => { + test.describe('Multiple Order Placement', () => { + test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement ',{tag: '@chromium'}, async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + }); + + await test.step('Navigate product details and validate tabs', async () => { + await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickOnReviewsTab(); + // await allPages.productDetailsPage.assertReviewsTab(); + // await allPages.productDetailsPage.clickOnAdditionalInfoTab(); + // await allPages.productDetailsPage.assertAdditionalInfoTab(); + }); + + await test.step('Place first order', async () => { + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.fillShippingAddress( + // process.env.SFIRST_NAME, + // email, + // process.env.SCITY, + // process.env.SSTATE, + // process.env.SSTREET_ADD, + // process.env.SZIP_CODE, + // process.env.SCOUNTRY + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.checkoutPage.clickOnContinueShoppingButton(); + }); + + await test.step('Place second order using existing address', async () => { + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + }); + }); + }); +}); + +test.describe('Authentication', () => { + test.describe('Signup & Login', () => { + test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully ',{tag: '@chromium'}, async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + }); + }); +}); + +test.describe('User Profile', () => { + test.describe('Personal Information', () => { + test('Verify that user can update personal information ',{tag: '@firefox'}, async () => { + await allPages.userPage.clickOnUserProfileIcon(); + // await allPages.userPage.updatePersonalInfo(); + // await allPages.userPage.verifyPersonalInfoUpdated(); + }); + }); +}); From 8a306ced372077de5e5152cf2ce63fa9e3868b11 Mon Sep 17 00:00:00 2001 From: TestDino Date: Tue, 10 Feb 2026 16:31:11 +0530 Subject: [PATCH 44/67] Fixed the changes related to pricing currency --- tests/cart_checkout.spec.js | 176 ++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 tests/cart_checkout.spec.js diff --git a/tests/cart_checkout.spec.js b/tests/cart_checkout.spec.js new file mode 100644 index 0000000..db8711f --- /dev/null +++ b/tests/cart_checkout.spec.js @@ -0,0 +1,176 @@ +// @ts-check +import { expect, test } from '@playwright/test'; +import AllPages from '../pages/AllPages.js'; + +let allPages; + +test.beforeEach(async ({ page }) => { + allPages = new AllPages(page); + await page.goto('/'); +}); + +test.describe('Cart Module', () => { + test.describe('Product Removal', () => { + test('Verify that user is able to delete selected product from cart ',{tag: '@ios'}, async () => { + const productName = 'GoPro HERO10 Black'; + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); + + await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.verifyCartItemVisible(productName); + await allPages.cartPage.clickOnDeleteProductIcon(); + await allPages.cartPage.verifyCartItemDeleted(productName); + + }); + }); +}); + +test.describe('Orders Module', () => { + test.describe('Order Cancellation', () => { + test('Verify new user views and cancels an order in my orders ',{tag: '@chromium'}, async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + let productName = `Rode NT1-A Condenser Mic`; + + await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + }); + + await test.step('Navigate to All Products and add view details of a random product', async () => { + await allPages.homePage.clickAllProductsNav(); + productName = await allPages.allProductsPage.getNthProductName(1); + await allPages.allProductsPage.clickNthProduct(1); + await allPages.productDetailsPage.clickAddToCartButton(); + }); + + await test.step('Add product to cart, add new address and checkout', async () => { + await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.assertYourCartTitle(); + // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.fillShippingAddress( + // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // }); + + // await test.step('Complete order and verify in my orders', async () => { + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.inventoryPage.clickOnContinueShopping(); + + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.clickCancelOrderButton(); + // await allPages.orderPage.confirmCancellation(); + }); + }); + }); +}); + +test.describe('User Journey', () => { + test.describe('Multiple Order Placement', () => { + test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement ',{tag: '@chromium'}, async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + }); + + await test.step('Navigate product details and validate tabs', async () => { + await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickOnReviewsTab(); + // await allPages.productDetailsPage.assertReviewsTab(); + // await allPages.productDetailsPage.clickOnAdditionalInfoTab(); + // await allPages.productDetailsPage.assertAdditionalInfoTab(); + }); + + await test.step('Place first order', async () => { + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.fillShippingAddress( + // process.env.SFIRST_NAME, + // email, + // process.env.SCITY, + // process.env.SSTATE, + // process.env.SSTREET_ADD, + // process.env.SZIP_CODE, + // process.env.SCOUNTRY + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.checkoutPage.clickOnContinueShoppingButton(); + }); + + await test.step('Place second order using existing address', async () => { + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + }); + }); + }); +}); + +test.describe('Authentication', () => { + test.describe('Signup & Login', () => { + test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully ',{tag: '@chromium'}, async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + }); + }); +}); + +test.describe('User Profile', () => { + test.describe('Personal Information', () => { + test('Verify that user can update personal information ',{tag: '@firefox'}, async () => { + await allPages.userPage.clickOnUserProfileIcon(); + // await allPages.userPage.updatePersonalInfo(); + // await allPages.userPage.verifyPersonalInfoUpdated(); + }); + }); +}); From 98a63e7406b99472e9d73ef4463b22f8d7824e85 Mon Sep 17 00:00:00 2001 From: TestDino Date: Thu, 12 Feb 2026 11:26:06 +0530 Subject: [PATCH 45/67] fix(trace): updated the yml related to trace --- tests/updateUser.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/updateUser.spec.js b/tests/updateUser.spec.js index ae4e06a..d2770c8 100644 --- a/tests/updateUser.spec.js +++ b/tests/updateUser.spec.js @@ -16,7 +16,6 @@ test.describe('PUT / PATCH Update User API', () => { age: 30 }; - // Try PUT first, fallback to PATCH if needed const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { data: updateData }); From bb0d8440829bac1c893037539391d5ab7aadfb4b Mon Sep 17 00:00:00 2001 From: TestDino Date: Fri, 13 Feb 2026 14:17:19 +0530 Subject: [PATCH 46/67] Managing flaky test cases in hotfix env --- playwright.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.js b/playwright.config.js index de2d0a8..a59e4df 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -24,7 +24,7 @@ export default defineConfig({ ], use: { - baseURL: 'https://demo.alphabin.co/', + baseURL: 'storedemo.testdino.com', headless: true, trace: 'on-first-retry', screenshot: 'only-on-failure', From d00700c725b1889a3ddc4c028723faffadf61efd Mon Sep 17 00:00:00 2001 From: TestDino Date: Fri, 13 Feb 2026 17:52:24 +0530 Subject: [PATCH 47/67] Managing flaky test cases --- playwright.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.js b/playwright.config.js index a59e4df..09e3ba2 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -24,7 +24,7 @@ export default defineConfig({ ], use: { - baseURL: 'storedemo.testdino.com', + baseURL: 'https://storedemo.testdino.com', headless: true, trace: 'on-first-retry', screenshot: 'only-on-failure', From 6af987a755f70b43de46ea5d65c154ff301ed10d Mon Sep 17 00:00:00 2001 From: TestDino Date: Wed, 18 Feb 2026 18:54:53 +0530 Subject: [PATCH 48/67] Updated and fixed failed test cases --- tests/example.spec.js | 88 ++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 47 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 288a098..9617a1b 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -20,17 +20,47 @@ async function login( // await allPages.loginPage.login(username, password); } -/* ---------- DEMO FLAKY TEST ---------- */ - -test( - 'Verify that user can login and logout successfully', - {tag: '@chromium'}, - async () => { - await login(); - // Intentionally failing test (no flaky/retry behavior) - throw new Error('Intentional permanent failure: login/logout test'); - } -); +/* ---------- FLAKY TESTS (fail on 1st run + 1st retry, pass on 2nd retry) ---------- */ + +test.describe('Flaky tests (pass on 2nd retry)', () => { + test.describe.configure({ retries: 2 }); + + test( + 'Verify that user can login and logout successfully', + { tag: '@chromium' }, + async ({}, testInfo) => { + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + } + ); + + test( + 'User searches products and views result (Searchbox)', + { tag: '@firefox' }, + async ({}, testInfo) => { + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + } + ); + + test( + 'User navigates through product categories (Product page)', + { tag: '@webkit' }, + async ({}, testInfo) => { + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + } + ); +}); /* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ @@ -141,24 +171,6 @@ test( } ); -test( - 'User searches products and views result (Searchbox)', - { tag: '@firefox' }, - async () => { - await login(); - // Intentionally failing test (no flaky/retry behavior) - throw new Error('Intentional permanent failure: search test'); - - // ✅ Pass on rerun - // await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.searchProduct('laptop'); - // await allPages.allProductsPage.verifySearchResultsVisible(); - - await expect(true).toBeTruthy(); - } -); - - test( 'Verify that user can update cart quantity and verify total price', { tag: '@chromium' }, @@ -174,24 +186,6 @@ test( } ); -test( - 'User navigates through product categories (Product page)', - { tag: '@webkit' }, - async () => { - await login(); - // Intentionally failing test (no flaky/retry behavior) - throw new Error('Intentional permanent failure: category navigation test'); - - // ✅ Pass on rerun - // await allPages.homePage.clickAllProductsNav(); - // await allPages.allProductsPage.selectCategory('Electronics'); - // await allPages.allProductsPage.verifyCategoryFilterApplied(); - - await expect(true).toBeTruthy(); - } -); - - test( 'Verify that user can view order history and order detail (Order page)', { tag: '@firefox' }, From 06805b3a98c185056d824936800a6a442912cc64 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:41:44 +0530 Subject: [PATCH 49/67] Refactor cart checkout tests and add helper functions --- tests/cart_checkout.spec.js | 403 ++++++++++++++++++++++-------------- 1 file changed, 243 insertions(+), 160 deletions(-) diff --git a/tests/cart_checkout.spec.js b/tests/cart_checkout.spec.js index db8711f..4106cbb 100644 --- a/tests/cart_checkout.spec.js +++ b/tests/cart_checkout.spec.js @@ -9,168 +9,251 @@ test.beforeEach(async ({ page }) => { await page.goto('/'); }); -test.describe('Cart Module', () => { - test.describe('Product Removal', () => { - test('Verify that user is able to delete selected product from cart ',{tag: '@ios'}, async () => { - const productName = 'GoPro HERO10 Black'; - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnDeleteProductIcon(); - await allPages.cartPage.verifyCartItemDeleted(productName); - - }); - }); -}); +/* ---------- Helpers ---------- */ -test.describe('Orders Module', () => { - test.describe('Order Cancellation', () => { - test('Verify new user views and cancels an order in my orders ',{tag: '@chromium'}, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - let productName = `Rode NT1-A Condenser Mic`; - - await test.step('Verify that user can register successfully', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - }); - - await test.step('Navigate to All Products and add view details of a random product', async () => { - await allPages.homePage.clickAllProductsNav(); - productName = await allPages.allProductsPage.getNthProductName(1); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickAddToCartButton(); - }); - - await test.step('Add product to cart, add new address and checkout', async () => { - await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.assertYourCartTitle(); - // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.fillShippingAddress( - // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - // ); - // await allPages.checkoutPage.clickSaveAddressButton(); - // }); - - // await test.step('Complete order and verify in my orders', async () => { - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // await allPages.inventoryPage.clickOnContinueShopping(); - - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.clickCancelOrderButton(); - // await allPages.orderPage.confirmCancellation(); - }); - }); - }); -}); +async function login( + username = process.env.USERNAME, + password = process.env.PASSWORD +) { + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(username, password); +} -test.describe('User Journey', () => { - test.describe('Multiple Order Placement', () => { - test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement ',{tag: '@chromium'}, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - await test.step('Verify that user can register successfully', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - }); - - await test.step('Navigate product details and validate tabs', async () => { - await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.assertAllProductsTitle(); - // await allPages.allProductsPage.clickNthProduct(1); - // await allPages.productDetailsPage.clickOnReviewsTab(); - // await allPages.productDetailsPage.assertReviewsTab(); - // await allPages.productDetailsPage.clickOnAdditionalInfoTab(); - // await allPages.productDetailsPage.assertAdditionalInfoTab(); - }); - - await test.step('Place first order', async () => { - // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.clickIncreaseQuantityButton(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.fillShippingAddress( - // process.env.SFIRST_NAME, - // email, - // process.env.SCITY, - // process.env.SSTATE, - // process.env.SSTREET_ADD, - // process.env.SZIP_CODE, - // process.env.SCOUNTRY - // ); - // await allPages.checkoutPage.clickSaveAddressButton(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // await allPages.checkoutPage.clickOnContinueShoppingButton(); - }); - - await test.step('Place second order using existing address', async () => { - // await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.assertAllProductsTitle(); - // await allPages.allProductsPage.clickNthProduct(1); - // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }); - }); - }); -}); +/* ---------- FLAKY TESTS (fail on 1st run + 1st retry, pass on 2nd retry) ---------- */ -test.describe('Authentication', () => { - test.describe('Signup & Login', () => { - test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully ',{tag: '@chromium'}, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(email, process.env.PASSWORD); - // await allPages.loginPage.verifySuccessSignIn(); - // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - }); - }); -}); +test.describe('Flaky tests (pass on 2nd retry)', () => { + test.describe.configure({ retries: 2 }); -test.describe('User Profile', () => { - test.describe('Personal Information', () => { - test('Verify that user can update personal information ',{tag: '@firefox'}, async () => { - await allPages.userPage.clickOnUserProfileIcon(); - // await allPages.userPage.updatePersonalInfo(); - // await allPages.userPage.verifyPersonalInfoUpdated(); - }); - }); + test( + 'Verify that user can login and logout successfully', + { tag: '@chromium' }, + async ({}, testInfo) => { + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + } + ); + + test( + 'User searches products and views result (Searchbox)', + { tag: '@firefox' }, + async ({}, testInfo) => { + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + } + ); + + test( + 'User navigates through product categories (Product page)', + { tag: '@webkit' }, + async ({}, testInfo) => { + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + } + ); }); + + +/* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ + +test( + 'Verify that all the navbar are working properly', + { tag: '@webkit' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can edit and delete a product review', + { tag: '@chromium' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that User Can Complete the Journey from Login to Order Placement', + { tag: '@chromium' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can filter products by price range', + { tag: '@firefox' }, + async () => { + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify if user can add product to wishlist, move to cart and checkout', + { tag: '@firefox' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user is able to submit a product review', + { tag: '@webkit' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that all the navbar are working properly (Navbar)', + { tag: '@webkit' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can edit and delete a product review (Single review)', + { tag: '@chromium' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that User Can Complete the Journey from Login to Order Placement (Single order)', + { tag: '@chromium' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can filter products by price range (Price page', + { tag: '@firefox' }, + async () => { + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify if user can add product to wishlist, move to cart(Checkout page)', + { tag: '@firefox' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user is able to submit a product review (Review)', + { tag: '@webkit' }, + async () => { + await login(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can update cart quantity and verify total price', + { tag: '@chromium' }, + async () => { + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can view order history and order detail (Order page)', + { tag: '@firefox' }, + async () => { + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can update cart quantity and verify total price (Pricing)', + { tag: '@chromium' }, + async () => { + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that user can view order history and order details properly (Order details)', + { tag: '@firefox' }, + async () => { + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that users can update cart quantity and verify total price (Single order)', + { tag: '@chromium' }, + async () => { + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + } +); + +test( + 'Verify that users can view order history and order details properly (Order history)', + { tag: '@firefox' }, + async () => { + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + } +); From a648fc1a3983d3a45ccbcf7604c34fad39d7f38f Mon Sep 17 00:00:00 2001 From: TestDino Date: Sat, 28 Feb 2026 16:56:00 +0530 Subject: [PATCH 50/67] Updated test cases by adding tags/annotations --- .../github-login-changed-chromium-darwin.png | Bin 0 -> 39352 bytes .../github-login-chromium-darwin.png | Bin 0 -> 29358 bytes package-lock.json | 1221 ++++++++++++++++- package.json | 3 +- testdino-mcp-1.0.6.tgz | Bin 0 -> 156948 bytes tests/cart_checkout.spec.js | 252 +++- tests/delete-api.spec.js | 36 +- tests/example.spec.js | 184 ++- tests/get-users.spec.js | 144 +- tests/post-api.spec.js | 84 +- tests/updateUser.spec.js | 84 +- tests/visual.spec.js | 12 +- 12 files changed, 1949 insertions(+), 71 deletions(-) create mode 100644 __screenshots__/visual.spec.js-snapshots/github-login-changed-chromium-darwin.png create mode 100644 __screenshots__/visual.spec.js-snapshots/github-login-chromium-darwin.png create mode 100644 testdino-mcp-1.0.6.tgz diff --git a/__screenshots__/visual.spec.js-snapshots/github-login-changed-chromium-darwin.png b/__screenshots__/visual.spec.js-snapshots/github-login-changed-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..81b5cb798972b03a82eb05f9e301d08e814e1cb4 GIT binary patch literal 39352 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

BA)FCK?921 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 b7y`)lO}Ptx>#^m#zM!P2oJgV2=O6zCV>D;% literal 0 HcmV?d00001 diff --git a/__screenshots__/visual.spec.js-snapshots/github-login-chromium-darwin.png b/__screenshots__/visual.spec.js-snapshots/github-login-chromium-darwin.png new file mode 100644 index 0000000000000000000000000000000000000000..568dbbbf457b889d93aa7dda4e9614a2d3118b4d GIT binary patch literal 29358 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

sTUuHgO{CYT^wEDX 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 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 8e94191..008f3b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,66 @@ "license": "ISC", "dependencies": { "dotenv": "^17.2.1", - "playwright": "^1.55.0" + "playwright": "^1.55.0", + "testdino-mcp": "file:testdino-mcp-1.0.6.tgz" }, "devDependencies": { "@playwright/test": "^1.54.1", "@types/node": "^24.1.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@playwright/test": { "version": "1.55.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", @@ -43,10 +96,240 @@ "undici-types": "~7.10.0" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dotenv": { - "version": "17.2.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", - "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -55,6 +338,248 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -69,6 +594,314 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz", + "integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/playwright": { "version": "1.55.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", @@ -99,12 +932,392 @@ "node": ">=18" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/testdino-mcp": { + "version": "1.0.6", + "resolved": "file:testdino-mcp-1.0.6.tgz", + "integrity": "sha512-a62WW0dB33rxZ42vsMYO0RT5pKJK7ZDlscSiu9yDnusjt0O/esdyvYW+LobYmvXWSA7N6UAuTUzxNfnoDCeIcQ==", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.1" + }, + "bin": { + "testdino-mcp": "dist/index.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index 4d7a316..1cbd2da 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "dotenv": "^17.2.1", - "playwright": "^1.55.0" + "playwright": "^1.55.0", + "testdino-mcp": "file:testdino-mcp-1.0.6.tgz" } } diff --git a/testdino-mcp-1.0.6.tgz b/testdino-mcp-1.0.6.tgz new file mode 100644 index 0000000000000000000000000000000000000000..464a81e14389f73c78973f9e9da60f29f0a1630b GIT binary patch literal 156948 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%0A4dQ9NylhPsZoUung9 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

1&pb3oman!DI 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 P#?Su&{gmE)03-|mPlB>4 literal 0 HcmV?d00001 diff --git a/tests/cart_checkout.spec.js b/tests/cart_checkout.spec.js index 4106cbb..37d1907 100644 --- a/tests/cart_checkout.spec.js +++ b/tests/cart_checkout.spec.js @@ -27,7 +27,17 @@ test.describe('Flaky tests (pass on 2nd retry)', () => { test( 'Verify that user can login and logout successfully', - { tag: '@chromium' }, + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Login' }, + { type: 'testdino:link', description: 'https://jira.example.com/LOGIN-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: login and logout on Chromium' } + ] + }, async ({}, testInfo) => { await login(); if (testInfo.retry < 2) { @@ -39,7 +49,17 @@ test.describe('Flaky tests (pass on 2nd retry)', () => { test( 'User searches products and views result (Searchbox)', - { tag: '@firefox' }, + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Search' }, + { type: 'testdino:link', description: 'https://jira.example.com/SEARCH-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: search products on Firefox' } + ] + }, async ({}, testInfo) => { await login(); if (testInfo.retry < 2) { @@ -51,7 +71,17 @@ test.describe('Flaky tests (pass on 2nd retry)', () => { test( 'User navigates through product categories (Product page)', - { tag: '@webkit' }, + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Products' }, + { type: 'testdino:link', description: 'https://jira.example.com/PRODUCTS-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: product categories on WebKit' } + ] + }, async ({}, testInfo) => { await login(); if (testInfo.retry < 2) { @@ -67,7 +97,17 @@ test.describe('Flaky tests (pass on 2nd retry)', () => { test( 'Verify that all the navbar are working properly', - { tag: '@webkit' }, + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar functionality on WebKit' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -76,7 +116,17 @@ test( test( 'Verify that user can edit and delete a product review', - { tag: '@chromium' }, + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete product review on Chromium' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -85,7 +135,17 @@ test( test( 'Verify that User Can Complete the Journey from Login to Order Placement', - { tag: '@chromium' }, + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order placement journey on Chromium' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -94,7 +154,17 @@ test( test( 'Verify that user can filter products by price range', - { tag: '@firefox' }, + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter products by price on Firefox' } + ] + }, async () => { await expect(true).toBeTruthy(); } @@ -102,7 +172,17 @@ test( test( 'Verify if user can add product to wishlist, move to cart and checkout', - { tag: '@firefox' }, + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart and checkout on Firefox' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -111,7 +191,17 @@ test( test( 'Verify that user is able to submit a product review', - { tag: '@webkit' }, + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review on WebKit' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -120,7 +210,17 @@ test( test( 'Verify that all the navbar are working properly (Navbar)', - { tag: '@webkit' }, + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar (Navbar) on WebKit' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -129,7 +229,17 @@ test( test( 'Verify that user can edit and delete a product review (Single review)', - { tag: '@chromium' }, + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete review (Single review) on Chromium' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -138,7 +248,17 @@ test( test( 'Verify that User Can Complete the Journey from Login to Order Placement (Single order)', - { tag: '@chromium' }, + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order (Single order) on Chromium' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -147,7 +267,17 @@ test( test( 'Verify that user can filter products by price range (Price page', - { tag: '@firefox' }, + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter by price (Price page) on Firefox' } + ] + }, async () => { await expect(true).toBeTruthy(); } @@ -155,7 +285,17 @@ test( test( 'Verify if user can add product to wishlist, move to cart(Checkout page)', - { tag: '@firefox' }, + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart (Checkout page) on Firefox' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -164,7 +304,17 @@ test( test( 'Verify that user is able to submit a product review (Review)', - { tag: '@webkit' }, + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review (Review) on WebKit' } + ] + }, async () => { await login(); await expect(true).toBeTruthy(); @@ -173,7 +323,17 @@ test( test( 'Verify that user can update cart quantity and verify total price', - { tag: '@chromium' }, + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Update cart quantity and total price on Chromium' } + ] + }, async () => { await login(); // await allPages.homePage.clickOnShopNowButton(); @@ -188,7 +348,17 @@ test( test( 'Verify that user can view order history and order detail (Order page)', - { tag: '@firefox' }, + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history and order detail (Order page) on Firefox' } + ] + }, async () => { await login(); // await allPages.loginPage.clickOnUserProfileIcon(); @@ -202,7 +372,17 @@ test( test( 'Verify that user can update cart quantity and verify total price (Pricing)', - { tag: '@chromium' }, + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Cart quantity and total price (Pricing) on Chromium' } + ] + }, async () => { await login(); // await allPages.homePage.clickOnShopNowButton(); @@ -217,7 +397,17 @@ test( test( 'Verify that user can view order history and order details properly (Order details)', - { tag: '@firefox' }, + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history and details (Order details) on Firefox' } + ] + }, async () => { await login(); // await allPages.loginPage.clickOnUserProfileIcon(); @@ -231,7 +421,17 @@ test( test( 'Verify that users can update cart quantity and verify total price (Single order)', - { tag: '@chromium' }, + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Cart quantity (Single order) on Chromium' } + ] + }, async () => { await login(); // await allPages.homePage.clickOnShopNowButton(); @@ -246,7 +446,17 @@ test( test( 'Verify that users can view order history and order details properly (Order history)', - { tag: '@firefox' }, + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history on Firefox' } + ] + }, async () => { await login(); // await allPages.loginPage.clickOnUserProfileIcon(); diff --git a/tests/delete-api.spec.js b/tests/delete-api.spec.js index 41a61c1..d2aef11 100644 --- a/tests/delete-api.spec.js +++ b/tests/delete-api.spec.js @@ -7,7 +7,17 @@ const USERS_ENDPOINT = '/users'; test.describe('DELETE User API', () => { - test('Remove user 1', { tag: '@api' }, async ({ request }) => { + test('Remove user 1', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'DELETE remove user by ID' } + ] + }, async ({ request }) => { const userId = 1; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); @@ -17,7 +27,17 @@ test.describe('DELETE User API', () => { expect(body).toHaveProperty('isDeleted', true); }); - test('Remove user twice', { tag: '@api' }, async ({ request }) => { + test('Remove user twice', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'DELETE user twice idempotency' } + ] + }, async ({ request }) => { const userId = 2; // First deletion @@ -32,7 +52,17 @@ test.describe('DELETE User API', () => { expect(body2).toHaveProperty('id', userId); }); - test('Validate body is returned', { tag: '@api' }, async ({ request }) => { + test('Validate body is returned', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'DELETE response body validation' } + ] + }, async ({ request }) => { const userId = 3; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); diff --git a/tests/example.spec.js b/tests/example.spec.js index dc3d25d..0811395 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -28,12 +28,32 @@ async function logout() { await allPages.loginPage.clickOnLogoutButton(); } -test('Verify that user can login and logout successfully', { tag: '@android' }, async () => { +test('Verify that user can login and logout successfully', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Login' }, + { type: 'testdino:link', description: 'https://jira.example.com/LOGIN-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login and logout on Android' } + ] +}, async () => { await login(); await logout(); }); -test('Verify that all the navbar are working properly', { tag: '@webkit' }, async () => { +test('Verify that all the navbar are working properly', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar functionality on WebKit' } + ] +}, async () => { // await login(); await allPages.homePage.clickBackToHomeButton(); // await allPages.homePage.assertHomePage(); @@ -45,7 +65,17 @@ test('Verify that all the navbar are working properly', { tag: '@webkit' }, asyn await allPages.homePage.assertAboutUsTitle(); }); -test('Verify that user can edit and delete a product review', { tag: '@firefox' }, async () => { +test('Verify that user can edit and delete a product review', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete product review on Firefox' } + ] +}, async () => { await test.step('Login as existing user and navigate to a product', async () => { // await login(); }) @@ -83,7 +113,17 @@ test('Verify that user can edit and delete a product review', { tag: '@firefox' }) }); -test('Verify that User Can Complete the Journey from Login to Order Placement', { tag: '@ios' }, async () => { +test('Verify that User Can Complete the Journey from Login to Order Placement', { + tag: '@ios', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order placement journey on iOS' } + ] +}, async () => { const productName = 'GoPro HERO10 Black'; // await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -103,7 +143,17 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', { tag: '@android' }, async () => { +test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Registration' }, + { type: 'testdino:link', description: 'https://jira.example.com/REG-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Registration to single order on Android' } + ] +}, async () => { // fresh test data const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; @@ -215,7 +265,17 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra }); }); -test('Verify that user add product to cart before logging in and then complete order after logging in', { tag: '@webkit' }, async () => { +test('Verify that user add product to cart before logging in and then complete order after logging in', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Add to cart before login, order after login on WebKit' } + ] +}, async () => { await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); @@ -235,7 +295,17 @@ test('Verify that user add product to cart before logging in and then complete o // }) }); -test('Verify that user can filter products by price range', { tag: '@filter' }, async () => { +test('Verify that user can filter products by price range', { + tag: '@filter', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter products by price range' } + ] +}, async () => { await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); @@ -243,7 +313,17 @@ test('Verify that user can filter products by price range', { tag: '@filter' }, await allPages.homePage.clickOnFilterButton(); }); -test('Verify if user can add product to wishlist, moves it to card and then checks out', { tag: '@wishlist' }, async () => { +test('Verify if user can add product to wishlist, moves it to card and then checks out', { + tag: '@wishlist', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart and checkout' } + ] +}, async () => { // await login(); await test.step('Add product to wishlistand then add to cart', async () => { @@ -267,7 +347,19 @@ test('Verify if user can add product to wishlist, moves it to card and then chec }); -test('Verify new user views and cancels an order in my orders', { tag: '@chromium' }, async () => { +test.describe('Orders Module', () => { + test.describe('Order Cancellation', () => { + test('Verify new user views and cancels an order in my orders ', { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p0' }, + { type: 'testdino:feature', description: 'Orders' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-002' }, + { type: 'testdino:owner', description: '@Kriti Verma' }, + { type: 'testdino:notify-slack', description: '@Kriti Verma' }, + { type: 'testdino:context', description: 'Critical order cancellation flow for new users' } + ] + }, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -323,9 +415,21 @@ test('Verify new user views and cancels an order in my orders', { tag: '@chromiu // await allPages.orderPage.clickCancelOrderButton(); // await allPages.orderPage.confirmCancellation(); // }); + }); + }); }); -test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', { tag: '@firefox' }, async () => { +test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Registration to multiple order placement on Firefox' } + ] +}, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -373,7 +477,17 @@ test('Verify That a New User Can Successfully Complete the Journey from Registra }) }); -test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', { tag: '@ios' }, async () => { +test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', { + tag: '@ios', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Registration' }, + { type: 'testdino:link', description: 'https://jira.example.com/REG-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Sign up, login and navigate home on iOS' } + ] +}, async () => { const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -395,7 +509,17 @@ test('Verify that the new user is able to Sign Up, Log In, and Navigate to the H // }) }) -test('Verify that user is able to fill Contact Us page successfully', { tag: '@chromium' }, async () => { +test('Verify that user is able to fill Contact Us page successfully', { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Contact' }, + { type: 'testdino:link', description: 'https://jira.example.com/CONTACT-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Contact Us form submission on Chromium' } + ] +}, async () => { await login(); await allPages.homePage.clickOnContactUsLink(); await allPages.contactUsPage.assertContactUsTitle(); @@ -403,7 +527,17 @@ test('Verify that user is able to fill Contact Us page successfully', { tag: '@c await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); }); -test('Verify that user is able to submit a product review', { tag: '@firefox' }, async () => { +test('Verify that user is able to submit a product review', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review on Firefox' } + ] +}, async () => { await test.step('Login as existing user and navigate to a product', async () => { // await login(); }) @@ -428,13 +562,33 @@ test('Verify that user is able to submit a product review', { tag: '@firefox' }, }) }); -test('Verify that user can update personal information', { tag: '@webkit' }, async () => { +test('Verify that user can update personal information', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Profile' }, + { type: 'testdino:link', description: 'https://jira.example.com/PROFILE-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Update personal information on WebKit' } + ] +}, async () => { await allPages.userPage.clickOnUserProfileIcon(); // await allPages.userPage.updatePersonalInfo(); // await allPages.userPage.verifyPersonalInfoUpdated(); }); -test('Verify that user is able to delete selected product from cart', { tag: '@android' }, async () => { +test('Verify that user is able to delete selected product from cart', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Delete selected product from cart on Android' } + ] +}, async () => { const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); diff --git a/tests/get-users.spec.js b/tests/get-users.spec.js index 1ab0a3d..31f6e3f 100644 --- a/tests/get-users.spec.js +++ b/tests/get-users.spec.js @@ -7,7 +7,17 @@ const USERS_ENDPOINT = '/users'; test.describe('GET Users API', () => { - test('Fetch all users', { tag: '@api' }, async ({ request }) => { + test('Fetch all users', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'GET fetch all users' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); @@ -16,7 +26,17 @@ test.describe('GET Users API', () => { expect(Array.isArray(body.users)).toBe(true); }); - test('Fetch user by ID = 1', { tag: '@api' }, async ({ request }) => { + test('Fetch user by ID = 1', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'GET user by ID' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -26,7 +46,17 @@ test.describe('GET Users API', () => { expect(body).toHaveProperty('lastName'); }); - test('Validate total users > 0', { tag: '@api' }, async ({ request }) => { + test('Validate total users > 0', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate total users count' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); @@ -35,7 +65,17 @@ test.describe('GET Users API', () => { expect(body.total).toBeGreaterThan(0); }); - test('Validate user image exists', { tag: '@api' }, async ({ request }) => { + test('Validate user image exists', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate user image field' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -45,7 +85,17 @@ test.describe('GET Users API', () => { expect(typeof body.image).toBe('string'); }); - test('Validate user 1 has firstName field', { tag: '@api' }, async ({ request }) => { + test('Validate user 1 has firstName field', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate firstName field' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -55,13 +105,33 @@ test.describe('GET Users API', () => { expect(body.firstName.length).toBeGreaterThan(0); }); - test('Invalid user ID returns 404', { tag: '@api' }, async ({ request }) => { + test('Invalid user ID returns 404', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-006' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Invalid user ID returns 404' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/999999`); expect(response.status()).toBe(404); }); - test('default users (no query) returns data object/array', { tag: '@api' }, async ({ request }) => { + test('default users (no query) returns data object/array', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-007' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Default users response structure' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); @@ -71,7 +141,17 @@ test.describe('GET Users API', () => { expect(body.users || Array.isArray(body)).toBeTruthy(); }); - test('limit param returns limited results', { tag: '@api' }, async ({ request }) => { + test('limit param returns limited results', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-008' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Limit param pagination' } + ] + }, async ({ request }) => { const limit = 5; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}`); @@ -82,7 +162,17 @@ test.describe('GET Users API', () => { expect(usersArray.length).toBeLessThanOrEqual(limit); }); - test('skip param shifts results', { tag: '@api' }, async ({ request }) => { + test('skip param shifts results', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-009' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Skip param pagination' } + ] + }, async ({ request }) => { const skip = 5; const limit = 10; @@ -107,7 +197,17 @@ test.describe('GET Users API', () => { } }); - test('sorting / search query (if supported) returns filtered results', { tag: '@api' }, async ({ request }) => { + test('sorting / search query (if supported) returns filtered results', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-010' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Search query filtered results' } + ] + }, async ({ request }) => { // Try search query parameter (common patterns: q, search, query) const searchTerm = 'john'; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/search?q=${searchTerm}`); @@ -123,7 +223,17 @@ test.describe('GET Users API', () => { } }); - test('delayed response (3s) should return 200', { tag: '@api' }, async ({ request }) => { + test('delayed response (3s) should return 200', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-011' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Delayed response returns 200' } + ] + }, async ({ request }) => { const isRetry = test.info().retry > 0; if (!isRetry) { expect(true).toBe(false); @@ -146,7 +256,17 @@ test.describe('GET Users API', () => { expect(body).toBeInstanceOf(Object); }); - test('enforce timeout (expect to fail if too slow) — set short timeout', { tag: '@api' }, async ({ request }) => { + test('enforce timeout (expect to fail if too slow) — set short timeout', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-012' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Enforce request timeout' } + ] + }, async ({ request }) => { const delay = 5; const shortTimeout = 2000; diff --git a/tests/post-api.spec.js b/tests/post-api.spec.js index 38059eb..27ac322 100644 --- a/tests/post-api.spec.js +++ b/tests/post-api.spec.js @@ -8,7 +8,17 @@ const ADD_ENDPOINT = '/users/add'; test.describe('POST Create User API', () => { - test('Bad endpoint returns 404', { tag: '@api' }, async ({ request }) => { + test('Bad endpoint returns 404', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'POST bad endpoint returns 404' } + ] + }, async ({ request }) => { const userData = { firstName: 'Test', lastName: 'User' @@ -21,7 +31,17 @@ test.describe('POST Create User API', () => { expect(response.status()).toBe(404); }); - test('Invalid JSON payload handling', { tag: '@api' }, async ({ request }) => { + test('Invalid JSON payload handling', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Invalid JSON payload handling' } + ] + }, async ({ request }) => { const response = await request.post(`${API_BASE_URL}${ADD_ENDPOINT}`, { data: 'invalid json string', headers: { @@ -33,14 +53,34 @@ test.describe('POST Create User API', () => { expect([400, 422]).toContain(response.status()); }); - test('Too large ID param should return 404', { tag: '@api' }, async ({ request }) => { + test('Too large ID param should return 404', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Too large ID param returns 404' } + ] + }, async ({ request }) => { const tooLargeId = 999999999; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/${tooLargeId}`); expect(response.status()).toBe(404); }); - test('Deleting invalid id returns 200/response but not crash', { tag: '@api' }, async ({ request }) => { + test('Deleting invalid id returns 200/response but not crash', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Delete invalid id response handling' } + ] + }, async ({ request }) => { const invalidId = 999999; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${invalidId}`); @@ -49,7 +89,17 @@ test.describe('POST Create User API', () => { expect(body).toBeInstanceOf(Object); }); - test('PUT: Invalid method usage returns appropriate response (no 500)', { tag: '@api' }, async ({ request }) => { + test('PUT: Invalid method usage returns appropriate response (no 500)', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Invalid PUT method usage response' } + ] + }, async ({ request }) => { const userId = 1; const updateData = { firstName: 'Updated' @@ -65,7 +115,17 @@ test.describe('POST Create User API', () => { expect(response.status()).not.toBe(500); }); - test('user schema contains expected keys', { tag: '@api' }, async ({ request }) => { + test('user schema contains expected keys', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-006' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'User schema expected keys validation' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -78,7 +138,17 @@ test.describe('POST Create User API', () => { }); }); - test('users list contains objects with id and email', { tag: '@api' },async ({ request }) => { + test('users list contains objects with id and email', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-007' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Users list id and email validation' } + ] + }, async ({ request }) => { const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); diff --git a/tests/updateUser.spec.js b/tests/updateUser.spec.js index d2770c8..5a86e2a 100644 --- a/tests/updateUser.spec.js +++ b/tests/updateUser.spec.js @@ -8,7 +8,17 @@ const AUTH_ENDPOINT = '/auth/login'; test.describe('PUT / PATCH Update User API', () => { - test('Update user details', { tag: '@api' }, async ({ request }) => { + test('Update user details', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'PUT/PATCH update user API' } + ] + }, async ({ request }) => { const userId = 1; const updateData = { firstName: 'John', @@ -27,7 +37,17 @@ test.describe('PUT / PATCH Update User API', () => { expect(body).toHaveProperty('lastName', updateData.lastName); }); - test('Update user with empty payload', { tag: '@api' }, async ({ request }) => { + test('Update user with empty payload', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Update user with empty payload' } + ] + }, async ({ request }) => { const userId = 2; const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { data: {} @@ -39,7 +59,17 @@ test.describe('PUT / PATCH Update User API', () => { expect(body).toHaveProperty('id', userId); }); - test('Update only one field', { tag: '@api' }, async ({ request }) => { + test('Update only one field', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'PATCH partial user update' } + ] + }, async ({ request }) => { const userId = 3; const updateData = { firstName: 'UpdatedFirstName' @@ -56,7 +86,17 @@ test.describe('PUT / PATCH Update User API', () => { expect(body).toHaveProperty('firstName', updateData.firstName); }); - test('Validate returned name field', { tag: '@api' }, async ({ request }) => { + test('Validate returned name field', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate returned name field from update' } + ] + }, async ({ request }) => { const userId = 4; const updateData = { firstName: 'Jane', @@ -77,7 +117,17 @@ test.describe('PUT / PATCH Update User API', () => { expect(body.lastName).toBe(updateData.lastName); }); - test('Update and validate response contains updatedAt simulation', { tag: '@api' }, async ({ request }) => { + test('Update and validate response contains updatedAt simulation', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Validate updatedAt in update response' } + ] + }, async ({ request }) => { const userId = 5; const updateData = { firstName: 'Updated', @@ -101,7 +151,17 @@ test.describe('PUT / PATCH Update User API', () => { expect(body).toHaveProperty('id', userId); }); - test('Login failure (invalid creds)', { tag: '@api' }, async ({ request }) => { + test('Login failure (invalid creds)', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-006' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Auth login failure with invalid credentials' } + ] + }, async ({ request }) => { const loginData = { username: 'invaliduser', password: 'wrongpassword' @@ -117,7 +177,17 @@ test.describe('PUT / PATCH Update User API', () => { expect(body).toBeInstanceOf(Object); }); - test('Login missing fields returns 400', { tag: '@api' }, async ({ request }) => { + test('Login missing fields returns 400', { + tag: '@api', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'API' }, + { type: 'testdino:link', description: 'https://jira.example.com/API-007' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login missing fields returns 400' } + ] + }, async ({ request }) => { const loginData = { username: 'kminchelle' }; diff --git a/tests/visual.spec.js b/tests/visual.spec.js index a410be9..b0a2172 100644 --- a/tests/visual.spec.js +++ b/tests/visual.spec.js @@ -11,7 +11,17 @@ test.beforeEach(async ({ page }) => { test.describe('Visual Comparison', () => { test.describe('GitHub Login Page', () => { - test('visual comparison demo test', { tag: ['@visual', '@chromium'] }, async ({ page }) => { + test('visual comparison demo test', { + tag: ['@visual', '@chromium'], + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Visual' }, + { type: 'testdino:link', description: 'https://jira.example.com/VISUAL-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Visual comparison demo for GitHub login on Chromium' } + ] + }, async ({ page }) => { await page.goto('https://github.com/login'); await expect(page).toHaveScreenshot('github-login.png'); From 6b84ceefb1b5ea6861d52ba1bdcca2d342bc95ca Mon Sep 17 00:00:00 2001 From: TestDino Date: Sat, 28 Feb 2026 17:27:54 +0530 Subject: [PATCH 51/67] Updated test cases by adding tags/annotations --- tests/cart_checkout.spec.js | 231 ++++++++++++++++++++++++++++++++++ tests/delete-api.spec.js | 60 +++++++++ tests/example.spec.js | 182 ++++++++++++++++++++++++++- tests/get-users.spec.js | 240 ++++++++++++++++++++++++++++++++++++ tests/post-api.spec.js | 140 +++++++++++++++++++++ tests/updateUser.spec.js | 140 +++++++++++++++++++++ tests/visual.spec.js | 14 ++- 7 files changed, 1000 insertions(+), 7 deletions(-) diff --git a/tests/cart_checkout.spec.js b/tests/cart_checkout.spec.js index 37d1907..7b86928 100644 --- a/tests/cart_checkout.spec.js +++ b/tests/cart_checkout.spec.js @@ -39,11 +39,22 @@ test.describe('Flaky tests (pass on 2nd retry)', () => { ] }, async ({}, testInfo) => { + const start = Date.now(); await login(); if (testInfo.retry < 2) { throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); } await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -61,11 +72,22 @@ test.describe('Flaky tests (pass on 2nd retry)', () => { ] }, async ({}, testInfo) => { + const start = Date.now(); await login(); if (testInfo.retry < 2) { throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); } await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -83,11 +105,22 @@ test.describe('Flaky tests (pass on 2nd retry)', () => { ] }, async ({}, testInfo) => { + const start = Date.now(); await login(); if (testInfo.retry < 2) { throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); } await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); }); @@ -109,8 +142,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -128,8 +172,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -147,8 +202,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -166,7 +232,18 @@ test( ] }, async () => { + const start = Date.now(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -184,8 +261,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -203,8 +291,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -222,8 +321,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -241,8 +351,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -260,8 +381,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -279,7 +411,18 @@ test( ] }, async () => { + const start = Date.now(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -297,8 +440,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -316,8 +470,19 @@ test( ] }, async () => { + const start = Date.now(); await login(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -335,6 +500,7 @@ test( ] }, async () => { + const start = Date.now(); await login(); // await allPages.homePage.clickOnShopNowButton(); // await allPages.allProductsPage.clickNthProduct(1); @@ -343,6 +509,16 @@ test( // await allPages.cartPage.clickIncreaseQuantityButton(); // await allPages.cartPage.verifyTotalPriceUpdated(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -360,6 +536,7 @@ test( ] }, async () => { + const start = Date.now(); await login(); // await allPages.loginPage.clickOnUserProfileIcon(); // await allPages.orderPage.clickOnMyOrdersTab(); @@ -367,6 +544,16 @@ test( // await allPages.orderPage.clickOnFirstOrder(); // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -384,6 +571,7 @@ test( ] }, async () => { + const start = Date.now(); await login(); // await allPages.homePage.clickOnShopNowButton(); // await allPages.allProductsPage.clickNthProduct(1); @@ -392,6 +580,16 @@ test( // await allPages.cartPage.clickIncreaseQuantityButton(); // await allPages.cartPage.verifyTotalPriceUpdated(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -409,6 +607,7 @@ test( ] }, async () => { + const start = Date.now(); await login(); // await allPages.loginPage.clickOnUserProfileIcon(); // await allPages.orderPage.clickOnMyOrdersTab(); @@ -416,6 +615,16 @@ test( // await allPages.orderPage.clickOnFirstOrder(); // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -433,6 +642,7 @@ test( ] }, async () => { + const start = Date.now(); await login(); // await allPages.homePage.clickOnShopNowButton(); // await allPages.allProductsPage.clickNthProduct(1); @@ -441,6 +651,16 @@ test( // await allPages.cartPage.clickIncreaseQuantityButton(); // await allPages.cartPage.verifyTotalPriceUpdated(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); @@ -458,6 +678,7 @@ test( ] }, async () => { + const start = Date.now(); await login(); // await allPages.loginPage.clickOnUserProfileIcon(); // await allPages.orderPage.clickOnMyOrdersTab(); @@ -465,5 +686,15 @@ test( // await allPages.orderPage.clickOnFirstOrder(); // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); } ); diff --git a/tests/delete-api.spec.js b/tests/delete-api.spec.js index d2aef11..0278cbd 100644 --- a/tests/delete-api.spec.js +++ b/tests/delete-api.spec.js @@ -18,6 +18,7 @@ test.describe('DELETE User API', () => { { type: 'testdino:context', description: 'DELETE remove user by ID' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 1; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); @@ -25,6 +26,25 @@ test.describe('DELETE User API', () => { const body = await response.json(); expect(body).toHaveProperty('id', userId); expect(body).toHaveProperty('isDeleted', true); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Remove user twice', { @@ -38,6 +58,7 @@ test.describe('DELETE User API', () => { { type: 'testdino:context', description: 'DELETE user twice idempotency' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 2; // First deletion @@ -50,6 +71,25 @@ test.describe('DELETE User API', () => { expect(response2.status()).toBe(200); const body2 = await response2.json(); expect(body2).toHaveProperty('id', userId); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 2, + unit: 'count', + }), + }); }); test('Validate body is returned', { @@ -63,6 +103,7 @@ test.describe('DELETE User API', () => { { type: 'testdino:context', description: 'DELETE response body validation' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 3; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`); @@ -73,5 +114,24 @@ test.describe('DELETE User API', () => { expect(body).toBeInstanceOf(Object); expect(body).toHaveProperty('id'); expect(typeof body.id).toBe('number'); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); }); diff --git a/tests/example.spec.js b/tests/example.spec.js index 0811395..e448e9b 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -39,8 +39,19 @@ test('Verify that user can login and logout successfully', { { type: 'testdino:context', description: 'Login and logout on Android' } ] }, async () => { + const start = Date.now(); await login(); await logout(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that all the navbar are working properly', { @@ -54,6 +65,7 @@ test('Verify that all the navbar are working properly', { { type: 'testdino:context', description: 'Navbar functionality on WebKit' } ] }, async () => { + const start = Date.now(); // await login(); await allPages.homePage.clickBackToHomeButton(); // await allPages.homePage.assertHomePage(); @@ -63,6 +75,16 @@ test('Verify that all the navbar are working properly', { await allPages.contactUsPage.assertContactUsTitle(); await allPages.homePage.clickAboutUsNav(); await allPages.homePage.assertAboutUsTitle(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that user can edit and delete a product review', { @@ -76,6 +98,7 @@ test('Verify that user can edit and delete a product review', { { type: 'testdino:context', description: 'Edit and delete product review on Firefox' } ] }, async () => { + const start = Date.now(); await test.step('Login as existing user and navigate to a product', async () => { // await login(); }) @@ -110,7 +133,18 @@ test('Verify that user can edit and delete a product review', { await test.step('Delete the submitted review and verify deletion', async () => { await allPages.productDetailsPage.clickOnDeleteReviewBtn(); - }) + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that User Can Complete the Journey from Login to Order Placement', { @@ -124,6 +158,7 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', { type: 'testdino:context', description: 'Login to order placement journey on iOS' } ] }, async () => { + const start = Date.now(); const productName = 'GoPro HERO10 Black'; // await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -141,6 +176,16 @@ test('Verify that User Can Complete the Journey from Login to Order Placement', // await allPages.checkoutPage.verifyCashOnDeliverySelected(); // await allPages.checkoutPage.clickOnPlaceOrder(); // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', { @@ -154,6 +199,7 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra { type: 'testdino:context', description: 'Registration to single order on Android' } ] }, async () => { + const start = Date.now(); // fresh test data const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; @@ -263,6 +309,17 @@ test('Verify that a New User Can Successfully Complete the Journey from Registra // await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); // await allPages.orderDetailsPage.clickBackToHomeButton(); }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that user add product to cart before logging in and then complete order after logging in', { @@ -276,6 +333,7 @@ test('Verify that user add product to cart before logging in and then complete o { type: 'testdino:context', description: 'Add to cart before login, order after login on WebKit' } ] }, async () => { + const start = Date.now(); await test.step('Navigate and add product to cart before logging in', async () => { await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickProductImage(); @@ -293,6 +351,16 @@ test('Verify that user add product to cart before logging in and then complete o // await allPages.checkoutPage.clickOnPlaceOrder(); // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); // }) + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that user can filter products by price range', { @@ -306,11 +374,22 @@ test('Verify that user can filter products by price range', { { type: 'testdino:context', description: 'Filter products by price range' } ] }, async () => { + const start = Date.now(); await login(); await allPages.homePage.clickOnShopNowButton(); await allPages.homePage.clickOnFilterButton(); await allPages.homePage.AdjustPriceRangeSlider('10000', '20000'); await allPages.homePage.clickOnFilterButton(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify if user can add product to wishlist, moves it to card and then checks out', { @@ -324,6 +403,7 @@ test('Verify if user can add product to wishlist, moves it to card and then chec { type: 'testdino:context', description: 'Wishlist to cart and checkout' } ] }, async () => { + const start = Date.now(); // await login(); await test.step('Add product to wishlistand then add to cart', async () => { @@ -343,8 +423,18 @@ test('Verify if user can add product to wishlist, moves it to card and then chec // await allPages.checkoutPage.verifyCashOnDeliverySelected(); // await allPages.checkoutPage.clickOnPlaceOrder(); // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) - + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test.describe('Orders Module', () => { @@ -360,6 +450,7 @@ test.describe('Orders Module', () => { { type: 'testdino:context', description: 'Critical order cancellation flow for new users' } ] }, async () => { + const start = Date.now(); const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -388,7 +479,18 @@ test.describe('Orders Module', () => { productName = await allPages.allProductsPage.getNthProductName(1); await allPages.allProductsPage.clickNthProduct(1); await allPages.productDetailsPage.clickAddToCartButton(); - }) + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'order-flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); // await test.step('Add product to cart, add new address and checkout', async () => { // await allPages.productDetailsPage.clickCartIcon(); @@ -430,6 +532,7 @@ test('Verify That a New User Can Successfully Complete the Journey from Registra { type: 'testdino:context', description: 'Registration to multiple order placement on Firefox' } ] }, async () => { + const start = Date.now(); const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -474,7 +577,18 @@ test('Verify That a New User Can Successfully Complete the Journey from Registra // await allPages.checkoutPage.verifyCashOnDeliverySelected(); // await allPages.checkoutPage.clickOnPlaceOrder(); // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', { @@ -488,6 +602,7 @@ test('Verify that the new user is able to Sign Up, Log In, and Navigate to the H { type: 'testdino:context', description: 'Sign up, login and navigate home on iOS' } ] }, async () => { + const start = Date.now(); const email = `test+${Date.now()}@test.com`; const firstName = 'Test'; const lastName = 'User'; @@ -507,6 +622,16 @@ test('Verify that the new user is able to Sign Up, Log In, and Navigate to the H // await allPages.loginPage.verifySuccessSignIn(); // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); // }) + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }) test('Verify that user is able to fill Contact Us page successfully', { @@ -520,11 +645,22 @@ test('Verify that user is able to fill Contact Us page successfully', { { type: 'testdino:context', description: 'Contact Us form submission on Chromium' } ] }, async () => { + const start = Date.now(); await login(); await allPages.homePage.clickOnContactUsLink(); await allPages.contactUsPage.assertContactUsTitle(); await allPages.contactUsPage.fillContactUsForm(); await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that user is able to submit a product review', { @@ -538,6 +674,7 @@ test('Verify that user is able to submit a product review', { { type: 'testdino:context', description: 'Submit product review on Firefox' } ] }, async () => { + const start = Date.now(); await test.step('Login as existing user and navigate to a product', async () => { // await login(); }) @@ -559,7 +696,18 @@ test('Verify that user is able to submit a product review', { title: 'Great Product', opinion: 'This product exceeded my expectations. Highly recommend!' }); - }) + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that user can update personal information', { @@ -573,9 +721,20 @@ test('Verify that user can update personal information', { { type: 'testdino:context', description: 'Update personal information on WebKit' } ] }, async () => { + const start = Date.now(); await allPages.userPage.clickOnUserProfileIcon(); // await allPages.userPage.updatePersonalInfo(); // await allPages.userPage.verifyPersonalInfoUpdated(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); test('Verify that user is able to delete selected product from cart', { @@ -589,6 +748,7 @@ test('Verify that user is able to delete selected product from cart', { { type: 'testdino:context', description: 'Delete selected product from cart on Android' } ] }, async () => { + const start = Date.now(); const productName = 'GoPro HERO10 Black'; await login(); await allPages.inventoryPage.clickOnShopNowButton(); @@ -604,4 +764,14 @@ test('Verify that user is able to delete selected product from cart', { await allPages.cartPage.verifyEmptyCartMessage(); await allPages.cartPage.clickOnStartShoppingButton(); await allPages.allProductsPage.assertAllProductsTitle(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); \ No newline at end of file diff --git a/tests/get-users.spec.js b/tests/get-users.spec.js index 31f6e3f..817e7e6 100644 --- a/tests/get-users.spec.js +++ b/tests/get-users.spec.js @@ -18,12 +18,32 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'GET fetch all users' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); const body = await response.json(); expect(body).toHaveProperty('users'); expect(Array.isArray(body.users)).toBe(true); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Fetch user by ID = 1', { @@ -37,6 +57,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'GET user by ID' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -44,6 +65,25 @@ test.describe('GET Users API', () => { expect(body).toHaveProperty('id', 1); expect(body).toHaveProperty('firstName'); expect(body).toHaveProperty('lastName'); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Validate total users > 0', { @@ -57,12 +97,32 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Validate total users count' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); const body = await response.json(); expect(body).toHaveProperty('total'); expect(body.total).toBeGreaterThan(0); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Validate user image exists', { @@ -76,6 +136,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Validate user image field' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -83,6 +144,25 @@ test.describe('GET Users API', () => { expect(body).toHaveProperty('image'); expect(body.image).toBeTruthy(); expect(typeof body.image).toBe('string'); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Validate user 1 has firstName field', { @@ -96,6 +176,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Validate firstName field' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -103,6 +184,25 @@ test.describe('GET Users API', () => { expect(body).toHaveProperty('firstName'); expect(typeof body.firstName).toBe('string'); expect(body.firstName.length).toBeGreaterThan(0); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Invalid user ID returns 404', { @@ -116,9 +216,29 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Invalid user ID returns 404' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/999999`); expect(response.status()).toBe(404); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('default users (no query) returns data object/array', { @@ -132,6 +252,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Default users response structure' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); @@ -139,6 +260,25 @@ test.describe('GET Users API', () => { expect(body).toBeInstanceOf(Object); // Should have either 'users' array or be an array itself expect(body.users || Array.isArray(body)).toBeTruthy(); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('limit param returns limited results', { @@ -152,6 +292,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Limit param pagination' } ] }, async ({ request }) => { + const start = Date.now(); const limit = 5; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}?limit=${limit}`); @@ -160,6 +301,25 @@ test.describe('GET Users API', () => { const users = body.users || body; const usersArray = Array.isArray(users) ? users : []; expect(usersArray.length).toBeLessThanOrEqual(limit); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('skip param shifts results', { @@ -173,6 +333,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Skip param pagination' } ] }, async ({ request }) => { + const start = Date.now(); const skip = 5; const limit = 10; @@ -195,6 +356,25 @@ test.describe('GET Users API', () => { if (firstUser1 && firstUser2) { expect(firstUser1.id).not.toBe(firstUser2.id); } + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 2, + unit: 'count', + }), + }); }); test('sorting / search query (if supported) returns filtered results', { @@ -208,6 +388,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Search query filtered results' } ] }, async ({ request }) => { + const start = Date.now(); // Try search query parameter (common patterns: q, search, query) const searchTerm = 'john'; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/search?q=${searchTerm}`); @@ -221,6 +402,25 @@ test.describe('GET Users API', () => { // At least verify the response structure is valid expect(Array.isArray(usersArray)).toBe(true); } + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('delayed response (3s) should return 200', { @@ -234,6 +434,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Delayed response returns 200' } ] }, async ({ request }) => { + const start = Date.now(); const isRetry = test.info().retry > 0; if (!isRetry) { expect(true).toBe(false); @@ -254,6 +455,25 @@ test.describe('GET Users API', () => { const body = await response.json(); expect(body).toBeInstanceOf(Object); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 10000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('enforce timeout (expect to fail if too slow) — set short timeout', { @@ -267,6 +487,7 @@ test.describe('GET Users API', () => { { type: 'testdino:context', description: 'Enforce request timeout' } ] }, async ({ request }) => { + const start = Date.now(); const delay = 5; const shortTimeout = 2000; @@ -280,5 +501,24 @@ test.describe('GET Users API', () => { } catch (error) { expect(error.message).toMatch(/timeout|Timeout/i); } + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); }); diff --git a/tests/post-api.spec.js b/tests/post-api.spec.js index 27ac322..a0518b3 100644 --- a/tests/post-api.spec.js +++ b/tests/post-api.spec.js @@ -19,6 +19,7 @@ test.describe('POST Create User API', () => { { type: 'testdino:context', description: 'POST bad endpoint returns 404' } ] }, async ({ request }) => { + const start = Date.now(); const userData = { firstName: 'Test', lastName: 'User' @@ -29,6 +30,25 @@ test.describe('POST Create User API', () => { }); expect(response.status()).toBe(404); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Invalid JSON payload handling', { @@ -42,6 +62,7 @@ test.describe('POST Create User API', () => { { type: 'testdino:context', description: 'Invalid JSON payload handling' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.post(`${API_BASE_URL}${ADD_ENDPOINT}`, { data: 'invalid json string', headers: { @@ -51,6 +72,25 @@ test.describe('POST Create User API', () => { // Should return 400 Bad Request for invalid JSON expect([400, 422]).toContain(response.status()); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Too large ID param should return 404', { @@ -64,10 +104,30 @@ test.describe('POST Create User API', () => { { type: 'testdino:context', description: 'Too large ID param returns 404' } ] }, async ({ request }) => { + const start = Date.now(); const tooLargeId = 999999999; const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/${tooLargeId}`); expect(response.status()).toBe(404); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Deleting invalid id returns 200/response but not crash', { @@ -81,12 +141,32 @@ test.describe('POST Create User API', () => { { type: 'testdino:context', description: 'Delete invalid id response handling' } ] }, async ({ request }) => { + const start = Date.now(); const invalidId = 999999; const response = await request.delete(`${API_BASE_URL}${USERS_ENDPOINT}/${invalidId}`); expect([200, 404]).toContain(response.status()); const body = await response.json(); expect(body).toBeInstanceOf(Object); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('PUT: Invalid method usage returns appropriate response (no 500)', { @@ -100,6 +180,7 @@ test.describe('POST Create User API', () => { { type: 'testdino:context', description: 'Invalid PUT method usage response' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 1; const updateData = { firstName: 'Updated' @@ -113,6 +194,25 @@ test.describe('POST Create User API', () => { // Should return appropriate error (400, 404, 405) but not 500 expect([400, 404, 405, 200]).toContain(response.status()); expect(response.status()).not.toBe(500); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('user schema contains expected keys', { @@ -126,6 +226,7 @@ test.describe('POST Create User API', () => { { type: 'testdino:context', description: 'User schema expected keys validation' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}/1`); expect(response.status()).toBe(200); @@ -136,6 +237,25 @@ test.describe('POST Create User API', () => { expectedKeys.forEach(key => { expect(body).toHaveProperty(key); }); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('users list contains objects with id and email', { @@ -149,6 +269,7 @@ test.describe('POST Create User API', () => { { type: 'testdino:context', description: 'Users list id and email validation' } ] }, async ({ request }) => { + const start = Date.now(); const response = await request.get(`${API_BASE_URL}${USERS_ENDPOINT}`); expect(response.status()).toBe(200); @@ -164,5 +285,24 @@ test.describe('POST Create User API', () => { expect(typeof firstUser.email).toBe('string'); } } + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); }); diff --git a/tests/updateUser.spec.js b/tests/updateUser.spec.js index 5a86e2a..52ace44 100644 --- a/tests/updateUser.spec.js +++ b/tests/updateUser.spec.js @@ -19,6 +19,7 @@ test.describe('PUT / PATCH Update User API', () => { { type: 'testdino:context', description: 'PUT/PATCH update user API' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 1; const updateData = { firstName: 'John', @@ -35,6 +36,25 @@ test.describe('PUT / PATCH Update User API', () => { expect(body).toHaveProperty('id', userId); expect(body).toHaveProperty('firstName', updateData.firstName); expect(body).toHaveProperty('lastName', updateData.lastName); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Update user with empty payload', { @@ -48,6 +68,7 @@ test.describe('PUT / PATCH Update User API', () => { { type: 'testdino:context', description: 'Update user with empty payload' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 2; const response = await request.put(`${API_BASE_URL}${USERS_ENDPOINT}/${userId}`, { data: {} @@ -57,6 +78,25 @@ test.describe('PUT / PATCH Update User API', () => { const body = await response.json(); expect(body).toBeInstanceOf(Object); expect(body).toHaveProperty('id', userId); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Update only one field', { @@ -70,6 +110,7 @@ test.describe('PUT / PATCH Update User API', () => { { type: 'testdino:context', description: 'PATCH partial user update' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 3; const updateData = { firstName: 'UpdatedFirstName' @@ -84,6 +125,25 @@ test.describe('PUT / PATCH Update User API', () => { const body = await response.json(); expect(body).toHaveProperty('id', userId); expect(body).toHaveProperty('firstName', updateData.firstName); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Validate returned name field', { @@ -97,6 +157,7 @@ test.describe('PUT / PATCH Update User API', () => { { type: 'testdino:context', description: 'Validate returned name field from update' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 4; const updateData = { firstName: 'Jane', @@ -115,6 +176,25 @@ test.describe('PUT / PATCH Update User API', () => { expect(typeof body.lastName).toBe('string'); expect(body.firstName).toBe(updateData.firstName); expect(body.lastName).toBe(updateData.lastName); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Update and validate response contains updatedAt simulation', { @@ -128,6 +208,7 @@ test.describe('PUT / PATCH Update User API', () => { { type: 'testdino:context', description: 'Validate updatedAt in update response' } ] }, async ({ request }) => { + const start = Date.now(); const userId = 5; const updateData = { firstName: 'Updated', @@ -149,6 +230,25 @@ test.describe('PUT / PATCH Update User API', () => { // At minimum, validate the response structure expect(body).toBeInstanceOf(Object); expect(body).toHaveProperty('id', userId); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Login failure (invalid creds)', { @@ -162,6 +262,7 @@ test.describe('PUT / PATCH Update User API', () => { { type: 'testdino:context', description: 'Auth login failure with invalid credentials' } ] }, async ({ request }) => { + const start = Date.now(); const loginData = { username: 'invaliduser', password: 'wrongpassword' @@ -175,6 +276,25 @@ test.describe('PUT / PATCH Update User API', () => { expect([400, 401, 403]).toContain(response.status()); const body = await response.json(); expect(body).toBeInstanceOf(Object); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); test('Login missing fields returns 400', { @@ -188,6 +308,7 @@ test.describe('PUT / PATCH Update User API', () => { { type: 'testdino:context', description: 'Login missing fields returns 400' } ] }, async ({ request }) => { + const start = Date.now(); const loginData = { username: 'kminchelle' }; @@ -199,5 +320,24 @@ test.describe('PUT / PATCH Update User API', () => { expect(response.status()).toBe(400); const body = await response.json(); expect(body).toBeInstanceOf(Object); + + const responseTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-response-time', + value: responseTime, + unit: 'ms', + threshold: 5000, + }), + }); + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'api-calls', + value: 1, + unit: 'count', + }), + }); }); }); diff --git a/tests/visual.spec.js b/tests/visual.spec.js index b0a2172..d884647 100644 --- a/tests/visual.spec.js +++ b/tests/visual.spec.js @@ -22,12 +22,24 @@ test.describe('Visual Comparison', () => { { type: 'testdino:context', description: 'Visual comparison demo for GitHub login on Chromium' } ] }, async ({ page }) => { - await page.goto('https://github.com/login'); + const start = Date.now(); + await page.goto('https://github.com/login'); await expect(page).toHaveScreenshot('github-login.png'); await page.getByRole('textbox', { name: 'Username or email address' }).click(); await page.getByRole('textbox', { name: 'Username or email address' }).fill('test'); await expect(page).toHaveScreenshot('github-login-changed.png'); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'visual-flow-time', + value: flowTime, + unit: 'ms', + threshold: 10000, + }), + }); }); }); }); From 2fa0b71b2e2c3eb7afaae996f8a485e22022e8d0 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:15:40 +0530 Subject: [PATCH 52/67] Updated test cases by adding tags/annotations feature --- tests/example.spec.js | 1242 ++++++++++++++++++++++------------------- 1 file changed, 671 insertions(+), 571 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index c2a66c6..8962050 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -1,8 +1,6 @@ // @ts-check import { expect, test } from '@playwright/test'; import AllPages from '../pages/AllPages.js'; -import dotenv from 'dotenv'; -dotenv.config({ override: true }); let allPages; @@ -11,590 +9,692 @@ test.beforeEach(async ({ page }) => { await page.goto('/'); }); -async function login(username = process.env.USERNAME, password = process.env.PASSWORD) { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(username, password); -} +/* ---------- Helpers ---------- */ -async function login1(username = process.env.USERNAME1, password = process.env.PASSWORD) { +async function login( + username = process.env.USERNAME, + password = process.env.PASSWORD +) { await allPages.loginPage.clickOnUserProfileIcon(); await allPages.loginPage.validateSignInPage(); - await allPages.loginPage.login(username, password); + // await allPages.loginPage.login(username, password); } -async function logout() { - await allPages.loginPage.clickOnUserProfileIcon(); - await allPages.loginPage.clickOnLogoutButton(); -} - -test('Verify that user can login and logout successfully', { tag: '@android' }, async () => { - await login(); - await logout(); -}); - -test('Verify that user can update personal information', { tag: '@webkit' }, async () => { - // await login(); - await allPages.userPage.clickOnUserProfileIcon(); - // await allPages.userPage.updatePersonalInfo(); - // await allPages.userPage.verifyPersonalInfoUpdated(); -}); - -test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', { tag: '@chromium' }, async () => { - // await login(); - - await test.step('Verify that user is able to add address successfully', async () => { - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnAddressTab(); - await allPages.userPage.clickOnAddAddressButton(); - await allPages.userPage.fillAddressForm(); - await allPages.userPage.verifytheAddressIsAdded(); - }); - - await test.step('Verify that user is able to edit address successfully', async () => { - await allPages.userPage.clickOnEditAddressButton(); - await allPages.userPage.updateAddressForm(); - await allPages.userPage.verifytheUpdatedAddressIsAdded(); - }) - - await test.step('Verify that user is able to delete address successfully', async () => { - await allPages.userPage.clickOnDeleteAddressButton(); - }); -}); - -test('Verify that user can change password successfully', { tag: '@firefox' }, async () => { - await test.step('Login with existing password', async () => { - // await login1(); - }); - - await test.step('Change password and verify login with new password', async () => { - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnSecurityButton(); - await allPages.userPage.enterNewPassword(); - await allPages.userPage.enterConfirmNewPassword(); - await allPages.userPage.clickOnUpdatePasswordButton(); - await allPages.userPage.getUpdatePasswordNotification(); - }); - await test.step('Verify login with new password and revert back to original password', async () => { - // Re-login with new password - await logout(); - await allPages.loginPage.login(process.env.USERNAME1, process.env.NEW_PASSWORD); - - // Revert back - await allPages.userPage.clickOnUserProfileIcon(); - await allPages.userPage.clickOnSecurityButton(); - await allPages.userPage.revertPasswordBackToOriginal(); - await allPages.userPage.getUpdatePasswordNotification(); - }) +/* ---------- FLAKY TESTS (fail on 1st run + 1st retry, pass on 2nd retry) ---------- */ + +test.describe('Flaky tests (pass on 2nd retry)', () => { + test.describe.configure({ retries: 2 }); + + test( + 'Verify that user can login and logout successfully', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Login' }, + { type: 'testdino:link', description: 'https://jira.example.com/LOGIN-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: login and logout on Chromium' } + ] + }, + async ({}, testInfo) => { + const start = Date.now(); + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } + ); + + test( + 'User searches products and views result (Searchbox)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Search' }, + { type: 'testdino:link', description: 'https://jira.example.com/SEARCH-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: search products on Firefox' } + ] + }, + async ({}, testInfo) => { + const start = Date.now(); + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } + ); + + test( + 'User navigates through product categories (Product page)', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Products' }, + { type: 'testdino:link', description: 'https://jira.example.com/PRODUCTS-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Flaky test: product categories on WebKit' } + ] + }, + async ({}, testInfo) => { + const start = Date.now(); + await login(); + if (testInfo.retry < 2) { + throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); + } + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } + ); }); -test('Verify that User Can Complete the Journey from Login to Order Placement', { tag: '@webkit' }, async () => { - const productName = 'GoPro HERO10 Black'; - // await login(); - await allPages.inventoryPage.clickOnShopNowButton(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.verifyCartItemVisible(productName); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.verifyProductInCheckout(productName); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -}); - -test('Verify user can place and cancel an order', { tag: '@webkit' }, async () => { - const productName = 'GoPro HERO10 Black'; - const productPriceAndQuantity = '₹49,999 × 1'; - const productQuantity = '1'; - const orderStatusProcessing = 'Processing'; - const orderStatusCanceled = 'Canceled'; - - await test.step('Verify that user can login successfully', async () => { - // await login(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - }) - - // await test.step('Add product to cart and checkout', async () => { - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.verifyCartItemVisible(productName); - // await allPages.cartPage.clickOnCheckoutButton(); - // }) - - // await test.step('Place order and click on continue shopping', async () => { - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.verifyProductInCheckout(productName); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // await allPages.checkoutPage.verifyOrderItemName(productName); - // await allPages.inventoryPage.clickOnContinueShopping(); - // }) - - // await test.step('Verify order in My Orders', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.verifyMyOrdersTitle(); - // await allPages.orderPage.clickOnPaginationButton(2); - // await allPages.orderPage.verifyProductInOrderList(productName); - // await allPages.orderPage.verifyPriceAndQuantityInOrderList(productPriceAndQuantity); - // await allPages.orderPage.verifyOrderStatusInList(orderStatusProcessing, productName); - // await allPages.orderPage.clickOnPaginationButton(1); - // await allPages.orderPage.clickViewDetailsButton(1); - // await allPages.orderPage.verifyOrderDetailsTitle(); - // await allPages.orderPage.verifyOrderSummary(productName, productQuantity, '₹49,999', orderStatusProcessing); - // }) - - // await test.step('Cancel order and verify status is updated to Canceled', async () => { - // await allPages.orderPage.clickCancelOrderButton(2); - // await allPages.orderPage.confirmCancellation(); - // await allPages.orderPage.verifyCancellationConfirmationMessage(); - // await allPages.orderPage.verifyMyOrdersCount(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.verifyMyOrdersTitle(); - // await allPages.orderPage.clickOnPaginationButton(2); - // await allPages.orderPage.verifyOrderStatusInList(orderStatusCanceled, productName); - // }) -}); - -test('Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement', { tag: '@firefox' }, async () => { - // fresh test data - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - let productName; - let productPrice; - let productReviewCount; - - // await test.step('Verify that user can register successfully', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - // }) - - // await test.step('Verify that user can login successfully', async () => { - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(email, process.env.PASSWORD); - // await allPages.loginPage.verifySuccessSignIn(); - // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - // }) - - await test.step('Navigate to all product and add to wishlist section', async () => { - await allPages.homePage.clickAllProductsNav(); - await allPages.allProductsPage.assertAllProductsTitle(); - - productName = await allPages.allProductsPage.getNthProductName(1); - productPrice = await allPages.allProductsPage.getNthProductPrice(1); - productReviewCount = await allPages.allProductsPage.getNthProductReviewCount(1); - - await allPages.allProductsPage.clickNthProductWishlistIcon(1); - await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); - await allPages.allProductsPage.clickNthProduct(1); - - await allPages.productDetailsPage.assertProductNameTitle(productName); - await allPages.productDetailsPage.assertProductPrice(productName, productPrice); - await allPages.productDetailsPage.assertProductReviewCount(productName, productReviewCount); - await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); - }) - await test.step('Add product to cart, add new address and checkout', async () => { +/* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ + +test( + 'Verify that all the navbar are working properly', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar functionality on WebKit' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can edit and delete a product review', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete product review on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that User Can Complete the Journey from Login to Order Placement', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order placement journey on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can filter products by price range', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter products by price on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify if user can add product to wishlist, move to cart and checkout', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart and checkout on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user is able to submit a product review', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review on WebKit' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that all the navbar are working properly (Navbar)', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar (Navbar) on WebKit' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that user can edit and delete a product review (Single review)', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete review (Single review) on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that User Can Complete the Journey from Login to Order Placement (Single order)', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order (Single order) on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test.only( + 'Verify that user can filter products by price range (Price page', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter by price (Price page) on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test.only( + 'Verify if user can add product to wishlist, move to cart(Checkout page)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart (Checkout page) on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test.only( + 'Verify that user is able to submit a product review (Review)', + { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review (Review) on WebKit' } + ] + }, + async () => { + const start = Date.now(); + await login(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test.only( + 'Verify that user can update cart quantity and verify total price', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Update cart quantity and total price on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); // await allPages.productDetailsPage.clickAddToCartButton(); - - // await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.assertYourCartTitle(); - // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - // await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); - // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); - // await allPages.cartPage.clickIncreaseQuantityButton(); - // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); - - // const cleanPrice = productPrice.replace(/[₹,]/g, ''); - // const priceValue = parseFloat(cleanPrice) * 2; - // await expect(allPages.cartPage.getTotalValue()).toContainText( - // priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') - // ); - // await allPages.cartPage.clickOnCheckoutButton(); - - // // Fill shipping address and save - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.fillShippingAddress( - // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - // ); - // await allPages.checkoutPage.clickSaveAddressButton(); - // await allPages.checkoutPage.assertAddressAddedToast(); - - // // COD, verify summary, place order - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.assertOrderSummaryTitle(); - // await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); - // await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); - // await allPages.checkoutPage.verifyProductInCheckout(productName); - // await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); - // await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); - - // const subtotalValue = parseFloat(cleanPrice) * 2; - // const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - // await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); - // await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); - // await allPages.checkoutPage.clickOnPlaceOrder(); - - // // Order details and return to home - // await allPages.orderDetailsPage.assertOrderDetailsTitle(); - // await allPages.orderDetailsPage.assertOrderPlacedName(); - // await allPages.orderDetailsPage.assertOrderPlacedMessage(); - // await allPages.orderDetailsPage.assertOrderPlacedDate(); - // await allPages.orderDetailsPage.assertOrderInformationTitle(); - // await allPages.orderDetailsPage.assertOrderConfirmedTitle(); - // await allPages.orderDetailsPage.assertOrderConfirmedMessage(); - // await allPages.orderDetailsPage.assertShippingDetailsTitle(); - // await allPages.orderDetailsPage.assertShippingEmailValue(email); - // await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); - // await allPages.orderDetailsPage.assertDeliveryAddressLabel(); - // await allPages.orderDetailsPage.assertDeliveryAddressValue(); - // await allPages.orderDetailsPage.assertContinueShoppingButton(); - - // await allPages.orderDetailsPage.assertOrderSummaryTitle(); - // await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); - // await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); - // await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); - // await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); - // await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); - // await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); - // await allPages.orderDetailsPage.clickBackToHomeButton(); - }); -}); - -test('Verify that user add product to cart before logging in and then complete order after logging in', { tag: '@chromium' }, async () => { - await test.step('Navigate and add product to cart before logging in', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.homePage.clickProductImage(); - await allPages.homePage.clickAddToCartButton(); - await allPages.homePage.validateAddCartNotification(); - // await allPages.loginPage.clickOnUserProfileIcon(); - }) - await test.step('Login and complete order', async () => { - // await login(); // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -}) -}); - -test('Verify that user can filter products by price range', { tag: '@chromium' }, async () => { - // await login(); - await allPages.homePage.clickOnShopNowButton(); - await allPages.homePage.clickOnFilterButton(); - await allPages.homePage.AdjustPriceRangeSlider('10000', '20000'); - await allPages.homePage.clickOnFilterButton(); -}); - -test('Verify if user can add product to wishlist, moves it to card and then checks out', { tag: '@webkit' }, async () => { - // await login(); - - await test.step('Add product to wishlistand then add to cart', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.inventoryPage.addToWishlist(); - await allPages.inventoryPage.assertWishlistIcon(); - await allPages.inventoryPage.clickOnWishlistIconHeader(); - await allPages.inventoryPage.assertWishlistPage(); - await allPages.inventoryPage.clickOnWishlistAddToCard(); - }) - - await test.step('Checkout product added to cart', async () => { - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) - -}); - -test('Verify new user views and cancels an order in my orders', { tag: '@webkit' }, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - let productName= `Rode NT1-A Condenser Mic`; - - // await test.step('Verify that user can register successfully', async () => { - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - // }) - - // await test.step('Verify that user can login successfully', async () => { - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(email, process.env.PASSWORD); - // await allPages.loginPage.verifySuccessSignIn(); - // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - // }) - - await test.step('Navigate to All Products and add view details of a random product', async () => { - await allPages.homePage.clickAllProductsNav(); - await allPages.allProductsPage.assertAllProductsTitle(); - productName = await allPages.allProductsPage.getNthProductName(1); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickAddToCartButton(); - }) - - // await test.step('Add product to cart, add new address and checkout', async () => { - // await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.assertYourCartTitle(); - // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.fillShippingAddress( - // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' - // ); - // await allPages.checkoutPage.clickSaveAddressButton(); - // await allPages.checkoutPage.assertAddressAddedToast(); - // }) - - // await test.step('Complete order and verify in my orders', async () => { - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // await allPages.inventoryPage.clickOnContinueShopping(); - - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.clickCancelOrderButton(); - // await allPages.orderPage.confirmCancellation(); - // }); -}); - -test('Verify That a New User Can Successfully Complete the Journey from Registration to a Multiple Order Placement', { tag: '@webkit' }, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - - let productName= `Rode NT1-A Condenser Mic`; - - // await test.step('Verify that user can register successfully', async () => { - // // Signup - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.clickOnSignupLink(); - // await allPages.signupPage.assertSignupPage(); - // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); - // await allPages.signupPage.verifySuccessSignUp(); - // }) - - // await test.step('Verify that user can login successfully', async () => { - // // Login as new user - // await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(email, process.env.PASSWORD); - // await allPages.loginPage.verifySuccessSignIn(); - // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); - // }) - - await test.step('Navigate to All Products and add view details of a random product', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); - await allPages.productDetailsPage.clickOnAdditionalInfoTab(); - await allPages.productDetailsPage.assertAdditionalInfoTab(); - }) - - await test.step('Add product to cart, change quantity, add new address and checkout', async () => { - // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.productDetailsPage.clickCartIcon(); // await allPages.cartPage.clickIncreaseQuantityButton(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); - // await allPages.checkoutPage.clickSaveAddressButton(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - // await allPages.checkoutPage.verifyOrderConfirmedTitle(); - // await allPages.checkoutPage.clickOnContinueShoppingButton(); - }) - - await test.step('Add another product to cart, select existing address and checkout', async () => { + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test.only( + 'Verify that user can view order history and order detail (Order page)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history and order detail (Order page) on Firefox' } + ] + }, + async () => { + const start = Date.now(); + await login(); + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test.only( + 'Verify that user can update cart quantity and verify total price (Pricing)', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Cart quantity and total price (Pricing) on Chromium' } + ] + }, + async () => { + const start = Date.now(); + await login(); // await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.assertAllProductsTitle(); // await allPages.allProductsPage.clickNthProduct(1); // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.productDetailsPage.clickCartIcon(); - // await allPages.cartPage.clickOnCheckoutButton(); - // await allPages.checkoutPage.verifyCheckoutTitle(); - // await allPages.checkoutPage.selectCashOnDelivery(); - // await allPages.checkoutPage.verifyCashOnDeliverySelected(); - // await allPages.checkoutPage.clickOnPlaceOrder(); - // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); - }) -}); - -test('Verify that the new user is able to Sign Up, Log In, and Navigate to the Home Page Successfully', { tag: '@ios' }, async () => { - const email = `test+${Date.now()}@test.com`; - const firstName = 'Test'; - const lastName = 'User'; - -// await test.step('Verify that user can register successfully', async () => { -// await allPages.loginPage.clickOnUserProfileIcon(); -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.clickOnSignupLink(); -// await allPages.signupPage.assertSignupPage(); -// await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); -// await allPages.signupPage.verifySuccessSignUp(); -// }) - -// await test.step('Verify that user can login successfully', async () => { -// await allPages.loginPage.validateSignInPage(); -// await allPages.loginPage.login(email, process.env.PASSWORD); -// await allPages.loginPage.verifySuccessSignIn(); -// await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); -// }) -}) - -test('Verify that user is able to fill Contact Us page successfully', { tag: '@firefox' }, async () => { - // await login(); - await allPages.homePage.clickOnContactUsLink(); - await allPages.contactUsPage.assertContactUsTitle(); - await allPages.contactUsPage.fillContactUsForm(); - await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); -}); - -test('Verify that user is able to submit a product review', { tag: '@android' }, async () => { - await test.step('Login as existing user and navigate to a product', async () => { - // await login(); - }) - - await test.step('Navigate to all product section and select a product', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - }) - - await test.step('Submit a product review and verify submission', async () => { - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); - - await allPages.productDetailsPage.clickOnWriteAReviewBtn(); - await allPages.productDetailsPage.fillReviewForm(); - await allPages.productDetailsPage.assertSubmittedReview({ - name: 'John Doe', - title: 'Great Product', - opinion: 'This product exceeded my expectations. Highly recommend!' - }); - }) -}); - -test('Verify that user can edit and delete a product review', { tag: '@chromium' }, async () => { - await test.step('Login as existing user and navigate to a product', async () => { - // await login(); - }) - - await test.step('Navigate to all product section and select a product', async () => { - await allPages.homePage.clickOnShopNowButton(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.allProductsPage.clickNthProduct(1); - }) - - await test.step('Submit a product review and verify submission', async () => { - await allPages.productDetailsPage.clickOnReviewsTab(); - await allPages.productDetailsPage.assertReviewsTab(); - - await allPages.productDetailsPage.clickOnWriteAReviewBtn(); - await allPages.productDetailsPage.fillReviewForm(); - await allPages.productDetailsPage.assertSubmittedReview({ - name: 'John Doe', - title: 'Great Product', - opinion: 'This product exceeded my expectations. Highly recommend!' - }); - }) - - await test.step('Edit the submitted review and verify changes', async () => { - await allPages.productDetailsPage.clickOnEditReviewBtn(); - await allPages.productDetailsPage.updateReviewForm(); - await allPages.productDetailsPage.assertUpdatedReview({ - title: 'Updated Review Title', - opinion: 'This is an updated review opinion.' - }) + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), }); - - await test.step('Delete the submitted review and verify deletion', async () => { - await allPages.productDetailsPage.clickOnDeleteReviewBtn(); - }) -}); - -test('Verify that user can purchase multiple quantities in a single order', { tag: '@ios' }, async () => { - const productName = 'GoPro HERO10 Black'; + } +); + +test.only( + 'Verify that user can view order history and order details properly (Order details)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-004' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history and details (Order details) on Firefox' } + ] + }, + async () => { + const start = Date.now(); await login(); - await allPages.inventoryPage.clickOnShopNowButton(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickIncreaseQuantityButton(); - await allPages.cartPage.verifyIncreasedQuantity('3'); - await allPages.cartPage.clickOnCheckoutButton(); - await allPages.checkoutPage.verifyCheckoutTitle(); - await allPages.checkoutPage.verifyProductInCheckout(productName); - await allPages.checkoutPage.selectCashOnDelivery(); - await allPages.checkoutPage.verifyCashOnDeliverySelected(); - await allPages.checkoutPage.clickOnPlaceOrder(); - await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); -}); - -test('Verify that all the navbar are working properly', { tag: '@ios' }, async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that users can update cart quantity and verify total price (Single order)', + { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Cart quantity (Single order) on Chromium' } + ] + }, + async () => { + const start = Date.now(); await login(); - await allPages.homePage.clickBackToHomeButton(); - // await allPages.homePage.assertHomePage(); - await allPages.homePage.clickAllProductsNav(); - await allPages.allProductsPage.assertAllProductsTitle(); - await allPages.homePage.clickOnContactUsLink(); - await allPages.contactUsPage.assertContactUsTitle(); - await allPages.homePage.clickAboutUsNav(); - await allPages.homePage.assertAboutUsTitle(); -}); - -test('Verify that user is able to delete selected product from cart', { tag: '@android' }, async () => { - const productName = 'GoPro HERO10 Black'; + // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.clickNthProduct(1); + // await allPages.productDetailsPage.clickAddToCartButton(); + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await allPages.cartPage.verifyTotalPriceUpdated(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); + +test( + 'Verify that users can view order history and order details properly (Order history)', + { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-005' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Order history on Firefox' } + ] + }, + async () => { + const start = Date.now(); await login(); - await allPages.inventoryPage.clickOnShopNowButton(); - await allPages.inventoryPage.clickOnAllProductsLink(); - await allPages.inventoryPage.searchProduct(productName); - await allPages.inventoryPage.verifyProductTitleVisible(productName); - await allPages.inventoryPage.clickOnAddToCartIcon(); - - await allPages.cartPage.clickOnCartIcon(); - await allPages.cartPage.verifyCartItemVisible(productName); - await allPages.cartPage.clickOnDeleteProductIcon(); - await allPages.cartPage.verifyCartItemDeleted(productName); - await allPages.cartPage.verifyEmptyCartMessage(); - await allPages.cartPage.clickOnStartShoppingButton(); - await allPages.allProductsPage.assertAllProductsTitle(); -}); \ No newline at end of file + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.verifyOrdersListVisible(); + // await allPages.orderPage.clickOnFirstOrder(); + // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); + await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + } +); From 6b38fd2ab22bc690cd1f251eb4afa2d3eb361c7c Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:20:35 +0530 Subject: [PATCH 53/67] Updated test cases by adding tags/annotations feature --- tests/example.spec.js | 1301 ++++++++++++++++++++++------------------- 1 file changed, 689 insertions(+), 612 deletions(-) diff --git a/tests/example.spec.js b/tests/example.spec.js index 8962050..96da862 100644 --- a/tests/example.spec.js +++ b/tests/example.spec.js @@ -1,6 +1,8 @@ // @ts-check import { expect, test } from '@playwright/test'; import AllPages from '../pages/AllPages.js'; +import dotenv from 'dotenv'; +dotenv.config({ override: true }); let allPages; @@ -9,172 +11,70 @@ test.beforeEach(async ({ page }) => { await page.goto('/'); }); -/* ---------- Helpers ---------- */ - -async function login( - username = process.env.USERNAME, - password = process.env.PASSWORD -) { +async function login(username = process.env.USERNAME, password = process.env.PASSWORD) { await allPages.loginPage.clickOnUserProfileIcon(); await allPages.loginPage.validateSignInPage(); - // await allPages.loginPage.login(username, password); + await allPages.loginPage.login(username, password); } -/* ---------- FLAKY TESTS (fail on 1st run + 1st retry, pass on 2nd retry) ---------- */ +async function login1(username = process.env.USERNAME1, password = process.env.PASSWORD) { + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.loginPage.validateSignInPage(); + await allPages.loginPage.login(username, password); +} -test.describe('Flaky tests (pass on 2nd retry)', () => { - test.describe.configure({ retries: 2 }); +async function logout() { + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.loginPage.clickOnLogoutButton(); +} - test( - 'Verify that user can login and logout successfully', - { - tag: '@chromium', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Login' }, - { type: 'testdino:link', description: 'https://jira.example.com/LOGIN-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Flaky test: login and logout on Chromium' } - ] - }, - async ({}, testInfo) => { - const start = Date.now(); - await login(); - if (testInfo.retry < 2) { - throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); - } - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } - ); - - test( - 'User searches products and views result (Searchbox)', - { - tag: '@firefox', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Search' }, - { type: 'testdino:link', description: 'https://jira.example.com/SEARCH-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Flaky test: search products on Firefox' } - ] - }, - async ({}, testInfo) => { - const start = Date.now(); - await login(); - if (testInfo.retry < 2) { - throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); - } - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } - ); - - test( - 'User navigates through product categories (Product page)', - { - tag: '@webkit', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Products' }, - { type: 'testdino:link', description: 'https://jira.example.com/PRODUCTS-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Flaky test: product categories on WebKit' } - ] - }, - async ({}, testInfo) => { - const start = Date.now(); - await login(); - if (testInfo.retry < 2) { - throw new Error(`Flaky: failing on attempt ${testInfo.retry + 1}, will pass on 2nd retry`); - } - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } - ); +test('Verify that user can log in and log out successfully', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Login' }, + { type: 'testdino:link', description: 'https://jira.example.com/LOGIN-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login and logout on Android' } + ] +}, async () => { + const start = Date.now(); + await login(); + await logout(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); }); - -/* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ - -test( - 'Verify that all the navbar are working properly', - { - tag: '@webkit', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Navbar' }, - { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Navbar functionality on WebKit' } - ] - }, - async () => { - const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test( - 'Verify that user can edit and delete a product review', - { - tag: '@chromium', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Review' }, - { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Edit and delete product review on Chromium' } - ] - }, - async () => { +test('Verify that all navbar links work properly', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Navbar' }, + { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Navbar functionality on WebKit' } + ] +}, async () => { const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); + // await login(); + await allPages.homePage.clickBackToHomeButton(); + // await allPages.homePage.assertHomePage(); + await allPages.homePage.clickAllProductsNav(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.homePage.clickOnContactUsLink(); + await allPages.contactUsPage.assertContactUsTitle(); + await allPages.homePage.clickAboutUsNav(); + await allPages.homePage.assertAboutUsTitle(); const flowTime = Date.now() - start; test.info().annotations.push({ type: 'testdino:metric', @@ -185,205 +85,301 @@ test( threshold: 5000, }), }); - } -); - -test( - 'Verify that User Can Complete the Journey from Login to Order Placement', - { - tag: '@chromium', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Order' }, - { type: 'testdino:link', description: 'https://jira.example.com/ORDER-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Login to order placement journey on Chromium' } - ] - }, - async () => { - const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test( - 'Verify that user can filter products by price range', - { - tag: '@firefox', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Filter' }, - { type: 'testdino:link', description: 'https://jira.example.com/FILTER-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Filter products by price on Firefox' } - ] - }, - async () => { - const start = Date.now(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test( - 'Verify if user can add product to wishlist, move to cart and checkout', - { - tag: '@firefox', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Wishlist' }, - { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Wishlist to cart and checkout on Firefox' } - ] - }, - async () => { - const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test( - 'Verify that user is able to submit a product review', - { - tag: '@webkit', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Review' }, - { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-002' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Submit product review on WebKit' } - ] - }, - async () => { - const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test( - 'Verify that all the navbar are working properly (Navbar)', - { - tag: '@webkit', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Navbar' }, - { type: 'testdino:link', description: 'https://jira.example.com/NAVBAR-002' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Navbar (Navbar) on WebKit' } - ] - }, - async () => { - const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test( - 'Verify that user can edit and delete a product review (Single review)', - { - tag: '@chromium', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Review' }, - { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-003' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Edit and delete review (Single review) on Chromium' } - ] - }, - async () => { - const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), +}); + +test('Verify that user can edit and delete a product review', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Edit and delete product review on Firefox' } + ] +}, async () => { + const start = Date.now(); + await test.step('Login as existing user and navigate to a product', async () => { + // await login(); + }) + + await test.step('Navigate to all product section and select a product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + }) + + await test.step('Submit a product review and verify submission', async () => { + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + + await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + await allPages.productDetailsPage.fillReviewForm(); + await allPages.productDetailsPage.assertSubmittedReview({ + name: 'John Doe', + title: 'Great Product', + opinion: 'This product exceeded my expectations. Highly recommend!' + }); + }) + + await test.step('Edit the submitted review and verify changes', async () => { + await allPages.productDetailsPage.clickOnEditReviewBtn(); + await allPages.productDetailsPage.updateReviewForm(); + await allPages.productDetailsPage.assertUpdatedReview({ + title: 'Updated Review Title', + opinion: 'This is an updated review opinion.' + }) }); - } -); - -test( - 'Verify that User Can Complete the Journey from Login to Order Placement (Single order)', - { - tag: '@chromium', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Order' }, - { type: 'testdino:link', description: 'https://jira.example.com/ORDER-002' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Login to order (Single order) on Chromium' } - ] - }, - async () => { + + await test.step('Delete the submitted review and verify deletion', async () => { + await allPages.productDetailsPage.clickOnDeleteReviewBtn(); + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); +}); + +test('Verify that user can complete the journey from login to order placement', { + tag: '@ios', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Login to order placement journey on iOS' } + ] +}, async () => { + const start = Date.now(); + const productName = 'GoPro HERO10 Black'; + // await login(); + await allPages.inventoryPage.clickOnShopNowButton(); + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); + + // await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.verifyCartItemVisible(productName); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.verifyProductInCheckout(productName); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); +}); + +test('Verify that a new user can complete the journey from registration to a single order placement', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Registration' }, + { type: 'testdino:link', description: 'https://jira.example.com/REG-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Registration to single order on Android' } + ] +}, async () => { + const start = Date.now(); + // fresh test data + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + let productName; + let productPrice; + let productReviewCount; + + await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + }) + + await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + }) + + await test.step('Navigate to all product and add to wishlist section', async () => { + await allPages.homePage.clickAllProductsNav(); + await allPages.allProductsPage.assertAllProductsTitle(); + + productName = await allPages.allProductsPage.getNthProductName(1); + productPrice = await allPages.allProductsPage.getNthProductPrice(1); + productReviewCount = await allPages.allProductsPage.getNthProductReviewCount(1); + + await allPages.allProductsPage.clickNthProductWishlistIcon(1); + await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); + await allPages.allProductsPage.clickNthProduct(1); + + await allPages.productDetailsPage.assertProductNameTitle(productName); + await allPages.productDetailsPage.assertProductPrice(productName, productPrice); + await allPages.productDetailsPage.assertProductReviewCount(productName, productReviewCount); + await expect(allPages.allProductsPage.getNthProductWishlistIconCount(1)).toContainText('1'); + }) + + await test.step('Add product to cart, add new address and checkout', async () => { + await allPages.productDetailsPage.clickAddToCartButton(); + + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.assertYourCartTitle(); + // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); + // await expect(allPages.cartPage.getCartItemPrice()).toContainText(productPrice); + // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('1'); + // await allPages.cartPage.clickIncreaseQuantityButton(); + // await expect(allPages.cartPage.getCartItemQuantity()).toContainText('2'); + + // const cleanPrice = productPrice.replace(/[₹,]/g, ''); + // const priceValue = parseFloat(cleanPrice) * 2; + // await expect(allPages.cartPage.getTotalValue()).toContainText( + // priceValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') + // ); + // await allPages.cartPage.clickOnCheckoutButton(); + + // // Fill shipping address and save + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.fillShippingAddress( + // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.assertAddressAddedToast(); + + // // COD, verify summary, place order + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.assertOrderSummaryTitle(); + // await expect(allPages.checkoutPage.getOrderSummaryImage()).toBeVisible(); + // await expect(allPages.checkoutPage.getOrderSummaryProductName()).toContainText(productName); + // await allPages.checkoutPage.verifyProductInCheckout(productName); + // await expect(allPages.checkoutPage.getOrderSummaryProductQuantity()).toContainText('2'); + // await expect(allPages.checkoutPage.getOrderSummaryProductPrice()).toContainText(productPrice); + + // const subtotalValue = parseFloat(cleanPrice) * 2; + // const formattedSubtotal = subtotalValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + // await expect(await allPages.checkoutPage.getOrderSummarySubtotalValue()).toContain(formattedSubtotal); + // await expect(allPages.checkoutPage.getOrderSummaryShippingValue()).toContainText('Free'); + // await allPages.checkoutPage.clickOnPlaceOrder(); + + // // Order details and return to home + // await allPages.orderDetailsPage.assertOrderDetailsTitle(); + // await allPages.orderDetailsPage.assertOrderPlacedName(); + // await allPages.orderDetailsPage.assertOrderPlacedMessage(); + // await allPages.orderDetailsPage.assertOrderPlacedDate(); + // await allPages.orderDetailsPage.assertOrderInformationTitle(); + // await allPages.orderDetailsPage.assertOrderConfirmedTitle(); + // await allPages.orderDetailsPage.assertOrderConfirmedMessage(); + // await allPages.orderDetailsPage.assertShippingDetailsTitle(); + // await allPages.orderDetailsPage.assertShippingEmailValue(email); + // await allPages.orderDetailsPage.assertPaymentMethodAmount(formattedSubtotal); + // await allPages.orderDetailsPage.assertDeliveryAddressLabel(); + // await allPages.orderDetailsPage.assertDeliveryAddressValue(); + // await allPages.orderDetailsPage.assertContinueShoppingButton(); + + // await allPages.orderDetailsPage.assertOrderSummaryTitle(); + // await allPages.orderDetailsPage.assertOrderSummaryProductName(productName); + // await allPages.orderDetailsPage.assertOrderSummaryProductQuantity('2'); + // await allPages.orderDetailsPage.assertOrderSummaryProductPrice(productPrice); + // await allPages.orderDetailsPage.assertOrderSummarySubtotalValue(formattedSubtotal); + // await allPages.orderDetailsPage.assertOrderSummaryShippingValue('Free'); + // await allPages.orderDetailsPage.assertOrderSummaryTotalValue(formattedSubtotal); + // await allPages.orderDetailsPage.clickBackToHomeButton(); + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); +}); + +test('Verify that user can add product to cart before logging in and complete order after logging in', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Add to cart before login, order after login on WebKit' } + ] +}, async () => { + const start = Date.now(); + await test.step('Navigate and add product to cart before logging in', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.homePage.clickProductImage(); + await allPages.homePage.clickAddToCartButton(); + await allPages.homePage.validateAddCartNotification(); + // await allPages.loginPage.clickOnUserProfileIcon(); + }) + // await test.step('Login and complete order', async () => { +// await login(); +// await allPages.cartPage.clickOnCartIcon(); +// await allPages.cartPage.clickOnCheckoutButton(); +// await allPages.checkoutPage.verifyCheckoutTitle(); +// await allPages.checkoutPage.selectCashOnDelivery(); +// await allPages.checkoutPage.verifyCashOnDeliverySelected(); +// await allPages.checkoutPage.clickOnPlaceOrder(); +// await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); +// }) + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); +}); + +test('Verify that user can filter products by price range', { + tag: '@filter', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Filter' }, + { type: 'testdino:link', description: 'https://jira.example.com/FILTER-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Filter products by price range' } + ] +}, async () => { const start = Date.now(); await login(); - await expect(true).toBeTruthy(); + await allPages.homePage.clickOnShopNowButton(); + await allPages.homePage.clickOnFilterButton(); + await allPages.homePage.AdjustPriceRangeSlider('10000', '20000'); + await allPages.homePage.clickOnFilterButton(); const flowTime = Date.now() - start; test.info().annotations.push({ type: 'testdino:metric', @@ -394,55 +390,41 @@ test( threshold: 5000, }), }); - } -); - -test.only( - 'Verify that user can filter products by price range (Price page', - { - tag: '@firefox', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Filter' }, - { type: 'testdino:link', description: 'https://jira.example.com/FILTER-002' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Filter by price (Price page) on Firefox' } - ] - }, - async () => { +}); + +test('Verify that user can add product to wishlist, move it to cart, and checkout', { + tag: '@wishlist', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Wishlist' }, + { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Wishlist to cart and checkout' } + ] +}, async () => { const start = Date.now(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), + // await login(); + + await test.step('Add product to wishlistand then add to cart', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.inventoryPage.addToWishlist(); + await allPages.inventoryPage.assertWishlistIcon(); + await allPages.inventoryPage.clickOnWishlistIconHeader(); + await allPages.inventoryPage.assertWishlistPage(); + await allPages.inventoryPage.clickOnWishlistAddToCard(); + }) + + await test.step('Checkout product added to cart', async () => { + await allPages.cartPage.clickOnCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); }); - } -); - -test.only( - 'Verify if user can add product to wishlist, move to cart(Checkout page)', - { - tag: '@firefox', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Wishlist' }, - { type: 'testdino:link', description: 'https://jira.example.com/WISHLIST-002' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Wishlist to cart (Checkout page) on Firefox' } - ] - }, - async () => { - const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); + const flowTime = Date.now() - start; test.info().annotations.push({ type: 'testdino:metric', @@ -453,204 +435,222 @@ test.only( threshold: 5000, }), }); - } -); - -test.only( - 'Verify that user is able to submit a product review (Review)', - { - tag: '@webkit', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Review' }, - { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-004' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Submit product review (Review) on WebKit' } - ] - }, - async () => { +}); + +test.describe('Orders Module', () => { + test.describe('Order Cancellation', () => { + test('Verify that new user can view and cancel an order in My Orders', { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p0' }, + { type: 'testdino:feature', description: 'Orders' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-002' }, + { type: 'testdino:owner', description: '@Kriti Verma' }, + { type: 'testdino:notify-slack', description: '@Kriti Verma' }, + { type: 'testdino:context', description: 'Critical order cancellation flow for new users' } + ] + }, async () => { const start = Date.now(); - await login(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + let productName= `Rode NT1-A Condenser Mic`; + + // await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + // }) + + // await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // }) + + await test.step('Navigate to All Products and add view details of a random product', async () => { + await allPages.homePage.clickAllProductsNav(); + await allPages.allProductsPage.assertAllProductsTitle(); + productName = await allPages.allProductsPage.getNthProductName(1); + await allPages.allProductsPage.clickNthProduct(1); + await allPages.productDetailsPage.clickAddToCartButton(); + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'order-flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); + + // await test.step('Add product to cart, add new address and checkout', async () => { + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.assertYourCartTitle(); + // await expect(allPages.cartPage.getCartItemName()).toContainText(productName, { timeout: 10000 }); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.fillShippingAddress( + // firstName, email, 'New York', 'New York', '123 Main St', '10001', 'United States' + // ); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.assertAddressAddedToast(); + // }) + + // await test.step('Complete order and verify in my orders', async () => { + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.inventoryPage.clickOnContinueShopping(); + + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.orderPage.clickOnMyOrdersTab(); + // await allPages.orderPage.clickCancelOrderButton(); + // await allPages.orderPage.confirmCancellation(); + // }); }); - } -); - -test.only( - 'Verify that user can update cart quantity and verify total price', - { - tag: '@chromium', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Cart' }, - { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Update cart quantity and total price on Chromium' } - ] - }, - async () => { + }); +}); + +test('Verify that a new user can complete the journey from registration to multiple order placements', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Order' }, + { type: 'testdino:link', description: 'https://jira.example.com/ORDER-003' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Registration to multiple order placement on Firefox' } + ] +}, async () => { const start = Date.now(); - await login(); - // await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.clickNthProduct(1); + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + let productName= `Rode NT1-A Condenser Mic`; + + await test.step('Navigate to All Products and add view details of a random product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + await allPages.productDetailsPage.clickOnAdditionalInfoTab(); + await allPages.productDetailsPage.assertAdditionalInfoTab(); + }) + + await test.step('Add product to cart, change quantity, add new address and checkout', async () => { // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.cartPage.clickOnCartIcon(); + // await allPages.productDetailsPage.clickCartIcon(); // await allPages.cartPage.clickIncreaseQuantityButton(); - // await allPages.cartPage.verifyTotalPriceUpdated(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test.only( - 'Verify that user can view order history and order detail (Order page)', - { - tag: '@firefox', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Order' }, - { type: 'testdino:link', description: 'https://jira.example.com/ORDER-003' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Order history and order detail (Order page) on Firefox' } - ] - }, - async () => { - const start = Date.now(); - await login(); - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.verifyOrdersListVisible(); - // await allPages.orderPage.clickOnFirstOrder(); - // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test.only( - 'Verify that user can update cart quantity and verify total price (Pricing)', - { - tag: '@chromium', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Cart' }, - { type: 'testdino:link', description: 'https://jira.example.com/CART-002' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Cart quantity and total price (Pricing) on Chromium' } - ] - }, - async () => { - const start = Date.now(); - await login(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.fillShippingAddress(process.env.SFIRST_NAME, email, process.env.SCITY, process.env.SSTATE, process.env.SSTREET_ADD, process.env.SZIP_CODE, process.env.SCOUNTRY); + // await allPages.checkoutPage.clickSaveAddressButton(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + // await allPages.checkoutPage.verifyOrderConfirmedTitle(); + // await allPages.checkoutPage.clickOnContinueShoppingButton(); + }) + + await test.step('Add another product to cart, select existing address and checkout', async () => { // await allPages.homePage.clickOnShopNowButton(); + // await allPages.allProductsPage.assertAllProductsTitle(); // await allPages.allProductsPage.clickNthProduct(1); // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.clickIncreaseQuantityButton(); - // await allPages.cartPage.verifyTotalPriceUpdated(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test.only( - 'Verify that user can view order history and order details properly (Order details)', - { - tag: '@firefox', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Order' }, - { type: 'testdino:link', description: 'https://jira.example.com/ORDER-004' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Order history and details (Order details) on Firefox' } - ] - }, - async () => { + // await allPages.productDetailsPage.clickCartIcon(); + // await allPages.cartPage.clickOnCheckoutButton(); + // await allPages.checkoutPage.verifyCheckoutTitle(); + // await allPages.checkoutPage.selectCashOnDelivery(); + // await allPages.checkoutPage.verifyCashOnDeliverySelected(); + // await allPages.checkoutPage.clickOnPlaceOrder(); + // await allPages.checkoutPage.verifyOrderPlacedSuccessfully(); + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); +}); + +test('Verify that a new user can sign up, log in, and navigate to the home page successfully', { + tag: '@ios', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Registration' }, + { type: 'testdino:link', description: 'https://jira.example.com/REG-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Sign up, login and navigate home on iOS' } + ] +}, async () => { const start = Date.now(); - await login(); - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.verifyOrdersListVisible(); - // await allPages.orderPage.clickOnFirstOrder(); - // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); - await expect(true).toBeTruthy(); - const flowTime = Date.now() - start; - test.info().annotations.push({ - type: 'testdino:metric', - description: JSON.stringify({ - name: 'flow-time', - value: flowTime, - unit: 'ms', - threshold: 5000, - }), - }); - } -); - -test( - 'Verify that users can update cart quantity and verify total price (Single order)', - { - tag: '@chromium', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Cart' }, - { type: 'testdino:link', description: 'https://jira.example.com/CART-003' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Cart quantity (Single order) on Chromium' } - ] - }, - async () => { + const email = `test+${Date.now()}@test.com`; + const firstName = 'Test'; + const lastName = 'User'; + + await test.step('Verify that user can register successfully', async () => { + // await allPages.loginPage.clickOnUserProfileIcon(); + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.clickOnSignupLink(); + // await allPages.signupPage.assertSignupPage(); + // await allPages.signupPage.signup(firstName, lastName, email, process.env.PASSWORD); + // await allPages.signupPage.verifySuccessSignUp(); + }) + + // await test.step('Verify that user can login successfully', async () => { + // await allPages.loginPage.validateSignInPage(); + // await allPages.loginPage.login(email, process.env.PASSWORD); + // await allPages.loginPage.verifySuccessSignIn(); + // await expect(allPages.homePage.getHomeNav()).toBeVisible({ timeout: 30000 }); + // }) + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); +}) + +test('Verify that user can fill and submit the Contact Us form successfully', { + tag: '@chromium', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Contact' }, + { type: 'testdino:link', description: 'https://jira.example.com/CONTACT-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Contact Us form submission on Chromium' } + ] +}, async () => { const start = Date.now(); await login(); - // await allPages.homePage.clickOnShopNowButton(); - // await allPages.allProductsPage.clickNthProduct(1); - // await allPages.productDetailsPage.clickAddToCartButton(); - // await allPages.cartPage.clickOnCartIcon(); - // await allPages.cartPage.clickIncreaseQuantityButton(); - // await allPages.cartPage.verifyTotalPriceUpdated(); - await expect(true).toBeTruthy(); + await allPages.homePage.clickOnContactUsLink(); + await allPages.contactUsPage.assertContactUsTitle(); + await allPages.contactUsPage.fillContactUsForm(); + await allPages.contactUsPage.verifySuccessContactUsFormSubmission(); const flowTime = Date.now() - start; test.info().annotations.push({ type: 'testdino:metric', @@ -661,31 +661,109 @@ test( threshold: 5000, }), }); - } -); - -test( - 'Verify that users can view order history and order details properly (Order history)', - { - tag: '@firefox', - annotation: [ - { type: 'testdino:priority', description: 'p1' }, - { type: 'testdino:feature', description: 'Order' }, - { type: 'testdino:link', description: 'https://jira.example.com/ORDER-005' }, - { type: 'testdino:owner', description: 'qa-team' }, - { type: 'testdino:notify-slack', description: '#e2e-alerts' }, - { type: 'testdino:context', description: 'Order history on Firefox' } - ] - }, - async () => { +}); + +test('Verify that user can submit a product review', { + tag: '@firefox', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Review' }, + { type: 'testdino:link', description: 'https://jira.example.com/REVIEW-002' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Submit product review on Firefox' } + ] +}, async () => { + const start = Date.now(); + await test.step('Login as existing user and navigate to a product', async () => { + // await login(); + }) + + await test.step('Navigate to all product section and select a product', async () => { + await allPages.homePage.clickOnShopNowButton(); + await allPages.allProductsPage.assertAllProductsTitle(); + await allPages.allProductsPage.clickNthProduct(1); + }) + + await test.step('Submit a product review and verify submission', async () => { + await allPages.productDetailsPage.clickOnReviewsTab(); + await allPages.productDetailsPage.assertReviewsTab(); + + await allPages.productDetailsPage.clickOnWriteAReviewBtn(); + await allPages.productDetailsPage.fillReviewForm(); + await allPages.productDetailsPage.assertSubmittedReview({ + name: 'John Doe', + title: 'Great Product', + opinion: 'This product exceeded my expectations. Highly recommend!' + }); + }); + + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); +}); + +test('Verify that user can update personal information in profile', { + tag: '@webkit', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Profile' }, + { type: 'testdino:link', description: 'https://jira.example.com/PROFILE-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Update personal information on WebKit' } + ] +}, async () => { + const start = Date.now(); + await allPages.userPage.clickOnUserProfileIcon(); +// await allPages.userPage.updatePersonalInfo(); +// await allPages.userPage.verifyPersonalInfoUpdated(); + const flowTime = Date.now() - start; + test.info().annotations.push({ + type: 'testdino:metric', + description: JSON.stringify({ + name: 'flow-time', + value: flowTime, + unit: 'ms', + threshold: 5000, + }), + }); +}); + +test('Verify that user can delete a selected product from cart', { + tag: '@android', + annotation: [ + { type: 'testdino:priority', description: 'p1' }, + { type: 'testdino:feature', description: 'Cart' }, + { type: 'testdino:link', description: 'https://jira.example.com/CART-001' }, + { type: 'testdino:owner', description: 'qa-team' }, + { type: 'testdino:notify-slack', description: '#e2e-alerts' }, + { type: 'testdino:context', description: 'Delete selected product from cart on Android' } + ] +}, async () => { const start = Date.now(); + const productName = 'GoPro HERO10 Black'; await login(); - // await allPages.loginPage.clickOnUserProfileIcon(); - // await allPages.orderPage.clickOnMyOrdersTab(); - // await allPages.orderPage.verifyOrdersListVisible(); - // await allPages.orderPage.clickOnFirstOrder(); - // await allPages.orderDetailsPage.verifyOrderDetailsDisplayed(); - await expect(true).toBeTruthy(); + await allPages.inventoryPage.clickOnShopNowButton(); + await allPages.inventoryPage.clickOnAllProductsLink(); + await allPages.inventoryPage.searchProduct(productName); + await allPages.inventoryPage.verifyProductTitleVisible(productName); + await allPages.inventoryPage.clickOnAddToCartIcon(); + + await allPages.cartPage.clickOnCartIcon(); + await allPages.cartPage.verifyCartItemVisible(productName); + await allPages.cartPage.clickOnDeleteProductIcon(); + await allPages.cartPage.verifyCartItemDeleted(productName); + await allPages.cartPage.verifyEmptyCartMessage(); + await allPages.cartPage.clickOnStartShoppingButton(); + await allPages.allProductsPage.assertAllProductsTitle(); const flowTime = Date.now() - start; test.info().annotations.push({ type: 'testdino:metric', @@ -696,5 +774,4 @@ test( threshold: 5000, }), }); - } -); +}); From 53a3c42f940f8bf60777bc3ea1bbcdee75497514 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:17:05 +0530 Subject: [PATCH 54/67] Refactor GitHub Actions workflow for Playwright tests --- .github/workflows/test.yml | 74 +++++++++----------------------------- 1 file changed, 16 insertions(+), 58 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 039d24f..60b3f0c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,11 +21,10 @@ jobs: steps: - uses: actions/checkout@v4 - # ✅ REQUIRED: Node 20 - name: Setup Node.js 20.x uses: actions/setup-node@v3 with: - node-version: '20' + node-version: "20" - name: Create .env file run: | @@ -47,39 +46,13 @@ jobs: restore-keys: | ${{ runner.os }}-node- - - name: Install deps + browsers + - name: Install dependencies run: | npm ci - npx playwright install --with-deps chromium firefox webkit + npx playwright install --with-deps - # ✅ FULL + RERUN LOGIC - - name: Run Playwright (rerun failed tests if applicable) - env: - TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} - SHARD_INDEX: ${{ matrix.shardIndex }} - SHARD_TOTAL: ${{ matrix.shardTotal }} + - name: Run Playwright Tests run: | - echo "GitHub run attempt: ${{ github.run_attempt }}" - if [[ "${{ github.run_attempt }}" -gt 1 ]]; then - echo "Detected re-run. Checking failed test metadata from TestDino." - npx tdpw last-failed \ - --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ - > last-failed-flags.txt - EXTRA_PW_FLAGS="$(cat last-failed-flags.txt)" - if [[ -n "$EXTRA_PW_FLAGS" ]]; then - echo "Running only failed tests for this shard:" - echo "$EXTRA_PW_FLAGS" - # IMPORTANT: JSON + BLOB BOTH REQUIRED - # Ensure playwright-report directory exists - mkdir -p ./playwright-report - eval "npx playwright test $EXTRA_PW_FLAGS" - exit 0 - fi - echo "No failed test metadata found. Falling back to full shard." - fi - # First run (full shard) - # Ensure playwright-report directory exists - mkdir -p ./playwright-report npx playwright test \ --grep="@chromium|@firefox|@webkit" \ --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} @@ -92,15 +65,6 @@ jobs: path: ./blob-report retention-days: 1 - # ✅ THIS WILL SHOW "Metadata cached successfully ✔" WHEN JSON EXISTS - - name: Cache tdpw last failed metadata - if: always() - env: - TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} - SHARD_INDEX: ${{ matrix.shardIndex }} - SHARD_TOTAL: ${{ matrix.shardTotal }} - run: | - npx tdpw cache --verbose merge-reports: name: Merge Reports needs: run-tests @@ -110,23 +74,16 @@ jobs: steps: - uses: actions/checkout@v4 - # ✅ Node 20 here as well - name: Setup Node.js 20.x uses: actions/setup-node@v3 with: - node-version: '20' + node-version: "20" - - name: Cache npm dependencies - uses: actions/cache@v3 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- - - name: Install deps + browsers + - name: Install dependencies run: | npm ci npx playwright install --with-deps + - name: Download all blob reports uses: actions/download-artifact@v4 with: @@ -135,19 +92,20 @@ jobs: merge-multiple: true - name: Merge HTML & JSON reports - run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports + run: | + npx playwright merge-reports \ + --config=playwright.config.js \ + ./all-blob-reports - - name: Upload combined report + - name: Upload combined Playwright report uses: actions/upload-artifact@v4 with: name: Playwright Test Report path: ./playwright-report retention-days: 14 - - name: Send TestDino report + - name: Upload report to TestDino + env: + TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} run: | - npx --yes tdpw ./playwright-report \ - --token="${{ secrets.TESTDINO_TOKEN }}" \ - --upload-html \ - --upload-traces \ - --verbose \ No newline at end of file + npx tdpw upload ./playwright-report --token="$TESTDINO_TOKEN" From 89692a31cc7828afeeec1d7129f36c2ec3c12176 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:25:54 +0530 Subject: [PATCH 55/67] Refactor Playwright config for improved reporting --- playwright.config.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 09e3ba2..a887a1c 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -3,29 +3,33 @@ import { defineConfig, devices } from '@playwright/test'; import * as dotenv from 'dotenv'; dotenv.config({ quiet: true }); + const isCI = !!process.env.CI; export default defineConfig({ testDir: './tests', + fullyParallel: true, forbidOnly: isCI, + retries: isCI ? 0 : 0, workers: isCI ? 1 : 1, - timeout: 30 * 1000, + reporter: [ + ['blob', { outputDir: 'blob-report' }], // Required for shard merging + ['json', { outputFile: './playwright-report/report.json' }], // Required for TestDino ['html', { outputFolder: 'playwright-report', open: 'never' - }], - ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging - ['json', { outputFile: './playwright-report/report.json' }], + }] ], use: { baseURL: 'https://storedemo.testdino.com', headless: true, + trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', @@ -35,17 +39,17 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, - grep: /@chromium/, // only run tests tagged @chromium + grep: /@chromium/, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, - grep: /@firefox/, // only run tests tagged @firefox + grep: /@firefox/, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, - grep: /@webkit/, // only run tests tagged @webkit + grep: /@webkit/, }, ], }); From 52745617f8f9515382328592752d788584446b0d Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:49:52 +0530 Subject: [PATCH 56/67] Reorder reporter configuration in playwright config --- playwright.config.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index a887a1c..89bc31f 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -17,14 +17,11 @@ export default defineConfig({ timeout: 30 * 1000, - reporter: [ - ['blob', { outputDir: 'blob-report' }], // Required for shard merging - ['json', { outputFile: './playwright-report/report.json' }], // Required for TestDino - ['html', { - outputFolder: 'playwright-report', - open: 'never' - }] - ], +reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging + ['json', { outputFile: './playwright-report/report.json' }], +], use: { baseURL: 'https://storedemo.testdino.com', From 88d951027f005792da2ad2da6f94323ad19191c6 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:58:12 +0530 Subject: [PATCH 57/67] Updates workflow file by adding merge report Updated cron schedule to run tests every day at 9:00 AM IST. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60b3f0c..506ac2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ on: push: pull_request: schedule: - - cron: '30 3 * * 1-5' # 11:00 AM IST + - cron: '30 3 * * *' # 9:00 AM IST everyday # 11:00 AM IST workflow_dispatch: jobs: From 4e574acd2ee59075531b01e6e2a1003694d3c729 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:59:00 +0530 Subject: [PATCH 58/67] Refactor Playwright test workflow configuration Updated Playwright test workflow to use Node.js 20.x and adjusted shard settings. Added merging of reports and improved environment variable handling. --- .github/workflows/test.yml | 206 +++++++++++-------------------------- 1 file changed, 60 insertions(+), 146 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70ea782..506ac2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,155 +1,31 @@ -# # .github/workflows/playwright-daily.yml -# # Runs Playwright test shards every day at **11 : 42 AM IST** (06 : 12 UTC) -# # plus anytime you trigger it manually from the Actions tab. - -# name: Run Playwright tests - -# on: -# push: # runs on every push -# pull_request: # runs on new PRs or PR updates -# schedule: -# - cron: '30 3 * * 1-5' -# workflow_dispatch: - -# jobs: -# run-tests: -# name: Run shard ${{ matrix.shardIndex }}/5 -# runs-on: ubuntu-latest - -# strategy: -# fail-fast: false -# matrix: -# shardIndex: [1,2,3,4,5] -# shardTotal: [5] - -# steps: -# - uses: actions/checkout@v4 - -# - name: Setup Node.js 18.x -# uses: actions/setup-node@v3 -# with: -# node-version: '18' - -# - name: Create .env file -# run: | -# echo "USERNAME=${{ secrets.USERNAME }}" >> .env -# echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env -# echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env -# echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env -# echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env -# echo "CITY=${{ secrets.CITY }}" >> .env -# echo "STATE=${{ secrets.STATE }}" >> .env -# echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env -# echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - -# - name: Cache npm dependencies -# uses: actions/cache@v3 -# with: -# path: ~/.npm -# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} -# restore-keys: | -# ${{ runner.os }}-node- - -# - name: Install deps + browsers -# run: | -# npm ci -# npx playwright install --with-deps chromium firefox webkit - -# - name: List Playwright projects (debug) -# run: npx playwright list --projects | cat - -# - name: Run shard ${{ matrix.shardIndex }} -# run: npx playwright test --grep="@chromium|@firefox|@webkit|@android|@ios" --reporter=blob --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - -# - name: Upload blob report -# if: ${{ !cancelled() }} -# uses: actions/upload-artifact@v4 -# with: -# name: blob-report-${{ matrix.shardIndex }} -# path: ./blob-report -# retention-days: 1 - -# merge-reports: -# name: Merge Reports -# needs: run-tests -# if: always() # run even if some shards fail -# runs-on: ubuntu-latest - -# steps: -# - uses: actions/checkout@v4 - -# - name: Setup Node.js 18.x -# uses: actions/setup-node@v3 -# with: -# node-version: '18' - -# - name: Cache npm dependencies -# uses: actions/cache@v3 -# with: -# path: ~/.npm -# key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} -# restore-keys: | -# ${{ runner.os }}-node- - -# - name: Install deps + browsers -# run: | -# npm ci -# npx playwright install --with-deps - -# - name: Download all blob reports -# uses: actions/download-artifact@v4 -# with: -# path: ./all-blob-reports -# pattern: blob-report-* -# merge-multiple: true - -# - name: Merge HTML & JSON reports -# run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports - -# - name: Upload combined report -# uses: actions/upload-artifact@v4 -# with: -# name: Playwright Test Report -# path: ./playwright-report -# retention-days: 14 - -# - name: Send TestDino report -# run: | -# npx --yes tdpw ./playwright-report \ -# --token="${{ secrets.TESTDINO_TOKEN }}" \ -# --upload-html \ -# --verbose - - name: Run Playwright tests on: push: pull_request: + schedule: + - cron: '30 3 * * *' # 9:00 AM IST everyday # 11:00 AM IST workflow_dispatch: jobs: run-tests: - name: Run shard ${{ matrix.shardIndex }}/5 + name: Run Playwright tests ${{ matrix.shardIndex }}/3 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4, 5] - shardTotal: [5] + shardIndex: [1, 2, 3] + shardTotal: [3] steps: - - name: Checkout repo - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - # ✅ Required Node version - name: Setup Node.js 20.x - uses: actions/setup-node@v4 + uses: actions/setup-node@v3 with: - node-version: '20' + node-version: "20" - # ✅ Required env variables for tests - name: Create .env file run: | echo "USERNAME=${{ secrets.USERNAME }}" >> .env @@ -162,7 +38,6 @@ jobs: echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env - # ✅ Cache npm dependencies - name: Cache npm dependencies uses: actions/cache@v3 with: @@ -171,27 +46,66 @@ jobs: restore-keys: | ${{ runner.os }}-node- - # ✅ Install dependencies + TestDino (.tgz) + Playwright browsers - - name: Install dependencies & browsers + - name: Install dependencies run: | - npm install - npm install --save-dev @testdino/playwright@latest - npx playwright install --with-deps chromium firefox webkit + npm ci + npx playwright install --with-deps - # ✅ Run Playwright shard with TestDino - - name: Run Playwright shard with TestDino - env: - TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} + - name: Run Playwright Tests run: | npx playwright test \ - --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \ - --grep "@chromium|@firefox|@webkit|@android|@ios|@api" \ + --grep="@chromium|@firefox|@webkit" \ + --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - # ✅ Upload blob report (needed for merge) - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: blob-report-${{ matrix.shardIndex }} path: ./blob-report - retention-days: 1 \ No newline at end of file + retention-days: 1 + + merge-reports: + name: Merge Reports + needs: run-tests + if: always() + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js 20.x + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: Install dependencies + run: | + npm ci + npx playwright install --with-deps + + - name: Download all blob reports + uses: actions/download-artifact@v4 + with: + path: ./all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge HTML & JSON reports + run: | + npx playwright merge-reports \ + --config=playwright.config.js \ + ./all-blob-reports + + - name: Upload combined Playwright report + uses: actions/upload-artifact@v4 + with: + name: Playwright Test Report + path: ./playwright-report + retention-days: 14 + + - name: Upload report to TestDino + env: + TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} + run: | + npx tdpw upload ./playwright-report --token="$TESTDINO_TOKEN" From f763542fe63273822abcdd53cd3d4ad9b638abb4 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:07:15 +0530 Subject: [PATCH 59/67] Refactor Playwright configuration settings Updated configuration settings for Playwright tests, including retries, workers, timeout, and reporter options. --- playwright.config.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 8af6021..88ef773 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,30 +1,33 @@ // @ts-check import { defineConfig, devices } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +dotenv.config({ quiet: true }); const isCI = !!process.env.CI; export default defineConfig({ testDir: './tests', + fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 1 : 1, - workers: isCI ? 5 : 5, - timeout: 60 * 1000, - reporter: [ - ['html', { - outputFolder: 'playwright-report', - open: 'never' - }], - ['blob', { outputDir: 'blob-report' }], - ['json', { outputFile: './playwright-report/report.json' }], - ['@testdino/playwright', { token: process.env.TESTDINO_TOKEN }], - ], + retries: isCI ? 0 : 0, + workers: isCI ? 1 : 1, + + timeout: 30 * 1000, + +reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging + ['json', { outputFile: './playwright-report/report.json' }], +], use: { - baseURL: 'https://storedemo.testdino.com/', + baseURL: 'https://storedemo.testdino.com', headless: true, - trace: 'on', + + trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, From d8c9add567663e58041e5dabaa1d68f608af9e91 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:08:14 +0530 Subject: [PATCH 60/67] Expand Playwright test grep filters to include API and mobile --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 506ac2f..40832b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,7 @@ jobs: - name: Run Playwright Tests run: | npx playwright test \ - --grep="@chromium|@firefox|@webkit" \ + --grep="@chromium|@firefox|@webkit|@api|@andriod|@ios" \ --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report From cca433437e2c0a243729ec9ed793d5f4813879e8 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:09:11 +0530 Subject: [PATCH 61/67] Increase shard count to 5 in test workflow --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40832b9..b7d40fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,8 +15,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3] - shardTotal: [3] + shardIndex: [1, 2, 3, 4, 5] + shardTotal: [5] steps: - uses: actions/checkout@v4 From 28f10402e21dbfe96cbd62e5cacbb5dc0733957f Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:18:54 +0530 Subject: [PATCH 62/67] Update Playwright config for retries and debugging --- playwright.config.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 88ef773..deac8db 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -12,57 +12,58 @@ export default defineConfig({ fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 0 : 0, + // ✅ Retries set to 2 + retries: 2, + workers: isCI ? 1 : 1, timeout: 30 * 1000, -reporter: [ - ['html', { outputFolder: 'playwright-report', open: 'never' }], - ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging - ['json', { outputFile: './playwright-report/report.json' }], -], + reporter: [ + ['html', { outputFolder: 'playwright-report', open: 'never' }], + ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging + ['json', { outputFile: './playwright-report/report.json' }], + ], use: { baseURL: 'https://storedemo.testdino.com', headless: true, - trace: 'on-first-retry', - screenshot: 'only-on-failure', - video: 'retain-on-failure', + // ✅ Always capture debugging artifacts + trace: 'on', + screenshot: 'on', + video: 'on', }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, - grep: /@chromium/, // only run tests tagged @chromium + grep: /@chromium/, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, - grep: /@firefox/, // only run tests tagged @firefox + grep: /@firefox/, }, { name: 'webkit', use: { ...devices['Desktop Safari'] }, - grep: /@webkit/, // only run tests tagged @webkit + grep: /@webkit/, }, { name: 'android', use: { ...devices['Pixel 5'] }, - grep: /@android/, // only run tests tagged @android + grep: /@android/, }, { name: 'ios', use: { ...devices['iPhone 12'] }, - grep: /@ios/, // only run tests tagged @ios + grep: /@ios/, }, - { name: 'api', - use: { ...devices['API'] }, - grep: /@api/, // only run tests tagged @api + grep: /@api/, }, ], }); From 16086cb11e553ef21b6e52b242e7ab044fe7f0a8 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:25:56 +0530 Subject: [PATCH 63/67] Update Playwright test shard count to 5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7d40fa..50e228c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ on: jobs: run-tests: - name: Run Playwright tests ${{ matrix.shardIndex }}/3 + name: Run Playwright tests ${{ matrix.shardIndex }}/5 runs-on: ubuntu-latest strategy: From 33a1042bd9cb0fb3721ed6fbaf17acc26b88ff95 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:31:01 +0530 Subject: [PATCH 64/67] Enhance TestDino upload command with additional options --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50e228c..cb2edc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,4 +108,7 @@ jobs: env: TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }} run: | - npx tdpw upload ./playwright-report --token="$TESTDINO_TOKEN" + npx tdpw upload ./playwright-report \ + --upload-traces \ + --upload-html \ + --token="$TESTDINO_TOKEN" From 5b58b845d2e178ff464fdb942bf9eb223a52d059 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:21:10 +0530 Subject: [PATCH 65/67] Modify Playwright config for test settings Updated configuration settings for retries, workers, timeout, and debugging artifacts. --- playwright.config.js | 48 ++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/playwright.config.js b/playwright.config.js index 489e616..6d2aecd 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -8,35 +8,31 @@ const isCI = !!process.env.CI; export default defineConfig({ testDir: './tests', - snapshotDir: './__screenshots__', // ✅ Baseline image storage + fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 1 : 1, // Enable retries for flaky test behavior - workers: isCI ? 5 : 5, - timeout: 60 * 1000, - expect: { - timeout: 10 * 1000, - }, - + // ✅ Retries set to 2 + retries: isCI ? 2 : 2, + + workers: isCI ? 1 : 1, + + timeout: 30 * 1000, + reporter: [ - ['html', { - outputFolder: 'playwright-report', - open: 'never' - }], + ['html', { outputFolder: 'playwright-report', open: 'never' }], ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging ['json', { outputFile: './playwright-report/report.json' }], - ['@testdino/playwright', { token: process.env.TESTDINO_TOKEN }], ], use: { - baseURL: 'https://storedemo.testdino.com/products', + baseURL: 'https://storedemo.testdino.com', headless: true, - trace: 'retain-on-failure', - screenshot: 'only-on-failure', - video: 'retain-on-failure', - actionTimeout: 15 * 1000, - navigationTimeout: 30 * 1000, + + // ✅ Always capture debugging artifacts + trace: 'on', + screenshot: 'on', + video: 'on', }, projects: [ @@ -55,5 +51,19 @@ export default defineConfig({ use: { ...devices['Desktop Safari'] }, grep: /@webkit/, }, + { + name: 'android', + use: { ...devices['Pixel 5'] }, + grep: /@android/, + }, + { + name: 'ios', + use: { ...devices['iPhone 12'] }, + grep: /@ios/, + }, + { + name: 'api', + grep: /@api/, + }, ], }); From 308aa28a937a9683f1d25597de2529f7679de3cc Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:33:04 +0530 Subject: [PATCH 66/67] Modify Playwright config for test settings --- tests/visual.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/visual.spec.js b/tests/visual.spec.js index d884647..76f2783 100644 --- a/tests/visual.spec.js +++ b/tests/visual.spec.js @@ -11,7 +11,7 @@ test.beforeEach(async ({ page }) => { test.describe('Visual Comparison', () => { test.describe('GitHub Login Page', () => { - test('visual comparison demo test', { + test.skip('visual comparison demo test', { tag: ['@visual', '@chromium'], annotation: [ { type: 'testdino:priority', description: 'p1' }, From facda91d273512491ae8d45828f0c5a19ef9cba1 Mon Sep 17 00:00:00 2001 From: Kriti Verma <155474803+kriti2710@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:44:28 +0530 Subject: [PATCH 67/67] Simplify retries configuration in Playwright config --- playwright.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.js b/playwright.config.js index 6d2aecd..deac8db 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -13,7 +13,7 @@ export default defineConfig({ forbidOnly: isCI, // ✅ Retries set to 2 - retries: isCI ? 2 : 2, + retries: 2, workers: isCI ? 1 : 1,