checkout-request-shape.spec.ts and checkout-idempotent-click.spec.ts were
flagged known-failing pending investigation; dda0a3d found and fixed the
actual cause (circular DI in apiHeadersInterceptor). Update the comments
and README so they no longer point at an unresolved mystery.
79 lines
3.8 KiB
TypeScript
79 lines
3.8 KiB
TypeScript
import { Page, Route, expect, test } from '@playwright/test';
|
|
|
|
/**
|
|
* Track Q Q5 / frontend backlog F62: "repeat webhook and double-click create
|
|
* exactly one order." The webhook-idempotency half is a backend contract
|
|
* (PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §6.3, provider + providerEventId as
|
|
* the dedup key) this suite cannot exercise without a live backend. This
|
|
* test covers the half that IS frontend-testable: a double-click on the
|
|
* checkout button must not fire two checkout-session requests.
|
|
*/
|
|
|
|
const FAKE_ITEM = {
|
|
categoryID: 1, itemID: 5151, name: 'Idempotency Test Item', photos: null,
|
|
description: '', currency: 'RUB', price: 500, discount: 0, rating: 0,
|
|
callbacks: null, questions: null, quantity: 1,
|
|
};
|
|
|
|
test('double-clicking checkout sends exactly one checkout-session request', async ({ page, context }) => {
|
|
await page.addInitScript(item => {
|
|
window.localStorage.setItem('marketplace_cart', JSON.stringify([item]));
|
|
}, FAKE_ITEM);
|
|
|
|
// Root-caused and fixed 2026-08-21 - see checkout-request-shape.spec.ts's
|
|
// fakeCustomerSession comment and api-headers.interceptor.ts.
|
|
await context.addCookies([{ name: 'webSessionID', value: 'e2e-fake-session', url: 'http://localhost:4200' }]);
|
|
await page.route('**/users/sessions/**', route =>
|
|
route.fulfill({
|
|
status: 200, contentType: 'application/json',
|
|
body: JSON.stringify({ sessionId: 'e2e-fake-session', status: 'active', username: 'e2e_user', userId: 1 }),
|
|
}),
|
|
);
|
|
await page.route('**/api/v2/pricing/fx-quote**', route =>
|
|
route.fulfill({
|
|
status: 200, contentType: 'application/json',
|
|
body: JSON.stringify({ quoteId: 'fxq_e2e', base: 'RUB', quote: 'RUB', rate: 1, source: 'e2e', observedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 300000).toISOString() }),
|
|
}),
|
|
);
|
|
|
|
let checkoutRequestCount = 0;
|
|
await page.route('**/api/v2/storefront/checkout', async (route: Route) => {
|
|
checkoutRequestCount += 1;
|
|
// Deliberately slow, so a real double-click's second event has to land
|
|
// while the first request is still in flight - the exact race this test
|
|
// exists to catch.
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
route.fulfill({
|
|
status: 200, contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
checkoutSessionId: 'chk_e2e_idempotent',
|
|
lines: [{ offerId: String(FAKE_ITEM.itemID), qty: 1, unitPrice: { amountMinor: 50000, currency: 'RUB' }, lineTotal: { amountMinor: 50000, currency: 'RUB' }, priceSnapshotId: 'snap_e2e' }],
|
|
subtotal: { amountMinor: 50000, currency: 'RUB' }, discount: { amountMinor: 0, currency: 'RUB' },
|
|
delivery: { amountMinor: 0, currency: 'RUB' }, total: { amountMinor: 50000, currency: 'RUB' },
|
|
fxQuoteId: 'fxq_e2e', expiresAt: new Date(Date.now() + 300000).toISOString(),
|
|
}),
|
|
});
|
|
});
|
|
await page.route('**/api/v2/storefront/payments/intents', route =>
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ qrId: 'qr_e2e', nspkurl: 'https://example.com/pay', qrTTL: 5 }) }),
|
|
);
|
|
|
|
await page.goto('/cart');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// No <label for="terms-checkbox"> exists in the markup - the checkbox and
|
|
// its text share a plain clickable wrapper - so toggle the input directly.
|
|
await page.locator('#terms-checkbox').dispatchEvent('click');
|
|
await expect(page.locator('#terms-checkbox')).toBeChecked();
|
|
|
|
const qrButton = page.getByRole('button', { name: /qr/i }).first();
|
|
await expect(qrButton).toBeEnabled({ timeout: 10_000 });
|
|
await qrButton.dblclick();
|
|
|
|
// Give the deliberately slow mock time to resolve and for any second,
|
|
// erroneously-fired request to have landed.
|
|
await page.waitForTimeout(1000);
|
|
|
|
expect(checkoutRequestCount, 'a double-click must not create two checkout sessions').toBe(1);
|
|
});
|