diff --git a/.claude/mcp.json b/.claude/mcp.json new file mode 100644 index 0000000..40dd310 --- /dev/null +++ b/.claude/mcp.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "TestDino": { + "command": "/opt/homebrew/bin/testdino-mcp", + "env": { + "TESTDINO_PAT": "tpu_d50a352ff1a128668ecaeba1da1026825eb314678cf47751d50c55503c89297e" + } + } + } + } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 039d24f..b417a53 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,28 +4,28 @@ 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: run-tests: - name: Run Playwright tests ${{ matrix.shardIndex }}/3 + name: Run shard ${{ matrix.shardIndex }}/5 runs-on: ubuntu-latest strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3] - shardTotal: [3] + shardIndex: [1, 2, 3, 4, 5] + shardTotal: [5] steps: - - uses: actions/checkout@v4 + - name: Checkout repo + 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: | @@ -38,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: @@ -46,44 +45,15 @@ jobs: 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 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 }} + npx playwright install --with-deps + - 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 }} - - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 @@ -92,15 +62,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,20 +71,12 @@ 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 @@ -135,19 +88,19 @@ jobs: merge-multiple: true - name: Merge HTML & JSON reports - run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports - - - name: Upload combined report + 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: 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" \ No newline at end of file diff --git a/playwright.config.js b/playwright.config.js index 09e3ba2..82cf3e4 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,34 +1,39 @@ // @ts-check import { defineConfig, devices } from '@playwright/test'; -import * as dotenv from 'dotenv'; +import dotenv from 'dotenv'; + +// Load environment variables +dotenv.config(); -dotenv.config({ quiet: true }); const isCI = !!process.env.CI; export default defineConfig({ testDir: './tests', + snapshotDir: './__screenshots__', fullyParallel: true, forbidOnly: isCI, - retries: isCI ? 0 : 0, - workers: isCI ? 1 : 1, - + retries: isCI ? 1 : 1, // Enable retries for flaky test behavior + workers: isCI ? 5 : 5, - timeout: 30 * 1000, + timeout: 60 * 1000, + expect: { + timeout: 10 * 1000, + }, + reporter: [ - ['html', { - outputFolder: 'playwright-report', - open: 'never' - }], - ['blob', { outputDir: 'blob-report' }], // Blob reporter for merging + ['blob'], // ✅ REQUIRED for sharding + ['html', { outputDir: './playwright-report' }], ['json', { outputFile: './playwright-report/report.json' }], ], use: { - baseURL: 'https://storedemo.testdino.com', + 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: [ @@ -47,5 +52,20 @@ export default defineConfig({ 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 + }, ], }); diff --git a/tests/example.spec.js b/tests/example.spec.js index 9617a1b..fa397e0 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,251 +11,212 @@ test.beforeEach(async ({ page }) => { await page.goto('/'); }); -/* ---------- Helpers ---------- */ +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 login( - username = process.env.USERNAME, - password = process.env.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); } -/* ---------- 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(); - } - ); -}); +async function logout() { + await allPages.loginPage.clickOnUserProfileIcon(); + await allPages.loginPage.clickOnLogoutButton(); +} -/* ---------- STABLE TESTS (NO RANDOM FAILURES) ---------- */ +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 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 () => { +test('Verify that User Can Add, Edit, and Delete Addresses after Logging In', 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(); - } -); \ No newline at end of file + + 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 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 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(); + }) +}); +