import { test, expect, uniqueSuffix, findOrFail, createAdminContext } from './fixtures'; const RUN = uniqueSuffix(); // The 1x1 PNG the other upload specs use, so this exercises the real validated // upload path rather than a buffer that merely starts correctly. const PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64' ); let token: string; test.beforeAll(async ({ playwright }) => { const api = await createAdminContext(playwright); const res = await api.post('/api/admin/upload-links', { data: { label: `Review queue spec ${RUN}` } }); expect(res.status(), 'creating the upload link').toBe(201); token = (await res.json()).token; await api.dispose(); }); /** * Seeded through the intake route rather than POST /api/admin/items, which * writes no item_drafts row — an item created that way would never appear in a * queue that joins that table. This is also the path a real submission takes. */ async function submitAnItem(page: import('@playwright/test').Page, note: string): Promise { await page.goto(`/submit/${token}`); await page.setInputFiles('input[type="file"]', { name: `${RUN}.png`, mimeType: 'image/png', buffer: PNG }); await page.getByLabel('Anything you know about this item').fill(note); await page.getByRole('button', { name: 'Send' }).click(); await expect(page.getByRole('heading', { name: 'Thank you — it arrived' })).toBeVisible(); } // Every assertion is scoped to the card this test created. The dev database // never truncates, so a queue-wide assertion outruns its timeout and fails for // reasons unrelated to the behaviour under test (#241). test.describe('The review queue', () => { test('publishes a submitted item at an edited price', async ({ page, admin }) => { const note = `Stoneware ${RUN}`; await submitAnItem(page, note); await admin.open('Review queue'); // Located by the sender's note, which carries this run's id. The item's own // name is a submission timestamp and is not unique to this test. const card = page.locator('.ant-card').filter({ hasText: note }); await expect(card).toBeVisible(); // The provenance warning is the entire point of the screen. With no API key // configured the draft stays queued at the 8000 default, so this is the // default wording rather than the model's. await expect(card.getByText(/nobody chose this/)).toBeVisible(); await card.getByLabel('Name').fill(`Blue vase ${RUN}`); await card.getByLabel('Price').fill('95'); await card.getByRole('button', { name: 'Publish', exact: true }).click(); // Editing the price confirms it, so no confirmation dialog appears and the // item simply publishes. await expect(card.getByText('you set this price')).toBeVisible(); const items = await (await page.request.get('/api/admin/items')).json(); const published = findOrFail( items as { id: number; name: string; status: string; price_cents: number }[], (item) => item.name === `Blue vase ${RUN}`, `the published item Blue vase ${RUN}` ); expect(published.status).toBe('available'); expect(published.price_cents).toBe(9500); }); // The protection this screen exists to provide. Publishing at a price nobody // chose is allowed — but it must be a decision, not an accident. test('asks before publishing at a price nobody chose', async ({ page, admin }) => { const note = `Unpriced ${RUN}`; await submitAnItem(page, note); await admin.open('Review queue'); const card = page.locator('.ant-card').filter({ hasText: note }); await expect(card).toBeVisible(); await card.getByRole('button', { name: 'Publish', exact: true }).click(); // Matched on the dialog's accessible name rather than its text: antd nests // the confirm title in two elements, so getByText resolves to both. const dialog = page.getByRole('dialog', { name: 'Publish at a price nobody chose?' }); await expect(dialog).toBeVisible(); await expect(dialog.getByText('$80.00')).toBeVisible(); // Backing out must leave it unpublished. await dialog.getByRole('button', { name: 'Cancel', exact: true }).click(); await expect(card.getByText(/nobody chose this/)).toBeVisible(); }); });