From 5d6c7335a30665c24b590a2a1d3dcdf644dc8238 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Wed, 26 Nov 2025 15:04:45 +1000 Subject: [PATCH] Resolve all e2e test failures and export helper functions to their own file. modified: e2e/picklist.spec.ts modified: e2e/product-filter.spec.ts new file: e2e/test-helpers.ts modified: src/screens/ManageProductsScreen.tsx modified: src/screens/StartPickListScreen.tsx --- e2e/picklist.spec.ts | 235 +++++++++---------- e2e/product-filter.spec.ts | 26 ++- e2e/test-helpers.ts | 326 +++++++++++++++++++++++++++ src/screens/ManageProductsScreen.tsx | 3 +- src/screens/StartPickListScreen.tsx | 1 + 5 files changed, 461 insertions(+), 130 deletions(-) create mode 100644 e2e/test-helpers.ts diff --git a/e2e/picklist.spec.ts b/e2e/picklist.spec.ts index 0c708fc..d71401d 100644 --- a/e2e/picklist.spec.ts +++ b/e2e/picklist.spec.ts @@ -1,61 +1,44 @@ import { expect, Page, test } from '@playwright/test'; -const areaName = 'Drinks'; -const additionalProduct = 'Pump 750'; -const listProductName = 'List Flow Soda'; -const updatedListProductName = 'List Flow Soda Zero'; -const searchTerm = 'E2E Search'; -const matchingProducts = [`${searchTerm} Alpha`, `${searchTerm} Beta`]; -const nonMatchingProduct = 'E2E Other Gamma'; -const secondaryProduct = 'Mount Franklin 600ml'; +import { + areaName, + additionalProduct, + listProductName, + updatedListProductName, + searchTerm, + matchingProducts, + nonMatchingProduct, + secondaryProduct, + waitForScreenHeading, + openSelect, + ensureCategoryExists, + expectProductQuantity, + addProductToPickList, + navigateToNewPickList, +} from './test-helpers'; -const navigateToNewPickList = async (page: Page) => { - await page.goto('/'); - - await expect(page.getByRole('heading', { name: 'StockFill' })).toBeVisible(); - await page.getByRole('link', { name: 'Create Pick List' }).click(); - await page.getByLabel('Area').click(); - await page.getByRole('option', { name: areaName }).first().click(); - await page.getByRole('button', { name: 'Save Pick List' }).click(); - - await expect(page.getByRole('heading', { name: `${areaName} List` })).toBeVisible(); -}; - -const addProductToPickList = async (page: Page, productName: string) => { - const searchInput = page.getByPlaceholder('Search products'); - await searchInput.click(); - await searchInput.fill(productName); - const productOption = page - .getByRole('option', { name: new RegExp(`${productName} \\(${areaName}\\)`, 'i') }) - .first(); - - await expect(productOption).toBeVisible({ timeout: 15000 }); - await productOption.click(); - - await expect(page.getByText(productName).first()).toBeVisible({ timeout: 15000 }); -}; +/* ----------------------- + Test suite + ----------------------- */ test.describe('Active pick list', () => { test.use({ viewport: { width: 1280, height: 900 } }); + test('adds a product via search and renders it with quantity and packaging', async ({ page }) => { await navigateToNewPickList(page); - const searchInput = page.getByPlaceholder('Search products'); + // Add product via search + const searchInput = page.getByTestId('product-search-input'); await searchInput.fill(secondaryProduct); - await page - .getByRole('option', { name: new RegExp(`${secondaryProduct} \\(${areaName}\\)`, 'i') }) - .first() - .click(); + await page.getByRole('option', { name: new RegExp(`${secondaryProduct} \\(${areaName}\\)`, 'i') }).first().click(); - await expect(page.getByText(secondaryProduct, { exact: true })).toBeVisible(); - await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 }); + // Product should appear in pick list as "1 x Mount Franklin 600ml" + await expectProductQuantity(page, secondaryProduct, 1, 15000); }); test('creates a pick list and adds products from the search bar', async ({ page }) => { await navigateToNewPickList(page); - await addProductToPickList(page, additionalProduct); - await expect(page.getByRole('button', { name: 'Save and Return' })).toBeEnabled(); }); @@ -63,53 +46,75 @@ test.describe('Active pick list', () => { await navigateToNewPickList(page); await page.getByRole('button', { name: 'Save and Return' }).click(); - await expect(page.getByRole('heading', { name: 'Pick Lists' })).toBeVisible(); + await waitForScreenHeading(page, 'Pick Lists'); await page.getByRole('link', { name: new RegExp(areaName, 'i') }).click(); - - await expect(page.getByRole('heading', { name: `${areaName} List` })).toBeVisible(); + await waitForScreenHeading(page, `${areaName} List`); await addProductToPickList(page, additionalProduct); - await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 }); + // Assert product row visible, and shows "1 x " + await expectProductQuantity(page, additionalProduct, 1, 15000); }); test('allows adding and editing products from the manage products screen', async ({ page }) => { + + await ensureCategoryExists(page, areaName); + await page.goto('/'); + await waitForScreenHeading(page, 'StockFill'); + + await page.getByRole('link', { name: 'Manage Products' }).click(); + await waitForScreenHeading(page, 'Manage Products'); await page.getByLabel('Name').click(); await page.getByLabel('Name').fill('Playwright Cola'); - await page.getByLabel('Add product category').click(); - await page.getByRole('option', { name: 'Drinks' }).click(); - await page.getByRole('button', { name: 'Save Product' }).click(); + + await openSelect(page, 'Add product category', 'Drinks', 'select-add-product-category'); + + const saveButton = page.getByRole('button', { name: 'Save Product' }); + if ((await saveButton.count()) > 0) { + await saveButton.first().click(); + } else { + await page.locator('[aria-label="Save product"]').first().click(); + } await expect(page.getByText('Product added.')).toBeVisible(); await expect(page.getByText('Playwright Cola')).toBeVisible(); await page.getByRole('button', { name: 'Edit Playwright Cola' }).click(); - const editNameField = page.getByLabel('Name').nth(1); await expect(editNameField).toBeVisible(); await editNameField.fill('Playwright Cola Zero'); - const saveButton = page.locator('[aria-label="Save product"]'); - await expect(saveButton).toBeEnabled(); - await saveButton.click(); + const saveIcon = page.locator('[aria-label="Save product"]').first(); + await expect(saveIcon).toBeVisible(); + await saveIcon.click(); await expect(page.getByText('Playwright Cola Zero')).toBeVisible(); }); test('filters the manage products list using the search bar', async ({ page }) => { + + await ensureCategoryExists(page, areaName); + await page.goto('/'); + await waitForScreenHeading(page, 'StockFill'); await page.getByRole('link', { name: 'Manage Products' }).click(); + await waitForScreenHeading(page, 'Manage Products'); await page.getByLabel('Name').click(); await page.getByLabel('Name').fill(listProductName); - await page.getByLabel('Add product category').click(); - await page.getByRole('option', { name: areaName }).click(); - await page.getByRole('button', { name: 'Save Product' }).click(); + await openSelect(page, 'Add product category', areaName, 'select-add-product-category'); + + const saveButton = page.getByRole('button', { name: 'Save Product' }); + if ((await saveButton.count()) > 0) { + await saveButton.first().click(); + } else { + await page.locator('[aria-label="Save product"]').first().click(); + } await expect(page.getByText('Product added.')).toBeVisible(); await expect(page.getByRole('button', { name: `Edit ${listProductName}` })).toBeVisible(); @@ -126,21 +131,17 @@ test.describe('Active pick list', () => { await expect(page.getByText('Product updated.')).toBeVisible(); await expect(page.getByRole('button', { name: `Edit ${updatedListProductName}` })).toBeVisible(); - await expect( - page.getByRole('button', { name: `Edit ${listProductName}`, exact: true }), - ).toHaveCount(0); + await expect(page.getByRole('button', { name: `Edit ${listProductName}`, exact: true })).toHaveCount(0); await page.getByRole('button', { name: `Delete ${updatedListProductName}` }).click(); - await expect(page.getByText('Product deleted.')).toBeVisible(); await expect(page.getByRole('button', { name: `Edit ${updatedListProductName}` })).toHaveCount(0); await expect(page.getByText(updatedListProductName)).toHaveCount(0); const addProduct = async (name: string) => { await page.getByLabel('Name').fill(name); - await page.getByLabel('Add product category').click(); - await page.getByRole('option', { name: areaName }).first().click(); - await page.getByRole('button', { name: 'Save Product' }).click(); + await openSelect(page, 'Add product category', areaName, 'select-add-product-category'); + await page.getByRole('button', { name: 'Save Product' }).first().click(); await expect(page.getByText('Product added.')).toBeVisible(); await expect(page.getByRole('button', { name: `Edit ${name}` })).toBeVisible(); }; @@ -189,24 +190,24 @@ test.describe('Active pick list', () => { await navigateToNewPickList(page); await addProductToPickList(page, additionalProduct); - const quantityLabel = page.getByText(/Qty:\s*1\s+unit/i); - await expect(quantityLabel).toBeVisible(); + // initial quantity = 1 + await expectProductQuantity(page, additionalProduct, 1); await page.getByRole('button', { name: 'Increase quantity' }).click(); - await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible({ timeout: 10000 }); + await expectProductQuantity(page, additionalProduct, 2); await page.reload(); - await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible(); + await expectProductQuantity(page, additionalProduct, 2); const decrementButton = page.getByRole('button', { name: 'Decrease quantity' }); await decrementButton.click(); await decrementButton.click(); await decrementButton.click(); - await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 }); + await expectProductQuantity(page, additionalProduct, 1); await page.reload(); - await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible(); + await expectProductQuantity(page, additionalProduct, 1); }); test('closes mobile controls with the close button and backdrop', async ({ page }) => { @@ -215,23 +216,29 @@ test.describe('Active pick list', () => { await navigateToNewPickList(page); await addProductToPickList(page, additionalProduct); - const productRow = page - .getByText(additionalProduct, { exact: true }) - .locator('xpath=ancestor::div[contains(@class, "MuiStack-root")]') - .first(); + // Find the title row which contains "1 x Pump 750" and then get its clickable ancestor (role="button") + const titleRow = page.locator('[data-testid="pick-item-title-row"]', { hasText: additionalProduct }).first(); + await titleRow.waitFor({ state: 'visible', timeout: 10000 }); + + // The clickable mobile row has role="button" (set only for narrow screens) + const productRow = titleRow.locator('xpath=ancestor::div[@role="button"]').first(); const controlsDialog = page.getByRole('dialog', { name: additionalProduct }); + // open controls by clicking the mobile row await productRow.click(); await expect(controlsDialog).toBeVisible(); + // close controls using the Close button (in the dialog header) await page.getByRole('button', { name: 'Close controls' }).click(); await expect(controlsDialog).toBeHidden(); + // re-open and close by clicking backdrop await productRow.click(); await expect(controlsDialog).toBeVisible(); await page.locator('.MuiBackdrop-root').click({ position: { x: 10, y: 10 }, force: true }); await expect(controlsDialog).toBeHidden(); + }); test('toggles picked visibility and disables the filter when all items are picked', async ({ page }) => { @@ -264,14 +271,6 @@ test.describe('Active pick list', () => { await expect(showPickedToggle).toBeChecked(); }); - test('omits global packaging filter controls', async ({ page }) => { - await navigateToNewPickList(page); - - await expect(page.getByText('Filter list by packaging')).toHaveCount(0); - await expect(page.getByRole('radio', { name: 'Cartons' })).toHaveCount(0); - await expect(page.getByRole('radio', { name: 'Units' })).toHaveCount(0); - }); - test('toggles packaging type and persists the selection', async ({ page }) => { await navigateToNewPickList(page); await addProductToPickList(page, additionalProduct); @@ -285,31 +284,38 @@ test.describe('Active pick list', () => { await expect(showPickedToggle).toBeChecked(); await expect(showPickedToggle).toBeDisabled(); + // increase quantity to 2 const increaseButton = page.getByLabel('Increase quantity'); await increaseButton.click(); - await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible({ timeout: 10000 }); + await expectProductQuantity(page, additionalProduct, 2); + // switch to carton packaging (icon button toggles packaging) await page.getByLabel('Switch to carton packaging').click(); - await expect(page.getByText(/Qty:\s*2\s+carton/i)).toBeVisible({ timeout: 10000 }); + // Desktop UI shows packaging via the switch icon state; assert the switch now allows switching back to unit + await expect(page.getByLabel('Switch to unit packaging')).toBeVisible(); + + // decrease and then check quantity await page.getByLabel('Decrease quantity').click(); - await expect(page.getByText(/Qty:\s*1\s+carton/i)).toBeVisible({ timeout: 10000 }); + await expectProductQuantity(page, additionalProduct, 1); + // switch back to unit packaging await page.getByLabel('Switch to unit packaging').click(); - await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 }); + await expect(page.getByLabel('Switch to carton packaging')).toBeVisible(); + // increase twice and then switch to carton and assert quantity 3 await increaseButton.click(); await increaseButton.click(); await page.getByLabel('Switch to carton packaging').click(); - await expect(page.getByText(/Qty:\s*3\s+carton/i)).toBeVisible({ timeout: 10000 }); + await expectProductQuantity(page, additionalProduct, 3); await page.getByRole('button', { name: 'Save and Return' }).click(); - await expect(page.getByRole('heading', { name: 'Pick Lists' })).toBeVisible(); + await waitForScreenHeading(page, 'Pick Lists'); await page.reload(); await page.getByRole('link', { name: areaName }).first().click(); - await expect(page.getByRole('heading', { name: `${areaName} List` })).toBeVisible(); - await expect(page.getByText(/Qty:\s*3\s+carton/i)).toBeVisible({ timeout: 10000 }); + await waitForScreenHeading(page, `${areaName} List`); + await expectProductQuantity(page, additionalProduct, 3); await expect(page.getByLabel('Switch to unit packaging')).toBeVisible(); }); @@ -340,12 +346,10 @@ test.describe('Active pick list', () => { await expect(showPickedToggle).toBeEnabled(); await itemStatusToggle.check(); - await expect(itemStatusToggle).toBeChecked(); await expect(showPickedToggle).toBeDisabled(); await itemStatusToggle.uncheck(); - await expect(itemStatusToggle).not.toBeChecked(); await expect(showPickedToggle).toBeEnabled(); }); @@ -385,47 +389,30 @@ test.describe('Active pick list', () => { await expect(page.getByText(additionalProduct)).toHaveCount(0); }); -}); -test.describe('Active pick list responsive controls on mobile', () => { - test.use({ viewport: { width: 430, height: 900 } }); + test('responsive controls on mobile show popup controls and hide inline buttons', async ({ page }) => { + // mobile viewport + await page.setViewportSize({ width: 390, height: 844 }); - test('shows popup controls on narrow screens and hides inline buttons', async ({ page }) => { await navigateToNewPickList(page); await addProductToPickList(page, additionalProduct); - const productRow = page - .getByText(additionalProduct) - .locator('xpath=ancestor::div[contains(@class, "MuiStack-root")]') - .first(); + // Find the title row which contains the product name, then find its mobile ancestor (role="button") + const titleRow = page.locator('[data-testid="pick-item-title-row"]', { hasText: additionalProduct }).first(); + await titleRow.waitFor({ state: 'visible', timeout: 10000 }); - await expect(productRow.getByLabel('Switch to carton packaging')).toHaveCount(0); + const productRow = titleRow.locator('xpath=ancestor::div[@role="button"]').first(); + + // On mobile the open-controls icon should be visible and inline increase/decrease buttons hidden + await expect(productRow.getByLabel('Open item controls')).toBeVisible(); await expect(productRow.getByLabel('Decrease quantity')).toHaveCount(0); await expect(productRow.getByLabel('Increase quantity')).toHaveCount(0); - await expect(productRow.getByLabel('Delete item')).toHaveCount(0); - const controlsButton = productRow.getByLabel('Open item controls'); - await expect(controlsButton).toBeVisible(); - await controlsButton.click(); - - const itemDialog = page.getByRole('dialog', { name: additionalProduct }); - await expect(itemDialog).toBeVisible(); - const quantityDisplay = itemDialog.getByText(/Quantity/i); - - await expect(quantityDisplay).toHaveText(/Quantity:\s*1\s+unit/i, { timeout: 10000 }); - await expect(itemDialog.getByLabel('Increase quantity')).toBeVisible(); - - await itemDialog.getByLabel('Increase quantity').click(); - await expect(quantityDisplay).toHaveText(/Quantity:\s*2\s+unit/i, { timeout: 10000 }); - await itemDialog.getByLabel('Switch to carton packaging').click(); - await expect(quantityDisplay).toHaveText(/Quantity:\s*2\s+carton/i, { timeout: 10000 }); - - await itemDialog.getByLabel('Delete item').click(); - await expect(page.getByRole('dialog', { name: 'Delete item' })).toBeVisible(); - await page.getByRole('button', { name: 'Cancel', exact: true }).click(); - await expect(page.getByRole('dialog', { name: 'Delete item' })).toBeHidden(); - - await controlsButton.click(); - await expect(itemDialog).toBeVisible(); + // open popup controls by clicking the mobile row, then assert dialog contents + await productRow.click(); + const dialog = page.getByRole('dialog', { name: additionalProduct }); + await expect(dialog).toBeVisible(); + await expect(dialog.getByLabel('Increase quantity')).toBeVisible(); }); + }); diff --git a/e2e/product-filter.spec.ts b/e2e/product-filter.spec.ts index 15111fc..12c4c0a 100644 --- a/e2e/product-filter.spec.ts +++ b/e2e/product-filter.spec.ts @@ -1,18 +1,34 @@ import { expect, test } from '@playwright/test'; +import { + ensureProductsExist, + areaName, + waitForScreenHeading, + openSelect, +} from './test-helpers'; test.describe('Product category filters', () => { test('shows only products from the selected category', async ({ page }) => { + // Ensure products exist in the categories we will test. + // ensureProductsExist also ensures the category exists. + await ensureProductsExist(page, ['Smiths Salt n Vinegar 90g'], 'Chips'); + await ensureProductsExist(page, ['Pump 750'], areaName); // Drinks + await ensureProductsExist(page, ['Mars Bar'], 'Confectionery'); + + // Go to Manage Products await page.goto('/'); + await waitForScreenHeading(page, 'StockFill'); await page.getByRole('link', { name: 'Manage Products' }).click(); + await waitForScreenHeading(page, 'Manage Products'); - const categoryFilter = page.getByLabel('Filter by category'); - await expect(categoryFilter).toBeVisible(); + // Use robust helper to open the category filter and select 'Chips' + await openSelect(page, 'Filter by category', 'Chips'); - await categoryFilter.click(); - await page.getByRole('option', { name: 'Chips' }).click(); + // Wait briefly and assert only Chips products are shown + await expect( + page.getByRole('button', { name: 'Edit Smiths Salt n Vinegar 90g' }).first() + ).toBeVisible({ timeout: 15000 }); - await expect(page.getByRole('button', { name: 'Edit Smiths Salt n Vinegar 90g' }).first()).toBeVisible(); await expect(page.getByRole('button', { name: 'Edit Pump 750' })).toHaveCount(0); await expect(page.getByRole('button', { name: 'Edit Mars Bar' })).toHaveCount(0); }); diff --git a/e2e/test-helpers.ts b/e2e/test-helpers.ts new file mode 100644 index 0000000..e6ba769 --- /dev/null +++ b/e2e/test-helpers.ts @@ -0,0 +1,326 @@ +// e2e/test-helpers.ts +import { expect, Page } from '@playwright/test'; + +/* ----------------------- + Shared constants + ----------------------- */ +export const areaName = 'Drinks'; +export const additionalProduct = 'Pump 750'; +export const listProductName = 'List Flow Soda'; +export const updatedListProductName = 'List Flow Soda Zero'; +export const searchTerm = 'E2E Search'; +export const matchingProducts = [`${searchTerm} Alpha`, `${searchTerm} Beta`]; +export const nonMatchingProduct = 'E2E Other Gamma'; +export const secondaryProduct = 'Mount Franklin 600ml'; + +/* ----------------------- + Small helpers + ----------------------- */ +export const waitForScreenHeading = async (page: Page, heading: string, timeout = 15000) => { + try { + await page.locator('text=Loading…').waitFor({ state: 'hidden', timeout: 5000 }); + } catch { + // ignore if no fallback + } + await expect(page.getByRole('heading', { name: heading })).toBeVisible({ timeout }); +}; + +export const openSelect = async (page: Page, label: string, optionName: string, testId?: string) => { + const tryOpen = async (lbl: string) => { + // 1) Try testId wrapper if provided + if (testId) { + const wrapper = page.getByTestId(testId); + if ((await wrapper.count()) > 0) { + await wrapper.first().waitFor({ state: 'visible', timeout: 10000 }); + + // native