import { expect } from '@playwright/test'; import { Given, When, Then } from 'playwright-bdd/decorators'; import { createBdd } from 'playwright-bdd'; import { waitForAppReady, seedItems, seedAndNavigate, clearItems } from '../support/seed'; import { buildItem, buildCheckedOutItem, buildConsumableItem, buildItemAtLocation, resetCounter } from '../support/item-factory'; import { assertMinTouchTarget, assertWithinViewport } from '../support/layout'; const { Given: given, When: when, Then: then } = createBdd(); // --- Navigation --- given('I am on the {string} page', async ({ page }, path: string) => { const routeMap: Record = { dashboard: '/', items: '/items', 'new item': '/items/new', scan: '/scan', locations: '/locations', labels: '/labels', settings: '/settings', }; const url = routeMap[path] ?? path; await page.goto(url); await waitForAppReady(page); }); given('I navigate to {string}', async ({ page }, path: string) => { await page.goto(path); await waitForAppReady(page); }); // --- Seeding --- given('the inventory is empty', async ({ page }) => { await page.goto('/'); await waitForAppReady(page); await clearItems(page); await page.reload(); await waitForAppReady(page); }); given('there are {int} items in the inventory', async ({ page }, count: number) => { resetCounter(); const items = Array.from({ length: count }, (_, i) => buildItem({ name: `Item ${i + 1}`, category: i % 2 === 0 ? 'Electronics' : 'Kitchen' }) ); await page.goto('/'); await waitForAppReady(page); await seedItems(page, items); }); given('there is an item {string} in category {string}', async ({ page }, name: string, category: string) => { const item = buildItem({ name, category }); await page.goto('/'); await waitForAppReady(page); await seedItems(page, [item]); }); given('there is a checked-out item {string}', async ({ page }, name: string) => { const item = buildCheckedOutItem({ name }); await page.goto('/'); await waitForAppReady(page); await seedItems(page, [item]); }); given('there is a consumable item {string} with {int} of {int} remaining', async ({ page }, name: string, current: number, original: number) => { const item = buildConsumableItem({ name, currentQuantity: current, originalQuantity: original, lowThreshold: Math.floor(original * 0.3) }); await page.goto('/'); await waitForAppReady(page); await seedItems(page, [item]); }); given('there is an item {string} at location {string}', async ({ page }, name: string, locationId: string) => { const item = buildItemAtLocation(locationId, { name }); await page.goto('/'); await waitForAppReady(page); await seedItems(page, [item]); }); // --- Interaction --- when('I click {string}', async ({ page }, text: string) => { await page.getByRole('button', { name: text }).or(page.getByRole('link', { name: text })).click(); }); when('I click the {string} button', async ({ page }, text: string) => { await page.getByRole('button', { name: text }).click(); }); when('I click the {string} link', async ({ page }, text: string) => { await page.getByRole('link', { name: text }).click(); }); when('I fill in {string} with {string}', async ({ page }, label: string, value: string) => { await page.getByLabel(label).fill(value); }); when('I select {string} from {string}', async ({ page }, value: string, label: string) => { await page.getByLabel(label).selectOption(value); }); when('I type {string} into the search field', async ({ page }, text: string) => { await page.getByPlaceholder('Search items...').fill(text); }); // --- Assertions --- then('I should see {string}', async ({ page }, text: string) => { await expect(page.getByText(text, { exact: false })).toBeVisible(); }); then('I should not see {string}', async ({ page }, text: string) => { await expect(page.getByText(text, { exact: false })).not.toBeVisible(); }); then('I should be on {string}', async ({ page }, path: string) => { await expect(page).toHaveURL(new RegExp(path)); }); then('the page title should be {string}', async ({ page }, title: string) => { await expect(page.getByRole('heading', { level: 1 })).toHaveText(title); }); // --- Visual --- then('the page should match the screenshot {string}', async ({ page }, name: string) => { await expect(page).toHaveScreenshot(`${name}.png`); }); // --- Layout --- then('all nav buttons should be touch-target sized', async ({ page }) => { const navLinks = page.locator('nav a'); const count = await navLinks.count(); for (let i = 0; i < count; i++) { await assertMinTouchTarget(navLinks.nth(i)); } }); then('the bottom nav should be within the viewport', async ({ page }) => { const nav = page.locator('nav'); await assertWithinViewport(nav, page); });