Files
marketplaces/e2e/checkout-idempotent-click.spec.ts
sdarbinyan d4959bd4da
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled
docs(e2e): clear the stale known-issue markers, root cause found
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.
2026-08-21 22:45:47 +04:00

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);
});