test(intake): cover reviewing and publishing a submission (#225)
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. Going through the link is also the path a real submission takes, so the test exercises what actually ships. Two cases. The first edits the price and publishes, which confirms the number, so no dialog appears and the item reaches the storefront at what was typed. The second publishes an untouched price and asserts the confirmation names the figure and the fact that nobody chose it, then cancels and checks the item is still unconfirmed. That second case is the protection this whole screen exists to provide. Scoped to the card each test creates, located by the sender's note carrying the run id — the item's own name is a submission timestamp and not unique to the test. The dialog is matched on its accessible name because antd nests the confirm title in two elements and getByText resolves to both. Full suite 157 of 157, up from 155. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
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<void> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user