diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b024fb2..6bc1490f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -333,11 +333,8 @@ jobs: with: node-version: 22 - - name: Install ts-node - run: npm install -g ts-node typescript --prefer-offline - - name: Run catalog drift check - run: ts-node --skip-project scripts/check-flag-catalog-drift.ts + run: npx --yes tsx scripts/check-flag-catalog-drift.ts backend: name: Backend Required Gate @@ -392,6 +389,8 @@ jobs: run: pnpm install --frozen-lockfile - name: Apply database migrations + env: + DATABASE_URL: postgresql://test:test@localhost:5432/test run: pnpm exec prisma migrate deploy - name: Build diff --git a/frontend/src/app/cooperative/__tests__/page.test.tsx b/frontend/src/app/cooperative/__tests__/page.test.tsx new file mode 100644 index 00000000..4dc65d8e --- /dev/null +++ b/frontend/src/app/cooperative/__tests__/page.test.tsx @@ -0,0 +1,13 @@ +import { redirect } from "next/navigation"; +import CooperativePage from "../page"; + +jest.mock("next/navigation", () => ({ + redirect: jest.fn(), +})); + +describe("CooperativePage", () => { + it("redirects to /cooperative/bulk-import", () => { + CooperativePage(); + expect(redirect).toHaveBeenCalledWith("/cooperative/bulk-import"); + }); +}); diff --git a/frontend/src/app/cooperative/page.tsx b/frontend/src/app/cooperative/page.tsx new file mode 100644 index 00000000..1cc69be8 --- /dev/null +++ b/frontend/src/app/cooperative/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function CooperativePage() { + redirect("/cooperative/bulk-import"); +} diff --git a/frontend/tests/visual/cooperative.spec.ts b/frontend/tests/visual/cooperative.spec.ts new file mode 100644 index 00000000..32b35ae1 --- /dev/null +++ b/frontend/tests/visual/cooperative.spec.ts @@ -0,0 +1,94 @@ +/** + * Visual regression tests — Cooperative pages. + * + * Covers: + * - /cooperative/bulk-import (unauthenticated state — connect wallet prompt) + * - /cooperative/bulk-import (authenticated state — CSV bulk import form) + * - /cooperative/bulk-import (authenticated state — bulk import result view) + * - /cooperative (redirect to /cooperative/bulk-import) + * + * Each test runs in both chromium-desktop and chromium-mobile projects + * (configured via playwright.config.ts testMatch). + */ + +import { test, expect } from './fixtures'; + +async function waitForStable(page: import('@playwright/test').Page): Promise { + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(150); +} + +test.describe('Cooperative Page — unauthenticated', () => { + test('connect-wallet prompt matches snapshot', async ({ publicPage: page }) => { + await page.goto('/cooperative/bulk-import'); + await waitForStable(page); + + await expect(page.getByRole('heading', { level: 1, name: 'Cooperative bulk import' })).toBeVisible(); + const main = page.locator('main').first(); + await expect(main).toHaveScreenshot('cooperative-public-main.png'); + }); + + test('full unauthenticated page layout matches snapshot', async ({ publicPage: page }) => { + await page.goto('/cooperative/bulk-import'); + await waitForStable(page); + + await expect(page.getByRole('heading', { level: 1, name: 'Cooperative bulk import' })).toBeVisible(); + await expect(page).toHaveScreenshot('cooperative-public-full.png', { fullPage: true }); + }); + + test('navigating to /cooperative redirects to /cooperative/bulk-import', async ({ publicPage: page }) => { + await page.goto('/cooperative'); + await page.waitForURL('**/cooperative/bulk-import', { timeout: 10_000 }); + await waitForStable(page); + + await expect(page.getByRole('heading', { level: 1, name: 'Cooperative bulk import' })).toBeVisible(); + expect(page.url()).toContain('/cooperative/bulk-import'); + }); +}); + +test.describe('Cooperative Page — authenticated', () => { + test('bulk import form matches snapshot', async ({ authenticatedPage: page }) => { + await page.goto('/cooperative/bulk-import'); + await waitForStable(page); + + await expect(page.getByRole('heading', { level: 1, name: 'Cooperative bulk import' })).toBeVisible(); + await expect(page.locator('#bulk-csv')).toBeVisible(); + + const main = page.locator('main').first(); + await expect(main).toHaveScreenshot('cooperative-auth-main.png'); + }); + + test('full authenticated page matches snapshot', async ({ authenticatedPage: page }) => { + await page.goto('/cooperative/bulk-import'); + await waitForStable(page); + + await expect(page.getByRole('heading', { level: 1, name: 'Cooperative bulk import' })).toBeVisible(); + await expect(page).toHaveScreenshot('cooperative-auth-full.png', { fullPage: true }); + }); + + test('import result view matches snapshot', async ({ authenticatedPage: page }) => { + await page.route('**/trades/bulk', (r) => + r.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + created: [{ index: 0, tradeId: 'TRD-FIXTURE-0001', unsignedXdr: 'mock-xdr' }], + failed: [], + }), + }), + ); + + await page.goto('/cooperative/bulk-import'); + await waitForStable(page); + + const submitBtn = page.getByRole('button', { name: /import 1 trade/i }); + await expect(submitBtn).toBeEnabled(); + await submitBtn.click(); + + await expect(page.getByRole('heading', { level: 2, name: 'Result' })).toBeVisible(); + await waitForStable(page); + + const main = page.locator('main').first(); + await expect(main).toHaveScreenshot('cooperative-import-result.png'); + }); +}); diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-full-chromium-desktop-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-full-chromium-desktop-linux.png new file mode 100644 index 00000000..39af2008 Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-full-chromium-desktop-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-full-chromium-mobile-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-full-chromium-mobile-linux.png new file mode 100644 index 00000000..5d52ae62 Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-full-chromium-mobile-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-main-chromium-desktop-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-main-chromium-desktop-linux.png new file mode 100644 index 00000000..d3bf6d39 Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-main-chromium-desktop-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-main-chromium-mobile-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-main-chromium-mobile-linux.png new file mode 100644 index 00000000..1ffc81a2 Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-auth-main-chromium-mobile-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-import-result-chromium-desktop-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-import-result-chromium-desktop-linux.png new file mode 100644 index 00000000..74302c4c Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-import-result-chromium-desktop-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-import-result-chromium-mobile-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-import-result-chromium-mobile-linux.png new file mode 100644 index 00000000..7d08c354 Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-import-result-chromium-mobile-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-full-chromium-desktop-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-full-chromium-desktop-linux.png new file mode 100644 index 00000000..827888fa Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-full-chromium-desktop-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-full-chromium-mobile-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-full-chromium-mobile-linux.png new file mode 100644 index 00000000..181d848f Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-full-chromium-mobile-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-main-chromium-desktop-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-main-chromium-desktop-linux.png new file mode 100644 index 00000000..19466821 Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-main-chromium-desktop-linux.png differ diff --git a/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-main-chromium-mobile-linux.png b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-main-chromium-mobile-linux.png new file mode 100644 index 00000000..8ed2d165 Binary files /dev/null and b/frontend/tests/visual/cooperative.spec.ts-snapshots/cooperative-public-main-chromium-mobile-linux.png differ diff --git a/frontend/tests/visual/fixtures.ts b/frontend/tests/visual/fixtures.ts index eb5939fa..68f8151b 100644 --- a/frontend/tests/visual/fixtures.ts +++ b/frontend/tests/visual/fixtures.ts @@ -174,6 +174,21 @@ async function seedAuthenticatedWallet( signTransaction: async (xdr: string) => ({ signedTxXdr: `signed-${xdr}` }), }; Object.assign(window, { freighter, freighterApi: freighter }); + + window.addEventListener('message', (event) => { + if (event.data?.source === 'FREIGHTER_EXTERNAL_MSG_REQUEST') { + window.postMessage( + { + source: 'FREIGHTER_EXTERNAL_MSG_RESPONSE', + messagedId: event.data.messageId, + isAllowed: true, + isConnected: true, + publicKey: addr, + }, + '*', + ); + } + }); }, { tok: token, addr: address }, ); diff --git a/jest.scripts.config.mjs b/jest.scripts.config.mjs index a9a663eb..df009714 100644 --- a/jest.scripts.config.mjs +++ b/jest.scripts.config.mjs @@ -5,7 +5,7 @@ * are written as ES modules (.mjs). * * Run: - * node --experimental-vm-modules node_modules/.bin/jest \ + * node --experimental-vm-modules node_modules/jest/bin/jest.js \ * --config jest.scripts.config.mjs * * Or via the npm script: pnpm test:scripts diff --git a/package.json b/package.json index 30960ac9..5a18aa6a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "scripts": { "codegen:events": "node scripts/codegen-events.mjs", "codegen:events:check": "node scripts/codegen-events.mjs --check", - "test:scripts": "node --experimental-vm-modules node_modules/.bin/jest --config jest.scripts.config.mjs" + "test:scripts": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.scripts.config.mjs" }, "devDependencies": { "@types/yamljs": "^0.2.34", diff --git a/scripts/__tests__/check-flag-catalog-drift.test.mjs b/scripts/__tests__/check-flag-catalog-drift.test.mjs index a53494c6..5970670e 100644 --- a/scripts/__tests__/check-flag-catalog-drift.test.mjs +++ b/scripts/__tests__/check-flag-catalog-drift.test.mjs @@ -34,6 +34,29 @@ function catalogFile(keys) { return `export const FLAG_CATALOG = {\n${listed}\n} as const;\n`; } +function runTypeScript(filePath) { + try { + const out = execSync(`node --experimental-strip-types "${filePath}"`, { + stdio: "pipe", + encoding: "utf8", + }); + return { code: 0, stdout: out, stderr: "" }; + } catch (err) { + if (err.stderr && String(err.stderr).includes("ERR_NO_TYPESCRIPT")) { + try { + const out = execSync(`npx --yes tsx "${filePath}"`, { + stdio: "pipe", + encoding: "utf8", + }); + return { code: 0, stdout: out, stderr: "" }; + } catch (inner) { + return { code: inner.status ?? -1, stderr: inner.stderr, stdout: inner.stdout }; + } + } + return { code: err.status ?? -1, stderr: err.stderr, stdout: err.stdout }; + } +} + function makeDriftRun(flagsPath) { const dir = join(tmpdir(), `flag-drift-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(dir, { recursive: true }); @@ -57,15 +80,7 @@ function makeDriftRun(flagsPath) { dir, run(flagsContent = catalogFile(BACKEND_KNOWN_FLAGS)) { writeFileSync(join(dir, "flags.ts"), flagsContent, "utf8"); - try { - execSync(`node --experimental-strip-types "${join(dir, "drift-check.mts")}"`, { - stdio: "pipe", - encoding: "utf8", - }); - return { code: 0 }; - } catch (err) { - return { code: err.status ?? -1, stderr: err.stderr, stdout: err.stdout }; - } + return runTypeScript(join(dir, "drift-check.mts")); }, }; } @@ -102,14 +117,10 @@ describe("scripts/check-flag-catalog-drift.ts", () => { )};`, ); writeFileSync(join(dir, "drift-check.mts"), patched, "utf8"); - try { - execSync(`node --experimental-strip-types "${join(dir, "drift-check.mts")}"`, { - stdio: "pipe", - encoding: "utf8", - }); - } catch (err) { + const result = runTypeScript(join(dir, "drift-check.mts")); + if (result.code !== 0) { throw new Error( - `real flag catalog should be in sync (exit 0), got ${err.status ?? -1}: ${err.stderr}`, + `real flag catalog should be in sync (exit 0), got ${result.code}: ${result.stderr}`, ); } }); @@ -137,15 +148,8 @@ describe("scripts/check-flag-catalog-drift.ts", () => { ); writeFileSync(join(dir, "drift-check.mts"), patched, "utf8"); writeFileSync(join(dir, "flags.ts"), "export const FLAG_CATALOG = not valid ;;\n", "utf8"); - try { - execSync(`node --experimental-strip-types "${join(dir, "drift-check.mts")}"`, { - stdio: "pipe", - encoding: "utf8", - }); - throw new Error("expected exit 1 for unparseable catalog"); - } catch (err) { - expect(err.status).toBe(1); - } + const result = runTypeScript(join(dir, "drift-check.mts")); + expect(result.code).toBe(1); }); it("fails when the frontend catalog file does not exist", () => { @@ -158,15 +162,8 @@ describe("scripts/check-flag-catalog-drift.ts", () => { `const frontendFlagsPath = ${JSON.stringify(resolve(dir, "does-not-exist.ts"))};`, ); writeFileSync(join(dir, "drift-check.mts"), patched, "utf8"); - try { - execSync(`node --experimental-strip-types "${join(dir, "drift-check.mts")}"`, { - stdio: "pipe", - encoding: "utf8", - }); - throw new Error("expected exit 1 for missing catalog"); - } catch (err) { - expect(err.status).toBe(1); - } + const result = runTypeScript(join(dir, "drift-check.mts")); + expect(result.code).toBe(1); }); }); });