@le-space/playwright 0.6.27 → 0.6.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/index.d.ts +76 -2
- package/index.js +143 -50
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,3 +17,29 @@ import {
|
|
|
17
17
|
|
|
18
18
|
`@playwright/test` is a peer dependency. Use a compatible 1.61.x client; remote
|
|
19
19
|
Playwright servers must use the exact same client/server version.
|
|
20
|
+
|
|
21
|
+
## Aleph remote Chromium
|
|
22
|
+
|
|
23
|
+
`connectAlephChromium()` is the thin consumer-facing adapter for the dedicated
|
|
24
|
+
Aleph runner. It authenticates the `/version` probe and websocket with the same
|
|
25
|
+
per-run bearer secret, requires HTTPS/WSS, checks the guest is exactly
|
|
26
|
+
Playwright `1.61.1`, and only then calls `chromium.connect()`.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
const browser = await connectAlephChromium({
|
|
30
|
+
chromium,
|
|
31
|
+
wsEndpoint,
|
|
32
|
+
versionUrl,
|
|
33
|
+
secret,
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`buildAlephCostEvidence()` and `formatAlephCostGithubSummary()` keep required
|
|
38
|
+
credit capacity separate from the authoritative before/after account balance
|
|
39
|
+
delta. A balance delta is attributable to one run only when that deployment
|
|
40
|
+
account is not being used concurrently.
|
|
41
|
+
|
|
42
|
+
`selectExpiredAlephPlaywrightRunners()` is the safety boundary used by the
|
|
43
|
+
retention janitor. The repository script requires an explicit repository scope
|
|
44
|
+
and owner key, then erases, owner-signs FORGET for, and verifies each selected
|
|
45
|
+
exact INSTANCE hash. It never cleans by display name alone.
|
package/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as _playwright_test from '@playwright/test';
|
|
2
|
-
import { Page, Locator, BrowserContext } from '@playwright/test';
|
|
2
|
+
import { Browser, Page, Locator, BrowserContext } from '@playwright/test';
|
|
3
3
|
import { RelayBootstrapPostRecord, RelayBootstrapContent, fetchAlephBootstrapPosts } from '@le-space/aleph-bootstrap';
|
|
4
4
|
import { eraseInstanceOnCrn, forgetAlephMessages } from '@le-space/core';
|
|
5
5
|
|
|
6
|
+
declare const PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
|
|
6
7
|
declare const SUPPORTED_ALEPH_API_HOSTS: readonly ["https://api2.aleph.im", "https://api.aleph.im"];
|
|
7
8
|
declare const DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
|
|
8
9
|
interface RelayWalletAccount {
|
|
@@ -28,6 +29,63 @@ interface RelayEvidence {
|
|
|
28
29
|
error?: string;
|
|
29
30
|
[key: string]: unknown;
|
|
30
31
|
}
|
|
32
|
+
interface AlephChromiumConnector {
|
|
33
|
+
connect(wsEndpoint: string, options?: {
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
timeout?: number;
|
|
36
|
+
}): Promise<Browser>;
|
|
37
|
+
}
|
|
38
|
+
interface AlephRemoteBrowserOptions {
|
|
39
|
+
chromium: AlephChromiumConnector;
|
|
40
|
+
wsEndpoint: string;
|
|
41
|
+
versionUrl: string;
|
|
42
|
+
secret: string;
|
|
43
|
+
expectedVersion?: string;
|
|
44
|
+
timeoutMs?: number;
|
|
45
|
+
fetch?: typeof fetch;
|
|
46
|
+
}
|
|
47
|
+
interface AlephCreditBalanceSnapshot {
|
|
48
|
+
capturedAt: string;
|
|
49
|
+
apiHost: string;
|
|
50
|
+
creditBalance: number;
|
|
51
|
+
lockedAmount: number;
|
|
52
|
+
}
|
|
53
|
+
interface AlephPricingSnapshot {
|
|
54
|
+
capturedAt: string;
|
|
55
|
+
apiHost: string;
|
|
56
|
+
unitCredit: number;
|
|
57
|
+
computeUnits: number;
|
|
58
|
+
vcpus: number;
|
|
59
|
+
memoryMiB: number;
|
|
60
|
+
diskMiB: number;
|
|
61
|
+
}
|
|
62
|
+
interface AlephCostEvidence {
|
|
63
|
+
paymentType: 'credit';
|
|
64
|
+
startedAt: string;
|
|
65
|
+
finishedAt: string;
|
|
66
|
+
runtimeSeconds: number;
|
|
67
|
+
pricing: AlephPricingSnapshot;
|
|
68
|
+
before: AlephCreditBalanceSnapshot;
|
|
69
|
+
after: AlephCreditBalanceSnapshot;
|
|
70
|
+
requiredCredits: number;
|
|
71
|
+
netAccountCreditDelta: number;
|
|
72
|
+
creditsConsumed: number;
|
|
73
|
+
creditsReturned: number;
|
|
74
|
+
accountingNote: string;
|
|
75
|
+
}
|
|
76
|
+
interface AlephRunnerInstanceCandidate {
|
|
77
|
+
itemHash: string;
|
|
78
|
+
ownerAddress: string;
|
|
79
|
+
instanceName: string;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
status: string;
|
|
82
|
+
}
|
|
83
|
+
interface AlephRunnerJanitorSelection {
|
|
84
|
+
expired: AlephRunnerInstanceCandidate[];
|
|
85
|
+
retained: Array<AlephRunnerInstanceCandidate & {
|
|
86
|
+
reason: string;
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
31
89
|
interface RelayAddressPolicy {
|
|
32
90
|
allowWebTransport?: boolean;
|
|
33
91
|
allowWebRtcDirect?: boolean;
|
|
@@ -116,6 +174,22 @@ interface CreateRelayTestOptions {
|
|
|
116
174
|
cleanup?: Omit<CleanupRelayOptions, 'page' | 'account' | 'instanceName' | 'instanceHash'>;
|
|
117
175
|
}
|
|
118
176
|
declare function resolveAlephApiHosts(candidates?: readonly string[]): string[];
|
|
177
|
+
declare function connectAlephChromium(options: AlephRemoteBrowserOptions): Promise<Browser>;
|
|
178
|
+
declare function buildAlephCostEvidence(options: {
|
|
179
|
+
startedAt: string;
|
|
180
|
+
finishedAt: string;
|
|
181
|
+
pricing: AlephPricingSnapshot;
|
|
182
|
+
before: AlephCreditBalanceSnapshot;
|
|
183
|
+
after: AlephCreditBalanceSnapshot;
|
|
184
|
+
}): AlephCostEvidence;
|
|
185
|
+
declare function formatAlephCostGithubSummary(cost: AlephCostEvidence): string;
|
|
186
|
+
declare function selectExpiredAlephPlaywrightRunners(options: {
|
|
187
|
+
candidates: readonly AlephRunnerInstanceCandidate[];
|
|
188
|
+
ownerAddress: string;
|
|
189
|
+
repository: string;
|
|
190
|
+
now?: number;
|
|
191
|
+
ttlMs?: number;
|
|
192
|
+
}): AlephRunnerJanitorSelection;
|
|
119
193
|
declare function waitForPubsubSubscriber(page: Page, options: WaitForPubsubSubscriberOptions): Promise<string[]>;
|
|
120
194
|
declare function installEip1193WalletMock(context: BrowserContext, account: RelayWalletAccount): Promise<void>;
|
|
121
195
|
declare class RelayButtonDriver {
|
|
@@ -181,4 +255,4 @@ declare function createRelayTest(options: CreateRelayTestOptions): _playwright_t
|
|
|
181
255
|
relayLifecycle: RelayLifecycleFixture;
|
|
182
256
|
}, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
|
|
183
257
|
|
|
184
|
-
export { type CleanupRelayHooks, type CleanupRelayOptions, type CleanupRelayResult, type CreateRelayTestOptions, DEFAULT_ALEPH_SCHEDULER_URL, type ProvisionRelayOptions, type ProvisionedRelay, type RelayAddressPolicy, RelayButtonDriver, type RelayButtonDriverOptions, type RelayEvidence, type RelayEvidenceStep, type RelayLifecycleFixture, type RelayWalletAccount, SUPPORTED_ALEPH_API_HOSTS, type WaitForPubsubSubscriberOptions, appendRelayGithubSummary, cleanupRelay, createRelayEvidence, createRelayTest, findAlephInstanceHash, formatRelayGithubSummary, installEip1193WalletMock, provisionRelay, resolveAlephApiHosts, selectBrowserRelayAddresses, updateRelayEvidenceStep, waitForAlephInstanceDeletion, waitForBootstrapRegistration, waitForDeployableManifest, waitForPubsubSubscriber, writeRelayEvidence };
|
|
258
|
+
export { type AlephChromiumConnector, type AlephCostEvidence, type AlephCreditBalanceSnapshot, type AlephPricingSnapshot, type AlephRemoteBrowserOptions, type AlephRunnerInstanceCandidate, type AlephRunnerJanitorSelection, type CleanupRelayHooks, type CleanupRelayOptions, type CleanupRelayResult, type CreateRelayTestOptions, DEFAULT_ALEPH_SCHEDULER_URL, PLAYWRIGHT_RUNNER_VERSION, type ProvisionRelayOptions, type ProvisionedRelay, type RelayAddressPolicy, RelayButtonDriver, type RelayButtonDriverOptions, type RelayEvidence, type RelayEvidenceStep, type RelayLifecycleFixture, type RelayWalletAccount, SUPPORTED_ALEPH_API_HOSTS, type WaitForPubsubSubscriberOptions, appendRelayGithubSummary, buildAlephCostEvidence, cleanupRelay, connectAlephChromium, createRelayEvidence, createRelayTest, findAlephInstanceHash, formatAlephCostGithubSummary, formatRelayGithubSummary, installEip1193WalletMock, provisionRelay, resolveAlephApiHosts, selectBrowserRelayAddresses, selectExpiredAlephPlaywrightRunners, updateRelayEvidenceStep, waitForAlephInstanceDeletion, waitForBootstrapRegistration, waitForDeployableManifest, waitForPubsubSubscriber, writeRelayEvidence };
|
package/index.js
CHANGED
|
@@ -2,17 +2,11 @@
|
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { appendFile, mkdir, writeFile } from "fs/promises";
|
|
4
4
|
import { dirname } from "path";
|
|
5
|
-
import {
|
|
6
|
-
fetchAlephBootstrapPosts
|
|
7
|
-
} from "@le-space/aleph-bootstrap";
|
|
5
|
+
import { fetchAlephBootstrapPosts } from "@le-space/aleph-bootstrap";
|
|
8
6
|
import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var SUPPORTED_ALEPH_API_HOSTS = [
|
|
13
|
-
"https://api2.aleph.im",
|
|
14
|
-
"https://api.aleph.im"
|
|
15
|
-
];
|
|
7
|
+
import { test as playwrightTest } from "@playwright/test";
|
|
8
|
+
var PLAYWRIGHT_RUNNER_VERSION = "1.61.1";
|
|
9
|
+
var SUPPORTED_ALEPH_API_HOSTS = ["https://api2.aleph.im", "https://api.aleph.im"];
|
|
16
10
|
var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
|
|
17
11
|
var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
|
|
18
12
|
function normalizeApiOrigin(value) {
|
|
@@ -25,12 +19,118 @@ function normalizeApiOrigin(value) {
|
|
|
25
19
|
}
|
|
26
20
|
}
|
|
27
21
|
function resolveAlephApiHosts(candidates) {
|
|
28
|
-
const allowed = new Set(
|
|
29
|
-
(candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null)
|
|
30
|
-
);
|
|
22
|
+
const allowed = new Set((candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null));
|
|
31
23
|
const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
|
|
32
24
|
return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
|
|
33
25
|
}
|
|
26
|
+
function requireFiniteNumber(value, label) {
|
|
27
|
+
const number = Number(value);
|
|
28
|
+
if (!Number.isFinite(number)) throw new Error(`${label} must be a finite number`);
|
|
29
|
+
return number;
|
|
30
|
+
}
|
|
31
|
+
async function connectAlephChromium(options) {
|
|
32
|
+
const expectedVersion = options.expectedVersion ?? PLAYWRIGHT_RUNNER_VERSION;
|
|
33
|
+
const secret = options.secret.trim();
|
|
34
|
+
if (!secret) throw new Error("Aleph Playwright secret is required");
|
|
35
|
+
if (!options.wsEndpoint.startsWith("wss://")) {
|
|
36
|
+
throw new Error("Aleph Playwright endpoint must use authenticated WSS");
|
|
37
|
+
}
|
|
38
|
+
if (!options.versionUrl.startsWith("https://")) {
|
|
39
|
+
throw new Error("Aleph Playwright version endpoint must use HTTPS");
|
|
40
|
+
}
|
|
41
|
+
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
42
|
+
if (!fetchImpl) throw new Error("A fetch implementation is required for version verification");
|
|
43
|
+
const authorization = `Bearer ${secret}`;
|
|
44
|
+
const response = await fetchImpl(options.versionUrl, {
|
|
45
|
+
headers: { Authorization: authorization },
|
|
46
|
+
cache: "no-store",
|
|
47
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 3e4)
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Aleph Playwright version endpoint returned HTTP ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
const payload = await response.json();
|
|
53
|
+
const actualVersion = String(payload.playwrightVersion ?? "");
|
|
54
|
+
if (actualVersion !== expectedVersion) {
|
|
55
|
+
throw new Error(`Playwright client/server version mismatch: client ${expectedVersion}, guest ${actualVersion || "unknown"}`);
|
|
56
|
+
}
|
|
57
|
+
return options.chromium.connect(options.wsEndpoint, {
|
|
58
|
+
headers: { Authorization: authorization },
|
|
59
|
+
timeout: options.timeoutMs ?? 3e4
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function buildAlephCostEvidence(options) {
|
|
63
|
+
const startedAtMs = Date.parse(options.startedAt);
|
|
64
|
+
const finishedAtMs = Date.parse(options.finishedAt);
|
|
65
|
+
if (!Number.isFinite(startedAtMs) || !Number.isFinite(finishedAtMs) || finishedAtMs < startedAtMs) {
|
|
66
|
+
throw new Error("Aleph cost evidence requires ordered ISO start/finish timestamps");
|
|
67
|
+
}
|
|
68
|
+
const unitCredit = requireFiniteNumber(options.pricing.unitCredit, "unitCredit");
|
|
69
|
+
const computeUnits = requireFiniteNumber(options.pricing.computeUnits, "computeUnits");
|
|
70
|
+
const beforeCredits = requireFiniteNumber(options.before.creditBalance, "before.creditBalance");
|
|
71
|
+
const afterCredits = requireFiniteNumber(options.after.creditBalance, "after.creditBalance");
|
|
72
|
+
const netAccountCreditDelta = afterCredits - beforeCredits;
|
|
73
|
+
return {
|
|
74
|
+
paymentType: "credit",
|
|
75
|
+
startedAt: options.startedAt,
|
|
76
|
+
finishedAt: options.finishedAt,
|
|
77
|
+
runtimeSeconds: Math.ceil((finishedAtMs - startedAtMs) / 1e3),
|
|
78
|
+
pricing: options.pricing,
|
|
79
|
+
before: options.before,
|
|
80
|
+
after: options.after,
|
|
81
|
+
requiredCredits: unitCredit * computeUnits,
|
|
82
|
+
netAccountCreditDelta,
|
|
83
|
+
creditsConsumed: Math.max(0, -netAccountCreditDelta),
|
|
84
|
+
creditsReturned: Math.max(0, netAccountCreditDelta),
|
|
85
|
+
accountingNote: "Required credits are Aleph credit-payment capacity, not a time-pro-rated charge. The balance delta is the authoritative account observation for this interval and is attributable to this test only when the deployment account is not used concurrently."
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function formatAlephCostGithubSummary(cost) {
|
|
89
|
+
return `## Aleph remote browser cost
|
|
90
|
+
|
|
91
|
+
| Field | Value |
|
|
92
|
+
| --- | ---: |
|
|
93
|
+
| Payment type | ${cost.paymentType} |
|
|
94
|
+
| Runtime | ${cost.runtimeSeconds} s |
|
|
95
|
+
| Hardware | ${cost.pricing.vcpus} vCPU \xB7 ${cost.pricing.memoryMiB} MiB RAM \xB7 ${cost.pricing.diskMiB} MiB disk |
|
|
96
|
+
| Compute units | ${cost.pricing.computeUnits} |
|
|
97
|
+
| Unit credit requirement | ${cost.pricing.unitCredit} credits |
|
|
98
|
+
| Required credit capacity | ${cost.requiredCredits} credits |
|
|
99
|
+
| Balance before | ${cost.before.creditBalance} credits |
|
|
100
|
+
| Balance after cleanup | ${cost.after.creditBalance} credits |
|
|
101
|
+
| Net account delta | ${cost.netAccountCreditDelta} credits |
|
|
102
|
+
| Credits consumed | ${cost.creditsConsumed} credits |
|
|
103
|
+
| Credits returned | ${cost.creditsReturned} credits |
|
|
104
|
+
|
|
105
|
+
Pricing source: \`${cost.pricing.apiHost}\` at ${cost.pricing.capturedAt}. Balance sources: \`${cost.before.apiHost}\` and \`${cost.after.apiHost}\`.
|
|
106
|
+
|
|
107
|
+
> ${cost.accountingNote}
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
function selectExpiredAlephPlaywrightRunners(options) {
|
|
111
|
+
const owner = options.ownerAddress.trim().toLowerCase();
|
|
112
|
+
const repository = options.repository.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "-");
|
|
113
|
+
if (!/^0x[a-f0-9]{40}$/u.test(owner)) throw new Error("Janitor requires an exact EVM owner address");
|
|
114
|
+
if (!repository) throw new Error("Janitor requires a repository name");
|
|
115
|
+
const prefix = `playwright-${repository}-`;
|
|
116
|
+
const now = options.now ?? Date.now();
|
|
117
|
+
const ttlMs = options.ttlMs ?? 60 * 6e4;
|
|
118
|
+
if (!Number.isFinite(ttlMs) || ttlMs < 15 * 6e4) throw new Error("Janitor TTL must be at least 15 minutes");
|
|
119
|
+
const selection = { expired: [], retained: [] };
|
|
120
|
+
for (const candidate of options.candidates) {
|
|
121
|
+
let reason = "";
|
|
122
|
+
const createdAt = Date.parse(candidate.createdAt);
|
|
123
|
+
if (!/^[a-f0-9]{64}$/iu.test(candidate.itemHash)) reason = "invalid exact INSTANCE hash";
|
|
124
|
+
else if (candidate.ownerAddress.toLowerCase() !== owner) reason = "different owner";
|
|
125
|
+
else if (!candidate.instanceName.toLowerCase().startsWith(prefix)) reason = "name outside repository scope";
|
|
126
|
+
else if (!Number.isFinite(createdAt)) reason = "invalid creation timestamp";
|
|
127
|
+
else if (now - createdAt < ttlMs) reason = "within TTL";
|
|
128
|
+
else if (!["processed", "pending"].includes(candidate.status.toLowerCase())) reason = `terminal status ${candidate.status}`;
|
|
129
|
+
if (reason) selection.retained.push({ ...candidate, reason });
|
|
130
|
+
else selection.expired.push(candidate);
|
|
131
|
+
}
|
|
132
|
+
return selection;
|
|
133
|
+
}
|
|
34
134
|
async function waitForPubsubSubscriber(page, options) {
|
|
35
135
|
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
36
136
|
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
@@ -72,13 +172,13 @@ async function installEip1193WalletMock(context, account) {
|
|
|
72
172
|
case "eth_chainId":
|
|
73
173
|
return "0x1";
|
|
74
174
|
case "personal_sign": {
|
|
75
|
-
const payload = params.find(
|
|
76
|
-
(value) => typeof value === "string" && value.startsWith("0x") && value.toLowerCase() !== account.address.toLowerCase()
|
|
77
|
-
);
|
|
175
|
+
const payload = params.find((value) => typeof value === "string" && value.startsWith("0x") && value.toLowerCase() !== account.address.toLowerCase());
|
|
78
176
|
if (typeof payload !== "string") {
|
|
79
177
|
throw new Error("personal_sign did not contain a payload");
|
|
80
178
|
}
|
|
81
|
-
return account.signMessage({
|
|
179
|
+
return account.signMessage({
|
|
180
|
+
message: { raw: payload }
|
|
181
|
+
});
|
|
82
182
|
}
|
|
83
183
|
default:
|
|
84
184
|
throw new Error(`Unsupported E2E wallet method: ${method ?? "missing"}`);
|
|
@@ -119,19 +219,26 @@ var RelayButtonDriver = class {
|
|
|
119
219
|
};
|
|
120
220
|
}
|
|
121
221
|
deployButton() {
|
|
122
|
-
return this.page.getByRole("button", {
|
|
222
|
+
return this.page.getByRole("button", {
|
|
223
|
+
name: this.options.deployButtonName
|
|
224
|
+
});
|
|
123
225
|
}
|
|
124
226
|
instance(instanceName) {
|
|
125
227
|
return this.page.locator("details").filter({ hasText: instanceName }).first();
|
|
126
228
|
}
|
|
127
229
|
async prepare(options) {
|
|
128
|
-
const launcher = this.page.getByRole("button", {
|
|
230
|
+
const launcher = this.page.getByRole("button", {
|
|
231
|
+
name: this.options.launcherName
|
|
232
|
+
});
|
|
129
233
|
await launcher.waitFor({ state: "visible", timeout: 6e4 });
|
|
130
234
|
await launcher.click();
|
|
131
235
|
await this.page.getByPlaceholder(this.options.instanceNamePlaceholder).fill(options.instanceName);
|
|
132
236
|
await this.page.getByText("Advanced", { exact: true }).click();
|
|
133
237
|
await this.page.getByPlaceholder(this.options.sshPublicKeyPlaceholder).fill(options.sshPublicKey);
|
|
134
|
-
await this.page.getByRole("button", {
|
|
238
|
+
await this.page.getByRole("button", {
|
|
239
|
+
name: this.options.connectWalletName,
|
|
240
|
+
exact: true
|
|
241
|
+
}).click();
|
|
135
242
|
}
|
|
136
243
|
async requestDelete(instanceName) {
|
|
137
244
|
await this.page.getByRole("button", { name: this.options.refreshButtonName }).click().catch(() => {
|
|
@@ -154,9 +261,7 @@ async function waitForDeployableManifest(page, options = {}) {
|
|
|
154
261
|
const panelText = document.querySelector("aside")?.textContent ?? document.body.textContent ?? "";
|
|
155
262
|
const failure = states.find((state) => panelText.includes(state));
|
|
156
263
|
if (failure) return { status: "error", message: failure };
|
|
157
|
-
const deployButton = [...document.querySelectorAll("button")].find(
|
|
158
|
-
(button) => button.textContent?.trim() === "Deploy Relay"
|
|
159
|
-
);
|
|
264
|
+
const deployButton = [...document.querySelectorAll("button")].find((button) => button.textContent?.trim() === "Deploy Relay");
|
|
160
265
|
return deployButton && !deployButton.disabled ? { status: "ready" } : null;
|
|
161
266
|
},
|
|
162
267
|
{ states: terminalStates },
|
|
@@ -164,18 +269,14 @@ async function waitForDeployableManifest(page, options = {}) {
|
|
|
164
269
|
);
|
|
165
270
|
const result = await outcome.jsonValue();
|
|
166
271
|
if (result?.status === "error") {
|
|
167
|
-
throw new Error(
|
|
168
|
-
`Relay Button manifest is not deployable: ${result.message}. Republish the rootfs and update the manifest before provisioning.`
|
|
169
|
-
);
|
|
272
|
+
throw new Error(`Relay Button manifest is not deployable: ${result.message}. Republish the rootfs and update the manifest before provisioning.`);
|
|
170
273
|
}
|
|
171
274
|
}
|
|
172
275
|
async function waitForDeploymentUi(page, instanceName, timeoutMs) {
|
|
173
276
|
const outcome = await page.waitForFunction(
|
|
174
277
|
(expectedName) => {
|
|
175
278
|
const instance = [...document.querySelectorAll("details")].find(
|
|
176
|
-
(element) => element.textContent?.includes(expectedName) && [...element.querySelectorAll("button")].some(
|
|
177
|
-
(button) => button.textContent?.trim() === "Delete"
|
|
178
|
-
)
|
|
279
|
+
(element) => element.textContent?.includes(expectedName) && [...element.querySelectorAll("button")].some((button) => button.textContent?.trim() === "Delete")
|
|
179
280
|
);
|
|
180
281
|
if (instance?.textContent?.includes("Aleph bootstrap registered")) {
|
|
181
282
|
return { status: "instance" };
|
|
@@ -183,9 +284,7 @@ async function waitForDeploymentUi(page, instanceName, timeoutMs) {
|
|
|
183
284
|
const error = document.querySelector("aside.panel .alert.error")?.textContent?.trim();
|
|
184
285
|
if (error) return { status: "error", message: error };
|
|
185
286
|
const panelText = document.querySelector("aside")?.textContent ?? "";
|
|
186
|
-
const deployButton = [...document.querySelectorAll("button")].find(
|
|
187
|
-
(button) => button.textContent?.includes("Deploy")
|
|
188
|
-
);
|
|
287
|
+
const deployButton = [...document.querySelectorAll("button")].find((button) => button.textContent?.includes("Deploy"));
|
|
189
288
|
if (panelText.includes("Deployment failed") && !deployButton?.textContent?.includes("Deploying")) {
|
|
190
289
|
return { status: "error", message: panelText };
|
|
191
290
|
}
|
|
@@ -310,7 +409,9 @@ async function provisionRelay(page, options) {
|
|
|
310
409
|
instanceName: options.instanceName,
|
|
311
410
|
sshPublicKey: options.sshPublicKey
|
|
312
411
|
});
|
|
313
|
-
await waitForDeployableManifest(page, {
|
|
412
|
+
await waitForDeployableManifest(page, {
|
|
413
|
+
timeoutMs: options.manifestTimeoutMs
|
|
414
|
+
});
|
|
314
415
|
options.onPhase?.("wallet-and-manifest-ready");
|
|
315
416
|
await driver.deployButton().click();
|
|
316
417
|
options.onDeploymentSubmitted?.();
|
|
@@ -356,20 +457,14 @@ async function waitForAlephInstanceDeletion(options) {
|
|
|
356
457
|
if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
|
|
357
458
|
const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
|
|
358
459
|
const hosts = resolveAlephApiHosts(options.apiHosts);
|
|
359
|
-
const schedulerUrl = new URL(
|
|
360
|
-
`/api/v0/allocation/${options.instanceHash}`,
|
|
361
|
-
options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL
|
|
362
|
-
);
|
|
460
|
+
const schedulerUrl = new URL(`/api/v0/allocation/${options.instanceHash}`, options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL);
|
|
363
461
|
let lastSummary = "Deletion has not been observed yet.";
|
|
364
462
|
while (Date.now() < deadline) {
|
|
365
463
|
const observations = [];
|
|
366
464
|
let replicasForgotten = true;
|
|
367
465
|
for (const apiHost of hosts) {
|
|
368
466
|
try {
|
|
369
|
-
const response = await fetchImpl(
|
|
370
|
-
new URL(`/api/v0/messages/${options.instanceHash}`, apiHost),
|
|
371
|
-
{ cache: "no-cache" }
|
|
372
|
-
);
|
|
467
|
+
const response = await fetchImpl(new URL(`/api/v0/messages/${options.instanceHash}`, apiHost), { cache: "no-cache" });
|
|
373
468
|
const payload = await response.json().catch(() => null);
|
|
374
469
|
const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
|
|
375
470
|
replicasForgotten &&= forgotten;
|
|
@@ -392,9 +487,7 @@ async function waitForAlephInstanceDeletion(options) {
|
|
|
392
487
|
if (replicasForgotten && unallocated) return lastSummary;
|
|
393
488
|
await delay(options.pollIntervalMs ?? 2e3);
|
|
394
489
|
}
|
|
395
|
-
throw new Error(
|
|
396
|
-
`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`
|
|
397
|
-
);
|
|
490
|
+
throw new Error(`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`);
|
|
398
491
|
}
|
|
399
492
|
async function cleanupRelay(options) {
|
|
400
493
|
if (!/^[a-f0-9]{64}$/iu.test(options.instanceHash)) {
|
|
@@ -505,12 +598,7 @@ function createRelayEvidence(options) {
|
|
|
505
598
|
instanceName: options.instanceName,
|
|
506
599
|
ownerAddress: options.ownerAddress,
|
|
507
600
|
startedAt: new Date(options.startedAt ?? Date.now()).toISOString(),
|
|
508
|
-
steps: Object.fromEntries(
|
|
509
|
-
Object.entries(options.steps).map(([key, label]) => [
|
|
510
|
-
key,
|
|
511
|
-
{ label, status: "pending" }
|
|
512
|
-
])
|
|
513
|
-
)
|
|
601
|
+
steps: Object.fromEntries(Object.entries(options.steps).map(([key, label]) => [key, { label, status: "pending" }]))
|
|
514
602
|
};
|
|
515
603
|
}
|
|
516
604
|
function updateRelayEvidenceStep(evidence, step, status, detail = "") {
|
|
@@ -638,18 +726,23 @@ function createRelayTest(options) {
|
|
|
638
726
|
}
|
|
639
727
|
export {
|
|
640
728
|
DEFAULT_ALEPH_SCHEDULER_URL,
|
|
729
|
+
PLAYWRIGHT_RUNNER_VERSION,
|
|
641
730
|
RelayButtonDriver,
|
|
642
731
|
SUPPORTED_ALEPH_API_HOSTS,
|
|
643
732
|
appendRelayGithubSummary,
|
|
733
|
+
buildAlephCostEvidence,
|
|
644
734
|
cleanupRelay,
|
|
735
|
+
connectAlephChromium,
|
|
645
736
|
createRelayEvidence,
|
|
646
737
|
createRelayTest,
|
|
647
738
|
findAlephInstanceHash,
|
|
739
|
+
formatAlephCostGithubSummary,
|
|
648
740
|
formatRelayGithubSummary,
|
|
649
741
|
installEip1193WalletMock,
|
|
650
742
|
provisionRelay,
|
|
651
743
|
resolveAlephApiHosts,
|
|
652
744
|
selectBrowserRelayAddresses,
|
|
745
|
+
selectExpiredAlephPlaywrightRunners,
|
|
653
746
|
updateRelayEvidenceStep,
|
|
654
747
|
waitForAlephInstanceDeletion,
|
|
655
748
|
waitForBootstrapRegistration,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@le-space/playwright",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.29",
|
|
4
4
|
"description": "Reusable Playwright fixtures and Relay Button lifecycle helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"url": "https://github.com/NiKrause/relay-button/issues"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@le-space/aleph-bootstrap": "0.6.
|
|
28
|
-
"@le-space/core": "0.6.
|
|
27
|
+
"@le-space/aleph-bootstrap": "0.6.29",
|
|
28
|
+
"@le-space/core": "0.6.29"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@playwright/test": ">=1.61.1 <1.62.0"
|