import { test, expect, type Page, type Route } from '@playwright/test'; /** * SETTINGS-35 UI contract against vite preview (no /api proxy). * Taxonomy/pricing are Playwright mocks with fixture ids only — never live UAT * store ids (QUALITY-105 / METADATA-5). */ const SETTINGS = { woo_base_url: 'https://example.invalid', woo_wp_username: 'wpuser', woo_app_password: { configured: true }, inference_base_url: { configured: false }, inference_api_key: { configured: false }, inference_model: null, inference_enabled: false, content_llm_enabled: false, brand_voice: null, geo_context: null, woo_create_status: 'draft', }; /** Synthetic parented tree. Fixture ids 10/11/12/20/21/99 — not production. */ const PARENTED_TAXONOMY = { categories: [ { id: 10, name: 'Species', slug: 'species', parent_id: null }, { id: 11, name: 'Cedar', slug: 'cedar', parent_id: 10 }, { id: 12, name: 'Maple', slug: 'maple', parent_id: 10 }, { id: 20, name: 'Wood category', slug: 'wcat', parent_id: null }, { id: 21, name: 'Live edge wood', slug: 'liveedge', parent_id: 20 }, { id: 99, name: 'Chairs', slug: 'chairs', parent_id: null }, ], attributes: [], tags: [], taxonomy_anchor: 'fixture', taxonomy_last_sync_at: '2026-01-01T00:00:00', }; async function json(route: Route, body: unknown, status = 200) { await route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body), }); } type ApiOpts = { taxonomy?: unknown; pricingGet?: () => unknown; onPricingPost?: (route: Route, payload: Record) => Promise; onPricingPut?: (route: Route, id: string, payload: Record) => Promise; }; async function mockApi(page: Page, opts: ApiOpts) { await page.route('**/api/v1/**', async (route) => { const req = route.request(); const url = new URL(req.url()); const path = url.pathname; const method = req.method(); const payload = (() => { try { return req.postDataJSON() as Record; } catch { return {}; } })(); if (path === '/api/v1/settings' && method === 'GET') { await json(route, SETTINGS); return; } if (path === '/api/v1/admin/taxonomy' && method === 'GET') { await json(route, opts.taxonomy ?? { categories: [] }); return; } const putMatch = path.match(/^\/api\/v1\/admin\/pricing\/(\d+)$/); if (putMatch && method === 'PUT') { if (opts.onPricingPut) { await opts.onPricingPut(route, putMatch[1], payload); return; } await json(route, { error: { code: 'not_found', message: 'rule not found' } }, 404); return; } if (path === '/api/v1/admin/pricing' && method === 'GET') { const list = opts.pricingGet ? opts.pricingGet() : []; await json(route, list); return; } if (path === '/api/v1/admin/pricing' && method === 'POST') { if (opts.onPricingPost) { await opts.onPricingPost(route, payload); return; } await json( route, { id: 601, species_id: payload.species_id, price_per_bdft: payload.price_per_bdft, }, 201 ); return; } await json(route, { error: { code: 'not_found', message: path } }, 404); }); } async function openSettings(page: Page) { await page.goto('/'); await page.getByRole('button', { name: 'Settings (dev)' }).click(); await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible(); await expect(page.getByTestId('species-pricing')).toBeVisible(); } test.describe('Settings species pricing (SETTINGS-35)', () => { test('empty taxonomy: prompt only, no seeded species rows', async ({ page }) => { await mockApi(page, { taxonomy: { categories: [] } }); await openSettings(page); const empty = page.getByTestId('species-pricing-empty'); await expect(empty).toBeVisible(); await expect(empty).toContainText(/Test WooCommerce/i); await expect(page.locator('.rate-row')).toHaveCount(0); await expect(page.getByTestId('save-rates')).toHaveCount(0); await expect(page.getByText('Cedar', { exact: true })).toHaveCount(0); await expect(page.getByText('Maple', { exact: true })).toHaveCount(0); }); test('lists only Species-grouper children (not wood cats / parents / furniture)', async ({ page, }) => { await mockApi(page, { taxonomy: PARENTED_TAXONOMY, pricingGet: () => [{ id: 501, species_id: 11, price_per_bdft: 8.25 }], }); await openSettings(page); await expect(page.locator('.rate-row')).toHaveCount(2); await expect(page.locator('[data-species-slug="cedar"]')).toBeVisible(); await expect(page.locator('[data-species-slug="maple"]')).toBeVisible(); await expect(page.getByLabel('Price per bdft for Cedar')).toHaveValue('8.25'); await expect(page.getByLabel('Price per bdft for Maple')).toHaveValue(''); await expect(page.locator('[data-species-slug="liveedge"]')).toHaveCount(0); await expect(page.locator('[data-species-slug="chairs"]')).toHaveCount(0); await expect(page.locator('[data-species-slug="species"]')).toHaveCount(0); await expect(page.locator('[data-species-slug="wcat"]')).toHaveCount(0); await expect(page.getByText('Live edge wood')).toHaveCount(0); await expect(page.getByText('Chairs', { exact: true })).toHaveCount(0); }); test('Save rates POSTs a new rule', async ({ page }) => { const posts: Record[] = []; await mockApi(page, { taxonomy: PARENTED_TAXONOMY, onPricingPost: async (route, payload) => { posts.push(payload); await json( route, { id: 601, species_id: payload.species_id, price_per_bdft: payload.price_per_bdft }, 201 ); }, }); await openSettings(page); await page.getByLabel('Price per bdft for Cedar').fill('12.5'); await page.getByTestId('save-rates').click(); await expect(page.getByText('Saved 1 rate')).toBeVisible(); expect(posts).toEqual([{ species_id: 11, price_per_bdft: 12.5 }]); }); test('Save rates PUT after POST 409', async ({ page }) => { const posts: Record[] = []; const puts: { id: string; payload: Record }[] = []; let sawConflict = false; await mockApi(page, { taxonomy: PARENTED_TAXONOMY, pricingGet: () => sawConflict ? [{ id: 501, species_id: 12, price_per_bdft: 8 }] : [], onPricingPost: async (route, payload) => { posts.push(payload); sawConflict = true; await json( route, { error: { code: 'duplicate', message: 'rule exists for species', detail: { species_id: payload.species_id }, }, }, 409 ); }, onPricingPut: async (route, id, payload) => { puts.push({ id, payload }); await json(route, { id: Number(id), species_id: 12, price_per_bdft: payload.price_per_bdft, }); }, }); await openSettings(page); await page.getByLabel('Price per bdft for Maple').fill('9.75'); await page.getByTestId('save-rates').click(); await expect(page.getByText('Saved 1 rate')).toBeVisible(); expect(posts).toEqual([{ species_id: 12, price_per_bdft: 9.75 }]); expect(puts).toEqual([{ id: '501', payload: { price_per_bdft: 9.75 } }]); }); });