Harden pick list e2e product selection waits

This commit is contained in:
beatz174-bit
2025-11-24 09:45:58 +10:00
parent 8c80c14f3b
commit cf994b876a
2 changed files with 182 additions and 118 deletions
+71 -14
View File
@@ -25,15 +25,18 @@ const addProductToPickList = async (page: Page, productName: string) => {
const searchInput = page.getByPlaceholder('Search products'); const searchInput = page.getByPlaceholder('Search products');
await searchInput.click(); await searchInput.click();
await searchInput.fill(productName); await searchInput.fill(productName);
await page const productOption = page
.getByRole('option', { name: new RegExp(`${productName} \\(${areaName}\\)`, 'i') }) .getByRole('option', { name: new RegExp(`${productName} \\(${areaName}\\)`, 'i') })
.first() .first();
.click();
await expect(page.getByText(productName).first()).toBeVisible(); await expect(productOption).toBeVisible({ timeout: 15000 });
await productOption.click();
await expect(page.getByText(productName).first()).toBeVisible({ timeout: 15000 });
}; };
test.describe('Active pick list', () => { 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 }) => { test('adds a product via search and renders it with quantity and packaging', async ({ page }) => {
await navigateToNewPickList(page); await navigateToNewPickList(page);
@@ -45,7 +48,7 @@ test.describe('Active pick list', () => {
.click(); .click();
await expect(page.getByText(secondaryProduct, { exact: true })).toBeVisible(); await expect(page.getByText(secondaryProduct, { exact: true })).toBeVisible();
await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 });
}); });
test('creates a pick list and adds products from the search bar', async ({ page }) => { test('creates a pick list and adds products from the search bar', async ({ page }) => {
@@ -67,7 +70,7 @@ test.describe('Active pick list', () => {
await addProductToPickList(page, additionalProduct); await addProductToPickList(page, additionalProduct);
await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 });
}); });
test('allows adding and editing products from the manage products screen', async ({ page }) => { test('allows adding and editing products from the manage products screen', async ({ page }) => {
@@ -190,7 +193,7 @@ test.describe('Active pick list', () => {
await expect(quantityLabel).toBeVisible(); await expect(quantityLabel).toBeVisible();
await page.getByRole('button', { name: 'Increase quantity' }).click(); await page.getByRole('button', { name: 'Increase quantity' }).click();
await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible({ timeout: 10000 });
await page.reload(); await page.reload();
await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible();
@@ -200,7 +203,7 @@ test.describe('Active pick list', () => {
await decrementButton.click(); await decrementButton.click();
await decrementButton.click(); await decrementButton.click();
await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 });
await page.reload(); await page.reload();
await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible();
@@ -251,21 +254,21 @@ test.describe('Active pick list', () => {
const increaseButton = page.getByLabel('Increase quantity'); const increaseButton = page.getByLabel('Increase quantity');
await increaseButton.click(); await increaseButton.click();
await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*2\s+unit/i)).toBeVisible({ timeout: 10000 });
await page.getByLabel('Switch to carton packaging').click(); await page.getByLabel('Switch to carton packaging').click();
await expect(page.getByText(/Qty:\s*2\s+carton/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*2\s+carton/i)).toBeVisible({ timeout: 10000 });
await page.getByLabel('Decrease quantity').click(); await page.getByLabel('Decrease quantity').click();
await expect(page.getByText(/Qty:\s*1\s+carton/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*1\s+carton/i)).toBeVisible({ timeout: 10000 });
await page.getByLabel('Switch to unit packaging').click(); await page.getByLabel('Switch to unit packaging').click();
await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 });
await increaseButton.click(); await increaseButton.click();
await increaseButton.click(); await increaseButton.click();
await page.getByLabel('Switch to carton packaging').click(); await page.getByLabel('Switch to carton packaging').click();
await expect(page.getByText(/Qty:\s*3\s+carton/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*3\s+carton/i)).toBeVisible({ timeout: 10000 });
await page.getByRole('button', { name: 'Save and Return' }).click(); await page.getByRole('button', { name: 'Save and Return' }).click();
await expect(page.getByRole('heading', { name: 'Pick Lists' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Pick Lists' })).toBeVisible();
@@ -273,10 +276,26 @@ test.describe('Active pick list', () => {
await page.reload(); await page.reload();
await page.getByRole('link', { name: areaName }).first().click(); await page.getByRole('link', { name: areaName }).first().click();
await expect(page.getByRole('heading', { name: `${areaName} List` })).toBeVisible(); await expect(page.getByRole('heading', { name: `${areaName} List` })).toBeVisible();
await expect(page.getByText(/Qty:\s*3\s+carton/i)).toBeVisible(); await expect(page.getByText(/Qty:\s*3\s+carton/i)).toBeVisible({ timeout: 10000 });
await expect(page.getByLabel('Switch to unit packaging')).toBeVisible(); await expect(page.getByLabel('Switch to unit packaging')).toBeVisible();
}); });
test('shows inline controls on wide view without the popup trigger', async ({ page }) => {
await navigateToNewPickList(page);
await addProductToPickList(page, additionalProduct);
const productRow = page
.getByText(additionalProduct)
.locator('xpath=ancestor::div[contains(@class, "MuiStack-root")]')
.first();
await expect(productRow.getByLabel('Switch to carton packaging')).toBeVisible();
await expect(productRow.getByLabel('Decrease quantity')).toBeVisible();
await expect(productRow.getByLabel('Increase quantity')).toBeVisible();
await expect(productRow.getByLabel('Delete item')).toBeVisible();
await expect(productRow.getByLabel('Open item controls')).toHaveCount(0);
});
test('lets a user mark an item as picked then revert it back to pending', async ({ page }) => { test('lets a user mark an item as picked then revert it back to pending', async ({ page }) => {
await navigateToNewPickList(page); await navigateToNewPickList(page);
await addProductToPickList(page, additionalProduct); await addProductToPickList(page, additionalProduct);
@@ -316,3 +335,41 @@ test.describe('Active pick list', () => {
await expect(page.getByText(additionalProduct).first()).not.toBeVisible(); await expect(page.getByText(additionalProduct).first()).not.toBeVisible();
}); });
}); });
test.describe('Active pick list responsive controls on mobile', () => {
test.use({ viewport: { width: 430, height: 900 } });
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();
await expect(productRow.getByLabel('Switch to carton packaging')).toHaveCount(0);
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();
await expect(itemDialog.getByText(/Quantity:\s*1\s+unit/i)).toBeVisible({ timeout: 10000 });
await expect(itemDialog.getByLabel('Increase quantity')).toBeVisible();
await itemDialog.getByLabel('Increase quantity').click();
await expect(itemDialog.getByText(/Quantity:\s*2\s+unit/i)).toBeVisible({ timeout: 10000 });
await itemDialog.getByLabel('Switch to carton packaging').click();
await expect(itemDialog.getByText(/Quantity:\s*2\s+carton/i)).toBeVisible({ 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: additionalProduct })).toBeVisible();
});
});
+83 -76
View File
@@ -1,4 +1,4 @@
import { Add, Close, Delete, Inventory2, Remove } from '@mui/icons-material'; import { Add, Close, Delete, Inventory2, MoreHoriz, Remove } from '@mui/icons-material';
import { import {
Button, Button,
Checkbox, Checkbox,
@@ -11,10 +11,11 @@ import {
Stack, Stack,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { alpha } from '@mui/material/styles'; import { alpha, useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useState } from 'react'; import { useState } from 'react';
import { PickItem } from '../models/PickItem'; import { PickItem } from '../models/PickItem';
import { Product } from '../models/Product'; import { DEFAULT_BULK_NAME, DEFAULT_UNIT_TYPE, Product } from '../models/Product';
interface PickItemRowProps { interface PickItemRowProps {
item: PickItem; item: PickItem;
@@ -37,9 +38,11 @@ export const PickItemRow = ({
}: PickItemRowProps) => { }: PickItemRowProps) => {
const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [isControlsOpen, setIsControlsOpen] = useState(false); const [isControlsOpen, setIsControlsOpen] = useState(false);
const theme = useTheme();
const isDesktop = useMediaQuery(theme.breakpoints.up('sm'));
const packagingLabel = item.is_carton const packagingLabel = item.is_carton
? product?.bulk_name ?? 'Carton' ? product?.bulk_name ?? DEFAULT_BULK_NAME
: product?.unit_type ?? 'Unit'; : product?.unit_type ?? DEFAULT_UNIT_TYPE;
const toggleStatus = (checked: boolean) => { const toggleStatus = (checked: boolean) => {
onStatusChange(checked ? 'picked' : 'pending'); onStatusChange(checked ? 'picked' : 'pending');
@@ -50,6 +53,8 @@ export const PickItemRow = ({
setIsConfirmOpen(false); setIsConfirmOpen(false);
}; };
const openControls = () => setIsControlsOpen(true);
return ( return (
<Stack <Stack
direction="row" direction="row"
@@ -57,21 +62,25 @@ export const PickItemRow = ({
justifyContent="space-between" justifyContent="space-between"
spacing={1.5} spacing={1.5}
sx={{ sx={{
p: 1, p: 1.5,
borderRadius: 1, borderRadius: 1,
bgcolor: 'background.paper', bgcolor: 'background.paper',
boxShadow: 1, boxShadow: 1,
cursor: 'pointer', cursor: isDesktop ? 'default' : 'pointer',
}} }}
onClick={() => setIsControlsOpen(true)} {...(!isDesktop
role="button" ? {
tabIndex={0} role: 'button',
onKeyDown={(event) => { tabIndex: 0,
onClick: openControls,
onKeyDown: (event) => {
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault(); event.preventDefault();
setIsControlsOpen(true); openControls();
} }
}} },
}
: undefined)}
> >
<Stack direction="row" spacing={1} alignItems="center" flex={1} minWidth={0}> <Stack direction="row" spacing={1} alignItems="center" flex={1} minWidth={0}>
<Checkbox <Checkbox
@@ -79,60 +88,18 @@ export const PickItemRow = ({
checked={item.status === 'picked'} checked={item.status === 'picked'}
onChange={(event) => toggleStatus(event.target.checked)} onChange={(event) => toggleStatus(event.target.checked)}
inputProps={{ 'aria-label': 'Toggle picked status' }} inputProps={{ 'aria-label': 'Toggle picked status' }}
onClick={(event) => event.stopPropagation()}
/> />
<Stack spacing={0.5} minWidth={0} flex={1}> <Stack spacing={0.25} minWidth={0} flex={1}>
<Typography <Typography variant="subtitle1" noWrap sx={{ minWidth: 0 }}>
variant="subtitle1"
noWrap
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
>
<Typography
component="span"
variant="subtitle1"
noWrap
sx={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{product?.name ?? 'Unknown product'} {product?.name ?? 'Unknown product'}
</Typography> </Typography>
<Typography <Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600 }}>
component="span" {`Qty: ${item.quantity} ${packagingLabel}`}
variant="subtitle1"
sx={{ fontWeight: 700, whiteSpace: 'nowrap' }}
>
{item.quantity} {packagingLabel}
</Typography>
</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
Tap to adjust quantity and packaging
</Typography> </Typography>
</Stack> </Stack>
</Stack> </Stack>
{isDesktop ? (
<Dialog <Stack direction="row" spacing={0.5} alignItems="center" flexWrap="wrap" justifyContent="flex-end">
open={isControlsOpen}
onClose={() => setIsControlsOpen(false)}
fullWidth
maxWidth="xs"
aria-labelledby="item-controls-title"
>
<DialogTitle
id="item-controls-title"
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
>
<Typography variant="h6" noWrap sx={{ minWidth: 0, flex: 1 }}>
{product?.name ?? 'Unknown product'}
</Typography>
<IconButton aria-label="Close controls" onClick={() => setIsControlsOpen(false)}>
<Close />
</IconButton>
</DialogTitle>
<DialogContent>
<Stack spacing={2} alignItems="stretch">
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
Quantity: {item.quantity} {packagingLabel}
</Typography>
<Stack direction="row" spacing={1} justifyContent="center" flexWrap="wrap">
<IconButton <IconButton
aria-label={`Switch to ${item.is_carton ? 'unit' : 'carton'} packaging`} aria-label={`Switch to ${item.is_carton ? 'unit' : 'carton'} packaging`}
color={item.is_carton ? 'primary' : 'default'} color={item.is_carton ? 'primary' : 'default'}
@@ -145,31 +112,75 @@ export const PickItemRow = ({
> >
<Inventory2 /> <Inventory2 />
</IconButton> </IconButton>
<IconButton <IconButton aria-label="Decrease quantity" color="primary" onClick={onDecrementQuantity}>
aria-label="Decrease quantity"
color="primary"
onClick={onDecrementQuantity}
>
<Remove /> <Remove />
</IconButton> </IconButton>
<IconButton <IconButton aria-label="Increase quantity" color="primary" onClick={onIncrementQuantity}>
aria-label="Increase quantity"
color="primary"
onClick={onIncrementQuantity}
>
<Add /> <Add />
</IconButton> </IconButton>
<IconButton <IconButton
color="error" color="error"
onClick={() => setIsConfirmOpen(true)} onClick={() => setIsConfirmOpen(true)}
aria-label="Delete item" aria-label="Delete item"
sx={{ ml: { xs: 0, sm: 1 } }}
> >
<Delete /> <Delete />
</IconButton> </IconButton>
</Stack> </Stack>
) : (
<IconButton aria-label="Open item controls" onClick={openControls}>
<MoreHoriz />
</IconButton>
)}
{!isDesktop && (
<Dialog
open={isControlsOpen}
onClose={() => setIsControlsOpen(false)}
fullWidth
maxWidth="xs"
aria-labelledby="item-controls-title"
>
<DialogTitle id="item-controls-title" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h6" noWrap sx={{ minWidth: 0, flex: 1 }}>
{product?.name ?? 'Unknown product'}
</Typography>
<IconButton aria-label="Close controls" onClick={() => setIsControlsOpen(false)}>
<Close />
</IconButton>
</DialogTitle>
<DialogContent>
<Stack spacing={2} alignItems="stretch">
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
{`Quantity: ${item.quantity} ${packagingLabel}`}
</Typography>
<Stack direction="row" spacing={1} justifyContent="center" flexWrap="wrap">
<IconButton
aria-label={`Switch to ${item.is_carton ? 'unit' : 'carton'} packaging`}
color={item.is_carton ? 'primary' : 'default'}
onClick={onToggleCarton}
sx={{
boxShadow: item.is_carton
? (dialogTheme) => `0 0 0 8px ${alpha(dialogTheme.palette.primary.main, 0.15)}`
: 'none',
}}
>
<Inventory2 />
</IconButton>
<IconButton aria-label="Decrease quantity" color="primary" onClick={onDecrementQuantity}>
<Remove />
</IconButton>
<IconButton aria-label="Increase quantity" color="primary" onClick={onIncrementQuantity}>
<Add />
</IconButton>
<IconButton color="error" onClick={() => setIsConfirmOpen(true)} aria-label="Delete item">
<Delete />
</IconButton>
</Stack>
</Stack> </Stack>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)}
<Dialog <Dialog
open={isConfirmOpen} open={isConfirmOpen}
@@ -187,11 +198,7 @@ export const PickItemRow = ({
<Button onClick={() => setIsConfirmOpen(false)} aria-label="Cancel delete" autoFocus> <Button onClick={() => setIsConfirmOpen(false)} aria-label="Cancel delete" autoFocus>
Cancel Cancel
</Button> </Button>
<Button <Button color="error" variant="contained" onClick={handleConfirmDelete}>
color="error"
variant="contained"
onClick={handleConfirmDelete}
>
Delete Delete
</Button> </Button>
</DialogActions> </DialogActions>