// ── Clover events discovery (THROWAWAY) ───────────────────────── // // Drives the fixture HTML through scripted interactions for every Clover field // and dumps the captured event log to console. The output is meant to be read // by a human (the SDK author) once, used to inform the deferred-error logic // and the README event-model docs, then this whole `_discovery/` folder is // DELETED. // // Not part of the regular test suite — uses its own playwright config so it // doesn't run on CI by accident. import { test, type Page, type FrameLocator } from '@playwright/test'; // `process` and `__dirname` are Node globals; this spec runs under Playwright's // Node loader. We avoid `import 'path'` so the spec typechecks even without // `@types/node` (Playwright's CLI executes it regardless of strict TS errors, // but this keeps the file clean for IDE consumers). declare const process: { env: Record }; const PAKMS = process.env.CLOVER_PAKMS; const MERCHANT = process.env.CLOVER_MERCHANT_ID; const SDK_URL = process.env.CLOVER_SDK_URL; // optional override // Fixture is served by the config's `webServer` (python http.server on port 4173) // because Clover's iframes target the parent origin via postMessage; `file://` // resolves to `null` and Clover drops all event delivery. const FIXTURE_URL_BASE = '/fixture.html'; test.skip( !PAKMS || !MERCHANT, 'Set CLOVER_PAKMS and CLOVER_MERCHANT_ID env vars to run discovery.', ); interface Capture { readonly ts: number; readonly category: string; readonly [key: string]: unknown; } function fixtureUrl(): string { const qs = new URLSearchParams({ pakms: PAKMS!, merchant: MERCHANT! }); if (SDK_URL) qs.set('sdk', SDK_URL); return `${FIXTURE_URL_BASE}?${qs.toString()}`; } async function readCaptures(page: Page): Promise { return await page.evaluate(() => (window as unknown as { __captures: Capture[] }).__captures); } async function clearCaptures(page: Page): Promise { await page.evaluate(() => { (window as unknown as { __captures: Capture[] }).__captures = []; }); } async function waitReady(page: Page): Promise { await page.waitForFunction( () => (window as unknown as { __ready: boolean }).__ready === true, null, { timeout: 20_000 }, ); // Allow any post-mount handshake postMessages to settle. await page.waitForTimeout(500); } function frameFor(page: Page, fieldId: string): FrameLocator { return page.frameLocator(`#${fieldId} iframe`); } async function focusField(page: Page, fieldId: string): Promise { await frameFor(page, fieldId).locator('input').click(); await page.waitForTimeout(80); } async function typeField(page: Page, fieldId: string, text: string): Promise { const input = frameFor(page, fieldId).locator('input'); await input.click(); await input.pressSequentially(text, { delay: 40 }); } async function clearField(page: Page, fieldId: string): Promise { const input = frameFor(page, fieldId).locator('input'); await input.click(); // Select-all + delete works across browsers inside iframes. We try Control+A // first (Linux/Win) then Meta+A (macOS); the second is a no-op on the wrong OS. await input.press('Control+a'); await input.press('Meta+a'); await input.press('Delete'); } async function blurAll(page: Page): Promise { // Clicking outside any field's iframe in the parent document removes focus. await page.locator('#status').click(); await page.waitForTimeout(150); } interface Step { readonly label: string; readonly run: () => Promise; } function dump(caps: readonly Capture[]): void { for (const c of caps) { // eslint-disable-next-line no-console console.log(JSON.stringify(c)); } } async function runSequence(page: Page, fieldName: string, steps: readonly Step[]): Promise { // eslint-disable-next-line no-console console.log(`\n══════════════════════════════════════════════════════════`); // eslint-disable-next-line no-console console.log(`══ FIELD: ${fieldName}`); // eslint-disable-next-line no-console console.log(`══════════════════════════════════════════════════════════`); for (const step of steps) { await clearCaptures(page); // eslint-disable-next-line no-console console.log(`\n── STEP: ${step.label} ──`); try { await step.run(); } catch (e) { // eslint-disable-next-line no-console console.log(`(step threw: ${String(e)})`); } // Settle: allow Clover's change/blur events to propagate before we read. await page.waitForTimeout(400); dump(await readCaptures(page)); } } test('Discover Clover field events', async ({ page }) => { page.on('console', (msg) => { // Forward browser console to test output for visibility into fixture-side errors. if (msg.type() === 'error' || msg.type() === 'warning') { // eslint-disable-next-line no-console console.log(`[browser/${msg.type()}] ${msg.text()}`); } }); page.on('pageerror', (err) => { // eslint-disable-next-line no-console console.log(`[browser/pageerror] ${err.message}`); }); await page.goto(fixtureUrl()); await waitReady(page); // eslint-disable-next-line no-console console.log(`\n══════════════════════════════════════════════════════════`); // eslint-disable-next-line no-console console.log(`══ INIT (everything captured before any interaction)`); // eslint-disable-next-line no-console console.log(`══════════════════════════════════════════════════════════`); dump(await readCaptures(page)); // ── cardNumber ── await runSequence(page, 'cardNumber', [ { label: 'focus then blur (empty)', run: async () => { await focusField(page, 'cardNumber'); await blurAll(page); } }, { label: 'type "4"', run: async () => { await focusField(page, 'cardNumber'); await typeField(page, 'cardNumber', '4'); } }, { label: 'type "242" (now "4242")', run: async () => { await typeField(page, 'cardNumber', '242'); } }, { label: 'blur with "4242"', run: async () => { await blurAll(page); } }, { label: 'focus + type 12 more digits (full Visa)', run: async () => { await focusField(page, 'cardNumber'); await typeField(page, 'cardNumber', '424242424242'); } }, { label: 'blur with full valid Visa', run: async () => { await blurAll(page); } }, { label: 'clear + type bad-Luhn "1234567890123456"', run: async () => { await clearField(page, 'cardNumber'); await typeField(page, 'cardNumber', '1234567890123456'); } }, { label: 'blur with bad-Luhn', run: async () => { await blurAll(page); } }, ]); // ── cardDate ── await runSequence(page, 'cardDate', [ { label: 'focus + blur (empty)', run: async () => { await focusField(page, 'cardDate'); await blurAll(page); } }, { label: 'type "0"', run: async () => { await focusField(page, 'cardDate'); await typeField(page, 'cardDate', '0'); } }, { label: 'type "1" (now "01")', run: async () => { await typeField(page, 'cardDate', '1'); } }, { label: 'type "25" (now "0125")', run: async () => { await typeField(page, 'cardDate', '25'); } }, { label: 'blur with "01/25"', run: async () => { await blurAll(page); } }, { label: 'clear + type "1325" (invalid month)', run: async () => { await clearField(page, 'cardDate'); await typeField(page, 'cardDate', '1325'); } }, { label: 'blur with bad month', run: async () => { await blurAll(page); } }, { label: 'clear + type "0120" (past)', run: async () => { await clearField(page, 'cardDate'); await typeField(page, 'cardDate', '0120'); } }, { label: 'blur with past date', run: async () => { await blurAll(page); } }, ]); // ── cardCvv ── await runSequence(page, 'cardCvv', [ { label: 'focus + blur (empty)', run: async () => { await focusField(page, 'cardCvv'); await blurAll(page); } }, { label: 'type "1"', run: async () => { await focusField(page, 'cardCvv'); await typeField(page, 'cardCvv', '1'); } }, { label: 'type "23" (now "123")', run: async () => { await typeField(page, 'cardCvv', '23'); } }, { label: 'blur with "123"', run: async () => { await blurAll(page); } }, { label: 'focus + add "4" (now "1234")', run: async () => { await focusField(page, 'cardCvv'); await typeField(page, 'cardCvv', '4'); } }, { label: 'blur with "1234"', run: async () => { await blurAll(page); } }, { label: 'clear + type "12" (short)', run: async () => { await clearField(page, 'cardCvv'); await typeField(page, 'cardCvv', '12'); } }, { label: 'blur with short CVV', run: async () => { await blurAll(page); } }, ]); // ── cardPostalCode ── await runSequence(page, 'cardPostalCode', [ { label: 'focus + blur (empty)', run: async () => { await focusField(page, 'cardPostalCode'); await blurAll(page); } }, { label: 'type "9"', run: async () => { await focusField(page, 'cardPostalCode'); await typeField(page, 'cardPostalCode', '9'); } }, { label: 'type "0210" (now "90210")', run: async () => { await typeField(page, 'cardPostalCode', '0210'); } }, { label: 'blur with "90210"', run: async () => { await blurAll(page); } }, { label: 'clear + type "ABC"', run: async () => { await clearField(page, 'cardPostalCode'); await typeField(page, 'cardPostalCode', 'ABC'); } }, { label: 'blur with "ABC"', run: async () => { await blurAll(page); } }, ]); // ── cardName ── await runSequence(page, 'cardName', [ { label: 'focus + blur (empty)', run: async () => { await focusField(page, 'cardName'); await blurAll(page); } }, { label: 'type "J"', run: async () => { await focusField(page, 'cardName'); await typeField(page, 'cardName', 'J'); } }, { label: 'type "ohn Doe" (now "John Doe")', run: async () => { await typeField(page, 'cardName', 'ohn Doe'); } }, { label: 'blur with "John Doe"', run: async () => { await blurAll(page); } }, { label: 'clear + blur (back to empty)', run: async () => { await clearField(page, 'cardName'); await blurAll(page); } }, ]); // ── cardEmail ── await runSequence(page, 'cardEmail', [ { label: 'focus + blur (empty)', run: async () => { await focusField(page, 'cardEmail'); await blurAll(page); } }, { label: 'type "j"', run: async () => { await focusField(page, 'cardEmail'); await typeField(page, 'cardEmail', 'j'); } }, { label: 'type "@" (now "j@")', run: async () => { await typeField(page, 'cardEmail', '@'); } }, { label: 'type "x.co" (now "j@x.co")', run: async () => { await typeField(page, 'cardEmail', 'x.co'); } }, { label: 'blur with valid-looking', run: async () => { await blurAll(page); } }, { label: 'clear + type "not-an-email"', run: async () => { await clearField(page, 'cardEmail'); await typeField(page, 'cardEmail', 'not-an-email'); } }, { label: 'blur with malformed email', run: async () => { await blurAll(page); } }, { label: 'clear + blur (back to empty)', run: async () => { await clearField(page, 'cardEmail'); await blurAll(page); } }, ]); // eslint-disable-next-line no-console console.log('\n══ DISCOVERY COMPLETE ══'); });