Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/app/cooperative/__tests__/page.test.tsx
Original file line number Diff line number Diff line change
@@ -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");
});
});
5 changes: 5 additions & 0 deletions frontend/src/app/cooperative/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";

export default function CooperativePage() {
redirect("/cooperative/bulk-import");
}
94 changes: 94 additions & 0 deletions frontend/tests/visual/cooperative.spec.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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');
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions frontend/tests/visual/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
);
Expand Down
2 changes: 1 addition & 1 deletion jest.scripts.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
65 changes: 31 additions & 34 deletions scripts/__tests__/check-flag-catalog-drift.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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"));
},
};
}
Expand Down Expand Up @@ -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}`,
);
}
});
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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);
});
});
});