@le-space/playwright 0.6.27
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 +19 -0
- package/index.d.ts +184 -0
- package/index.js +659 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# `@le-space/playwright`
|
|
2
|
+
|
|
3
|
+
Shared Playwright fixtures and lifecycle helpers for Relay Button consumers.
|
|
4
|
+
|
|
5
|
+
The package keeps application scenarios local while consolidating wallet
|
|
6
|
+
injection, accessible Relay Button controls, Aleph INSTANCE/bootstrap lookup,
|
|
7
|
+
browser-address selection, PubSub readiness, evidence, and verified cleanup.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
createRelayTest,
|
|
12
|
+
installEip1193WalletMock,
|
|
13
|
+
waitForPubsubSubscriber,
|
|
14
|
+
type RelayWalletAccount,
|
|
15
|
+
} from '@le-space/playwright'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@playwright/test` is a peer dependency. Use a compatible 1.61.x client; remote
|
|
19
|
+
Playwright servers must use the exact same client/server version.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as _playwright_test from '@playwright/test';
|
|
2
|
+
import { Page, Locator, BrowserContext } from '@playwright/test';
|
|
3
|
+
import { RelayBootstrapPostRecord, RelayBootstrapContent, fetchAlephBootstrapPosts } from '@le-space/aleph-bootstrap';
|
|
4
|
+
import { eraseInstanceOnCrn, forgetAlephMessages } from '@le-space/core';
|
|
5
|
+
|
|
6
|
+
declare const SUPPORTED_ALEPH_API_HOSTS: readonly ["https://api2.aleph.im", "https://api.aleph.im"];
|
|
7
|
+
declare const DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
|
|
8
|
+
interface RelayWalletAccount {
|
|
9
|
+
address: string;
|
|
10
|
+
signMessage(args: {
|
|
11
|
+
message: string | {
|
|
12
|
+
raw: `0x${string}`;
|
|
13
|
+
};
|
|
14
|
+
}): Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
interface RelayEvidenceStep {
|
|
17
|
+
label: string;
|
|
18
|
+
status: 'pending' | 'passed' | 'failed' | 'skipped';
|
|
19
|
+
detail?: string;
|
|
20
|
+
}
|
|
21
|
+
interface RelayEvidence {
|
|
22
|
+
instanceName: string;
|
|
23
|
+
ownerAddress: string;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
finishedAt?: string;
|
|
26
|
+
instanceHash?: string;
|
|
27
|
+
steps: Record<string, RelayEvidenceStep>;
|
|
28
|
+
error?: string;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
interface RelayAddressPolicy {
|
|
32
|
+
allowWebTransport?: boolean;
|
|
33
|
+
allowWebRtcDirect?: boolean;
|
|
34
|
+
allowSecureWebSocket?: boolean;
|
|
35
|
+
requireCertificateHash?: boolean;
|
|
36
|
+
}
|
|
37
|
+
interface WaitForPubsubSubscriberOptions {
|
|
38
|
+
topic: string;
|
|
39
|
+
peerId: string;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
pollIntervalMs?: number;
|
|
42
|
+
stableForMs?: number;
|
|
43
|
+
}
|
|
44
|
+
interface RelayButtonDriverOptions {
|
|
45
|
+
launcherName?: RegExp;
|
|
46
|
+
instanceNamePlaceholder?: string;
|
|
47
|
+
sshPublicKeyPlaceholder?: string;
|
|
48
|
+
connectWalletName?: string;
|
|
49
|
+
deployButtonName?: string;
|
|
50
|
+
deleteButtonName?: string;
|
|
51
|
+
refreshButtonName?: string;
|
|
52
|
+
}
|
|
53
|
+
interface ProvisionRelayOptions {
|
|
54
|
+
accountAddress: string;
|
|
55
|
+
instanceName: string;
|
|
56
|
+
sshPublicKey: string;
|
|
57
|
+
startedAt?: number;
|
|
58
|
+
apiHosts?: readonly string[];
|
|
59
|
+
manifestTimeoutMs?: number;
|
|
60
|
+
provisionTimeoutMs?: number;
|
|
61
|
+
registrationTimeoutMs?: number;
|
|
62
|
+
registrationPollIntervalMs?: number;
|
|
63
|
+
driver?: RelayButtonDriver;
|
|
64
|
+
addressPolicy?: RelayAddressPolicy;
|
|
65
|
+
fetch?: typeof fetch;
|
|
66
|
+
onDeploymentSubmitted?: () => void;
|
|
67
|
+
onPhase?: (phase: 'wallet-and-manifest-ready' | 'deployment-submitted' | 'instance-resolved' | 'bootstrap-resolved', detail?: string) => void;
|
|
68
|
+
}
|
|
69
|
+
interface ProvisionedRelay {
|
|
70
|
+
instanceName: string;
|
|
71
|
+
instanceHash: string;
|
|
72
|
+
ownerAddress: string;
|
|
73
|
+
startedAt: number;
|
|
74
|
+
peerId: string;
|
|
75
|
+
addresses: string[];
|
|
76
|
+
registration: RelayBootstrapPostRecord;
|
|
77
|
+
driver: RelayButtonDriver;
|
|
78
|
+
}
|
|
79
|
+
interface CleanupRelayOptions {
|
|
80
|
+
page: Page;
|
|
81
|
+
account: RelayWalletAccount;
|
|
82
|
+
instanceName: string;
|
|
83
|
+
instanceHash: string;
|
|
84
|
+
driver?: RelayButtonDriver;
|
|
85
|
+
apiHosts?: readonly string[];
|
|
86
|
+
schedulerUrl?: string;
|
|
87
|
+
channel?: string;
|
|
88
|
+
uiGracePeriodMs?: number;
|
|
89
|
+
timeoutMs?: number;
|
|
90
|
+
pollIntervalMs?: number;
|
|
91
|
+
eraseFirst?: boolean;
|
|
92
|
+
fetch?: typeof fetch;
|
|
93
|
+
hooks?: CleanupRelayHooks;
|
|
94
|
+
}
|
|
95
|
+
interface CleanupRelayResult {
|
|
96
|
+
instanceHash: string;
|
|
97
|
+
uiDeleteRequested: boolean;
|
|
98
|
+
fallbackUsed: boolean;
|
|
99
|
+
eraseSummary: string;
|
|
100
|
+
forgetSummary: string;
|
|
101
|
+
verificationSummary: string;
|
|
102
|
+
}
|
|
103
|
+
interface CleanupRelayHooks {
|
|
104
|
+
erase?: typeof eraseInstanceOnCrn;
|
|
105
|
+
forget?: typeof forgetAlephMessages;
|
|
106
|
+
verify?: typeof waitForAlephInstanceDeletion;
|
|
107
|
+
}
|
|
108
|
+
interface RelayLifecycleFixture {
|
|
109
|
+
provision(page: Page, options: Omit<ProvisionRelayOptions, 'accountAddress'>): Promise<ProvisionedRelay>;
|
|
110
|
+
cleanupAll(): Promise<CleanupRelayResult[]>;
|
|
111
|
+
evidence: RelayEvidence;
|
|
112
|
+
}
|
|
113
|
+
interface CreateRelayTestOptions {
|
|
114
|
+
account: RelayWalletAccount;
|
|
115
|
+
evidence: RelayEvidence;
|
|
116
|
+
cleanup?: Omit<CleanupRelayOptions, 'page' | 'account' | 'instanceName' | 'instanceHash'>;
|
|
117
|
+
}
|
|
118
|
+
declare function resolveAlephApiHosts(candidates?: readonly string[]): string[];
|
|
119
|
+
declare function waitForPubsubSubscriber(page: Page, options: WaitForPubsubSubscriberOptions): Promise<string[]>;
|
|
120
|
+
declare function installEip1193WalletMock(context: BrowserContext, account: RelayWalletAccount): Promise<void>;
|
|
121
|
+
declare class RelayButtonDriver {
|
|
122
|
+
readonly page: Page;
|
|
123
|
+
readonly options: Required<RelayButtonDriverOptions>;
|
|
124
|
+
constructor(page: Page, options?: RelayButtonDriverOptions);
|
|
125
|
+
deployButton(): Locator;
|
|
126
|
+
instance(instanceName: string): Locator;
|
|
127
|
+
prepare(options: {
|
|
128
|
+
instanceName: string;
|
|
129
|
+
sshPublicKey: string;
|
|
130
|
+
}): Promise<void>;
|
|
131
|
+
requestDelete(instanceName: string): Promise<void>;
|
|
132
|
+
}
|
|
133
|
+
declare function waitForDeployableManifest(page: Page, options?: {
|
|
134
|
+
timeoutMs?: number;
|
|
135
|
+
terminalStates?: readonly string[];
|
|
136
|
+
}): Promise<void>;
|
|
137
|
+
declare function findAlephInstanceHash(options: {
|
|
138
|
+
ownerAddress: string;
|
|
139
|
+
instanceName: string;
|
|
140
|
+
startedAt: number;
|
|
141
|
+
apiHosts?: readonly string[];
|
|
142
|
+
timeoutMs?: number;
|
|
143
|
+
pollIntervalMs?: number;
|
|
144
|
+
fetch?: typeof fetch;
|
|
145
|
+
}): Promise<string>;
|
|
146
|
+
declare function waitForBootstrapRegistration(options: {
|
|
147
|
+
ownerAddress: string;
|
|
148
|
+
instanceName: string;
|
|
149
|
+
startedAt: number;
|
|
150
|
+
apiHosts?: readonly string[];
|
|
151
|
+
timeoutMs?: number;
|
|
152
|
+
pollIntervalMs?: number;
|
|
153
|
+
fetch?: typeof fetch;
|
|
154
|
+
fetchPosts?: typeof fetchAlephBootstrapPosts;
|
|
155
|
+
}): Promise<RelayBootstrapPostRecord>;
|
|
156
|
+
declare function selectBrowserRelayAddresses(content: Pick<RelayBootstrapContent, 'browserMultiaddrs' | 'multiaddrs'>, policy?: RelayAddressPolicy): string[];
|
|
157
|
+
declare function provisionRelay(page: Page, options: ProvisionRelayOptions): Promise<ProvisionedRelay>;
|
|
158
|
+
declare function waitForAlephInstanceDeletion(options: {
|
|
159
|
+
instanceHash: string;
|
|
160
|
+
apiHosts?: readonly string[];
|
|
161
|
+
schedulerUrl?: string;
|
|
162
|
+
timeoutMs?: number;
|
|
163
|
+
pollIntervalMs?: number;
|
|
164
|
+
fetch?: typeof fetch;
|
|
165
|
+
}): Promise<string>;
|
|
166
|
+
declare function cleanupRelay(options: CleanupRelayOptions): Promise<CleanupRelayResult>;
|
|
167
|
+
declare function createRelayEvidence(options: {
|
|
168
|
+
instanceName: string;
|
|
169
|
+
ownerAddress: string;
|
|
170
|
+
steps: Record<string, string>;
|
|
171
|
+
startedAt?: number;
|
|
172
|
+
}): RelayEvidence;
|
|
173
|
+
declare function updateRelayEvidenceStep(evidence: RelayEvidence, step: string, status: RelayEvidenceStep['status'], detail?: string): void;
|
|
174
|
+
declare function writeRelayEvidence(path: string, evidence: RelayEvidence): Promise<void>;
|
|
175
|
+
declare function formatRelayGithubSummary(evidence: RelayEvidence, title?: string): string;
|
|
176
|
+
declare function appendRelayGithubSummary(evidence: RelayEvidence, options?: {
|
|
177
|
+
path?: string;
|
|
178
|
+
title?: string;
|
|
179
|
+
}): Promise<string>;
|
|
180
|
+
declare function createRelayTest(options: CreateRelayTestOptions): _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & {
|
|
181
|
+
relayLifecycle: RelayLifecycleFixture;
|
|
182
|
+
}, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
|
|
183
|
+
|
|
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 };
|
package/index.js
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { appendFile, mkdir, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
import {
|
|
6
|
+
fetchAlephBootstrapPosts
|
|
7
|
+
} from "@le-space/aleph-bootstrap";
|
|
8
|
+
import { eraseInstanceOnCrn, forgetAlephMessages } from "@le-space/core";
|
|
9
|
+
import {
|
|
10
|
+
test as playwrightTest
|
|
11
|
+
} from "@playwright/test";
|
|
12
|
+
var SUPPORTED_ALEPH_API_HOSTS = [
|
|
13
|
+
"https://api2.aleph.im",
|
|
14
|
+
"https://api.aleph.im"
|
|
15
|
+
];
|
|
16
|
+
var DEFAULT_ALEPH_SCHEDULER_URL = "https://scheduler.api.aleph.cloud";
|
|
17
|
+
var API3_HOST_PATTERN = /(^|\.)api3\.aleph\.im$/iu;
|
|
18
|
+
function normalizeApiOrigin(value) {
|
|
19
|
+
try {
|
|
20
|
+
const url = new URL(value);
|
|
21
|
+
if (API3_HOST_PATTERN.test(url.hostname)) return null;
|
|
22
|
+
return url.origin;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function resolveAlephApiHosts(candidates) {
|
|
28
|
+
const allowed = new Set(
|
|
29
|
+
(candidates ?? SUPPORTED_ALEPH_API_HOSTS).map(normalizeApiOrigin).filter((value) => value != null)
|
|
30
|
+
);
|
|
31
|
+
const selected = SUPPORTED_ALEPH_API_HOSTS.filter((host) => allowed.has(host));
|
|
32
|
+
return selected.length > 0 ? [...selected] : [...SUPPORTED_ALEPH_API_HOSTS];
|
|
33
|
+
}
|
|
34
|
+
async function waitForPubsubSubscriber(page, options) {
|
|
35
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
36
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
37
|
+
const stableForMs = options.stableForMs ?? 1e3;
|
|
38
|
+
const deadline = Date.now() + timeoutMs;
|
|
39
|
+
let stableSince = null;
|
|
40
|
+
let subscribers = [];
|
|
41
|
+
while (Date.now() <= deadline) {
|
|
42
|
+
subscribers = await page.evaluate((topic) => {
|
|
43
|
+
const pubsub = window.libp2p?.services?.pubsub;
|
|
44
|
+
if (!pubsub) return [];
|
|
45
|
+
try {
|
|
46
|
+
return pubsub.getSubscribers(topic).map(String);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}, options.topic);
|
|
51
|
+
if (subscribers.includes(options.peerId)) {
|
|
52
|
+
stableSince ??= Date.now();
|
|
53
|
+
if (Date.now() - stableSince >= stableForMs) return subscribers;
|
|
54
|
+
} else {
|
|
55
|
+
stableSince = null;
|
|
56
|
+
}
|
|
57
|
+
const remainingMs = deadline - Date.now();
|
|
58
|
+
if (remainingMs <= 0) break;
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remainingMs)));
|
|
60
|
+
}
|
|
61
|
+
throw new Error(
|
|
62
|
+
`PubSub subscriber ${options.peerId} was not stable on ${options.topic} within ${timeoutMs}ms; last subscribers: ${subscribers.join(", ") || "none"}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
async function installEip1193WalletMock(context, account) {
|
|
66
|
+
await context.exposeBinding("__relayE2eWalletRequest", async (_source, request) => {
|
|
67
|
+
const { method, params = [] } = request;
|
|
68
|
+
switch (method) {
|
|
69
|
+
case "eth_requestAccounts":
|
|
70
|
+
case "eth_accounts":
|
|
71
|
+
return [account.address];
|
|
72
|
+
case "eth_chainId":
|
|
73
|
+
return "0x1";
|
|
74
|
+
case "personal_sign": {
|
|
75
|
+
const payload = params.find(
|
|
76
|
+
(value) => typeof value === "string" && value.startsWith("0x") && value.toLowerCase() !== account.address.toLowerCase()
|
|
77
|
+
);
|
|
78
|
+
if (typeof payload !== "string") {
|
|
79
|
+
throw new Error("personal_sign did not contain a payload");
|
|
80
|
+
}
|
|
81
|
+
return account.signMessage({ message: { raw: payload } });
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
throw new Error(`Unsupported E2E wallet method: ${method ?? "missing"}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
await context.addInitScript(() => {
|
|
88
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
89
|
+
Object.defineProperty(window, "ethereum", {
|
|
90
|
+
configurable: true,
|
|
91
|
+
value: {
|
|
92
|
+
isMetaMask: true,
|
|
93
|
+
request: (request) => window.__relayE2eWalletRequest(request),
|
|
94
|
+
on(event, listener) {
|
|
95
|
+
const eventListeners = listeners.get(event) ?? /* @__PURE__ */ new Set();
|
|
96
|
+
eventListeners.add(listener);
|
|
97
|
+
listeners.set(event, eventListeners);
|
|
98
|
+
},
|
|
99
|
+
removeListener(event, listener) {
|
|
100
|
+
listeners.get(event)?.delete(listener);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
var RelayButtonDriver = class {
|
|
107
|
+
page;
|
|
108
|
+
options;
|
|
109
|
+
constructor(page, options = {}) {
|
|
110
|
+
this.page = page;
|
|
111
|
+
this.options = {
|
|
112
|
+
launcherName: options.launcherName ?? /(?:Sponsor Relay|Relay Button|Relay)/,
|
|
113
|
+
instanceNamePlaceholder: options.instanceNamePlaceholder ?? "Instance name",
|
|
114
|
+
sshPublicKeyPlaceholder: options.sshPublicKeyPlaceholder ?? "SSH public key",
|
|
115
|
+
connectWalletName: options.connectWalletName ?? "Connect MetaMask",
|
|
116
|
+
deployButtonName: options.deployButtonName ?? "Deploy Relay",
|
|
117
|
+
deleteButtonName: options.deleteButtonName ?? "Delete",
|
|
118
|
+
refreshButtonName: options.refreshButtonName ?? "Refresh"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
deployButton() {
|
|
122
|
+
return this.page.getByRole("button", { name: this.options.deployButtonName });
|
|
123
|
+
}
|
|
124
|
+
instance(instanceName) {
|
|
125
|
+
return this.page.locator("details").filter({ hasText: instanceName }).first();
|
|
126
|
+
}
|
|
127
|
+
async prepare(options) {
|
|
128
|
+
const launcher = this.page.getByRole("button", { name: this.options.launcherName });
|
|
129
|
+
await launcher.waitFor({ state: "visible", timeout: 6e4 });
|
|
130
|
+
await launcher.click();
|
|
131
|
+
await this.page.getByPlaceholder(this.options.instanceNamePlaceholder).fill(options.instanceName);
|
|
132
|
+
await this.page.getByText("Advanced", { exact: true }).click();
|
|
133
|
+
await this.page.getByPlaceholder(this.options.sshPublicKeyPlaceholder).fill(options.sshPublicKey);
|
|
134
|
+
await this.page.getByRole("button", { name: this.options.connectWalletName, exact: true }).click();
|
|
135
|
+
}
|
|
136
|
+
async requestDelete(instanceName) {
|
|
137
|
+
await this.page.getByRole("button", { name: this.options.refreshButtonName }).click().catch(() => {
|
|
138
|
+
});
|
|
139
|
+
const instance = this.instance(instanceName);
|
|
140
|
+
await instance.waitFor({ state: "visible", timeout: 6e4 });
|
|
141
|
+
await instance.getByRole("button", { name: this.options.deleteButtonName, exact: true }).click();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
async function waitForDeployableManifest(page, options = {}) {
|
|
145
|
+
const terminalStates = options.terminalStates ?? [
|
|
146
|
+
"manifest rootfs not deployable",
|
|
147
|
+
"manifest invalid",
|
|
148
|
+
"not found on Aleph",
|
|
149
|
+
"Rootfs unavailable \u2014 deployment blocked",
|
|
150
|
+
"Rejected by Aleph"
|
|
151
|
+
];
|
|
152
|
+
const outcome = await page.waitForFunction(
|
|
153
|
+
({ states }) => {
|
|
154
|
+
const panelText = document.querySelector("aside")?.textContent ?? document.body.textContent ?? "";
|
|
155
|
+
const failure = states.find((state) => panelText.includes(state));
|
|
156
|
+
if (failure) return { status: "error", message: failure };
|
|
157
|
+
const deployButton = [...document.querySelectorAll("button")].find(
|
|
158
|
+
(button) => button.textContent?.trim() === "Deploy Relay"
|
|
159
|
+
);
|
|
160
|
+
return deployButton && !deployButton.disabled ? { status: "ready" } : null;
|
|
161
|
+
},
|
|
162
|
+
{ states: terminalStates },
|
|
163
|
+
{ timeout: options.timeoutMs ?? 12e4, polling: 500 }
|
|
164
|
+
);
|
|
165
|
+
const result = await outcome.jsonValue();
|
|
166
|
+
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
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function waitForDeploymentUi(page, instanceName, timeoutMs) {
|
|
173
|
+
const outcome = await page.waitForFunction(
|
|
174
|
+
(expectedName) => {
|
|
175
|
+
const instance = [...document.querySelectorAll("details")].find(
|
|
176
|
+
(element) => element.textContent?.includes(expectedName) && [...element.querySelectorAll("button")].some(
|
|
177
|
+
(button) => button.textContent?.trim() === "Delete"
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
if (instance?.textContent?.includes("Aleph bootstrap registered")) {
|
|
181
|
+
return { status: "instance" };
|
|
182
|
+
}
|
|
183
|
+
const error = document.querySelector("aside.panel .alert.error")?.textContent?.trim();
|
|
184
|
+
if (error) return { status: "error", message: error };
|
|
185
|
+
const panelText = document.querySelector("aside")?.textContent ?? "";
|
|
186
|
+
const deployButton = [...document.querySelectorAll("button")].find(
|
|
187
|
+
(button) => button.textContent?.includes("Deploy")
|
|
188
|
+
);
|
|
189
|
+
if (panelText.includes("Deployment failed") && !deployButton?.textContent?.includes("Deploying")) {
|
|
190
|
+
return { status: "error", message: panelText };
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
},
|
|
194
|
+
instanceName,
|
|
195
|
+
{ timeout: timeoutMs, polling: 500 }
|
|
196
|
+
);
|
|
197
|
+
const result = await outcome.jsonValue();
|
|
198
|
+
if (result?.status === "error") {
|
|
199
|
+
throw new Error(`Relay Button deployment failed: ${result.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function delay(ms) {
|
|
203
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
204
|
+
}
|
|
205
|
+
async function findAlephInstanceHash(options) {
|
|
206
|
+
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
207
|
+
if (!fetchImpl) throw new Error("A fetch implementation is required for INSTANCE lookup");
|
|
208
|
+
const deadline = Date.now() + (options.timeoutMs ?? 6e4);
|
|
209
|
+
const hosts = resolveAlephApiHosts(options.apiHosts);
|
|
210
|
+
let lastSummary = "No Aleph replica responded.";
|
|
211
|
+
while (Date.now() < deadline) {
|
|
212
|
+
const observations = [];
|
|
213
|
+
for (const apiHost of hosts) {
|
|
214
|
+
try {
|
|
215
|
+
const url = new URL("/api/v0/messages.json", apiHost);
|
|
216
|
+
url.searchParams.set("msgTypes", "INSTANCE");
|
|
217
|
+
url.searchParams.set("addresses", options.ownerAddress);
|
|
218
|
+
url.searchParams.set("message_statuses", "processed,pending,rejected");
|
|
219
|
+
url.searchParams.set("pagination", "100");
|
|
220
|
+
url.searchParams.set("page", "1");
|
|
221
|
+
url.searchParams.set("sortOrder", "-1");
|
|
222
|
+
const response = await fetchImpl(url, { cache: "no-cache" });
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
observations.push(`${apiHost}: HTTP ${response.status}`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const payload = await response.json();
|
|
228
|
+
const instance = payload.messages?.find((message) => {
|
|
229
|
+
const content = message.content;
|
|
230
|
+
const timestamp = Number(message.reception_time ?? message.time ?? 0) * 1e3;
|
|
231
|
+
return content?.metadata?.name === options.instanceName && timestamp >= options.startedAt - 6e4;
|
|
232
|
+
});
|
|
233
|
+
if (typeof instance?.item_hash === "string" && instance.item_hash) {
|
|
234
|
+
return instance.item_hash;
|
|
235
|
+
}
|
|
236
|
+
observations.push(`${apiHost}: no matching INSTANCE`);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
lastSummary = observations.join("; ");
|
|
242
|
+
await delay(options.pollIntervalMs ?? 2e3);
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`Could not resolve Aleph INSTANCE for ${options.instanceName}: ${lastSummary}`);
|
|
245
|
+
}
|
|
246
|
+
async function waitForBootstrapRegistration(options) {
|
|
247
|
+
const deadline = Date.now() + (options.timeoutMs ?? 9e4);
|
|
248
|
+
const hosts = resolveAlephApiHosts(options.apiHosts);
|
|
249
|
+
const fetchPosts = options.fetchPosts ?? fetchAlephBootstrapPosts;
|
|
250
|
+
let lastSummary = "No bootstrap posts returned.";
|
|
251
|
+
while (Date.now() < deadline) {
|
|
252
|
+
for (const apiHost of hosts) {
|
|
253
|
+
try {
|
|
254
|
+
const posts = await fetchPosts({
|
|
255
|
+
apiHost,
|
|
256
|
+
pagination: 200,
|
|
257
|
+
fetch: options.fetch
|
|
258
|
+
});
|
|
259
|
+
const registration = posts.find(({ address, content }) => {
|
|
260
|
+
if (!content) return false;
|
|
261
|
+
const owner = (content.ownerAddress ?? content.publisherAddress ?? address)?.toLowerCase();
|
|
262
|
+
const addresses = content.browserMultiaddrs?.length ? content.browserMultiaddrs : content.multiaddrs;
|
|
263
|
+
return owner === options.ownerAddress.toLowerCase() && content.registrationId?.includes(`:${options.instanceName}:`) && content.updatedAt >= options.startedAt - 6e4 && addresses.length > 0;
|
|
264
|
+
});
|
|
265
|
+
if (registration) return registration;
|
|
266
|
+
lastSummary = `${apiHost}: ${posts.length} posts checked`;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
lastSummary = `${apiHost}: ${error instanceof Error ? error.message : String(error)}`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
await delay(options.pollIntervalMs ?? 1e4);
|
|
272
|
+
}
|
|
273
|
+
throw new Error(`Relay bootstrap registration timed out: ${lastSummary}`);
|
|
274
|
+
}
|
|
275
|
+
function selectBrowserRelayAddresses(content, policy = {}) {
|
|
276
|
+
const resolvedPolicy = {
|
|
277
|
+
allowWebTransport: policy.allowWebTransport ?? true,
|
|
278
|
+
allowWebRtcDirect: policy.allowWebRtcDirect ?? true,
|
|
279
|
+
allowSecureWebSocket: policy.allowSecureWebSocket ?? true,
|
|
280
|
+
requireCertificateHash: policy.requireCertificateHash ?? true
|
|
281
|
+
};
|
|
282
|
+
const candidates = content.browserMultiaddrs?.length ? content.browserMultiaddrs : content.multiaddrs;
|
|
283
|
+
const rank = (address) => {
|
|
284
|
+
if (address.includes("/webtransport/")) return 0;
|
|
285
|
+
if (address.includes("/webrtc-direct/")) return 1;
|
|
286
|
+
if (address.includes(".libp2p.direct/")) return 2;
|
|
287
|
+
if (address.includes(".2n6.me/")) return 3;
|
|
288
|
+
return 4;
|
|
289
|
+
};
|
|
290
|
+
return [...new Set(candidates)].filter((address) => {
|
|
291
|
+
if (resolvedPolicy.allowSecureWebSocket && /\/(?:tls\/ws|wss)\/p2p\//u.test(address)) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
const hasPeer = /\/p2p\//u.test(address);
|
|
295
|
+
const hasCertificate = /\/certhash\//u.test(address);
|
|
296
|
+
const authenticated = !resolvedPolicy.requireCertificateHash || hasCertificate;
|
|
297
|
+
if (resolvedPolicy.allowWebTransport && /\/webtransport\//u.test(address)) {
|
|
298
|
+
return hasPeer && authenticated;
|
|
299
|
+
}
|
|
300
|
+
if (resolvedPolicy.allowWebRtcDirect && /\/webrtc-direct\//u.test(address)) {
|
|
301
|
+
return hasPeer && authenticated;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}).sort((left, right) => rank(left) - rank(right));
|
|
305
|
+
}
|
|
306
|
+
async function provisionRelay(page, options) {
|
|
307
|
+
const startedAt = options.startedAt ?? Date.now();
|
|
308
|
+
const driver = options.driver ?? new RelayButtonDriver(page);
|
|
309
|
+
await driver.prepare({
|
|
310
|
+
instanceName: options.instanceName,
|
|
311
|
+
sshPublicKey: options.sshPublicKey
|
|
312
|
+
});
|
|
313
|
+
await waitForDeployableManifest(page, { timeoutMs: options.manifestTimeoutMs });
|
|
314
|
+
options.onPhase?.("wallet-and-manifest-ready");
|
|
315
|
+
await driver.deployButton().click();
|
|
316
|
+
options.onDeploymentSubmitted?.();
|
|
317
|
+
options.onPhase?.("deployment-submitted");
|
|
318
|
+
await waitForDeploymentUi(page, options.instanceName, options.provisionTimeoutMs ?? 32 * 6e4);
|
|
319
|
+
const instanceHash = await findAlephInstanceHash({
|
|
320
|
+
ownerAddress: options.accountAddress,
|
|
321
|
+
instanceName: options.instanceName,
|
|
322
|
+
startedAt,
|
|
323
|
+
apiHosts: options.apiHosts,
|
|
324
|
+
fetch: options.fetch
|
|
325
|
+
});
|
|
326
|
+
options.onPhase?.("instance-resolved", instanceHash);
|
|
327
|
+
const registration = await waitForBootstrapRegistration({
|
|
328
|
+
ownerAddress: options.accountAddress,
|
|
329
|
+
instanceName: options.instanceName,
|
|
330
|
+
startedAt,
|
|
331
|
+
apiHosts: options.apiHosts,
|
|
332
|
+
timeoutMs: options.registrationTimeoutMs,
|
|
333
|
+
pollIntervalMs: options.registrationPollIntervalMs,
|
|
334
|
+
fetch: options.fetch
|
|
335
|
+
});
|
|
336
|
+
const content = registration.content;
|
|
337
|
+
if (!content?.peerId) throw new Error("Bootstrap registration did not include a peer ID");
|
|
338
|
+
const addresses = selectBrowserRelayAddresses(content, options.addressPolicy);
|
|
339
|
+
if (addresses.length === 0) {
|
|
340
|
+
throw new Error("Relay did not advertise an authenticated browser-dialable address");
|
|
341
|
+
}
|
|
342
|
+
options.onPhase?.("bootstrap-resolved", `${content.peerId}: ${addresses.join(", ")}`);
|
|
343
|
+
return {
|
|
344
|
+
instanceName: options.instanceName,
|
|
345
|
+
instanceHash,
|
|
346
|
+
ownerAddress: options.accountAddress,
|
|
347
|
+
startedAt,
|
|
348
|
+
peerId: content.peerId,
|
|
349
|
+
addresses,
|
|
350
|
+
registration,
|
|
351
|
+
driver
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async function waitForAlephInstanceDeletion(options) {
|
|
355
|
+
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
356
|
+
if (!fetchImpl) throw new Error("A fetch implementation is required for cleanup verification");
|
|
357
|
+
const deadline = Date.now() + (options.timeoutMs ?? 5 * 6e4);
|
|
358
|
+
const hosts = resolveAlephApiHosts(options.apiHosts);
|
|
359
|
+
const schedulerUrl = new URL(
|
|
360
|
+
`/api/v0/allocation/${options.instanceHash}`,
|
|
361
|
+
options.schedulerUrl ?? DEFAULT_ALEPH_SCHEDULER_URL
|
|
362
|
+
);
|
|
363
|
+
let lastSummary = "Deletion has not been observed yet.";
|
|
364
|
+
while (Date.now() < deadline) {
|
|
365
|
+
const observations = [];
|
|
366
|
+
let replicasForgotten = true;
|
|
367
|
+
for (const apiHost of hosts) {
|
|
368
|
+
try {
|
|
369
|
+
const response = await fetchImpl(
|
|
370
|
+
new URL(`/api/v0/messages/${options.instanceHash}`, apiHost),
|
|
371
|
+
{ cache: "no-cache" }
|
|
372
|
+
);
|
|
373
|
+
const payload = await response.json().catch(() => null);
|
|
374
|
+
const forgotten = payload?.status === "forgotten" || Boolean(payload?.forgotten_by?.length);
|
|
375
|
+
replicasForgotten &&= forgotten;
|
|
376
|
+
observations.push(`${apiHost}: ${forgotten ? "forgotten" : payload?.status ?? `HTTP ${response.status}`}`);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
replicasForgotten = false;
|
|
379
|
+
observations.push(`${apiHost}: ${error instanceof Error ? error.message : String(error)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
let unallocated = false;
|
|
383
|
+
try {
|
|
384
|
+
const response = await fetchImpl(schedulerUrl, { cache: "no-cache" });
|
|
385
|
+
const payload = await response.json().catch(() => null);
|
|
386
|
+
unallocated = response.status === 404 || payload?.error === "VM is not allocated to any node";
|
|
387
|
+
observations.push(`scheduler: ${unallocated ? "unallocated" : `HTTP ${response.status}`}`);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
observations.push(`scheduler: ${error instanceof Error ? error.message : String(error)}`);
|
|
390
|
+
}
|
|
391
|
+
lastSummary = observations.join("; ");
|
|
392
|
+
if (replicasForgotten && unallocated) return lastSummary;
|
|
393
|
+
await delay(options.pollIntervalMs ?? 2e3);
|
|
394
|
+
}
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Aleph INSTANCE ${options.instanceHash} was not deleted within ${options.timeoutMs ?? 5 * 6e4}ms: ${lastSummary}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
async function cleanupRelay(options) {
|
|
400
|
+
if (!/^[a-f0-9]{64}$/iu.test(options.instanceHash)) {
|
|
401
|
+
throw new Error(`Invalid Aleph INSTANCE hash: ${options.instanceHash}`);
|
|
402
|
+
}
|
|
403
|
+
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
404
|
+
if (!fetchImpl) throw new Error("A fetch implementation is required for Relay cleanup");
|
|
405
|
+
const driver = options.driver ?? new RelayButtonDriver(options.page);
|
|
406
|
+
const verify = options.hooks?.verify ?? waitForAlephInstanceDeletion;
|
|
407
|
+
const erase = options.hooks?.erase ?? eraseInstanceOnCrn;
|
|
408
|
+
const forget = options.hooks?.forget ?? forgetAlephMessages;
|
|
409
|
+
const hosts = resolveAlephApiHosts(options.apiHosts);
|
|
410
|
+
let uiDeleteRequested = false;
|
|
411
|
+
try {
|
|
412
|
+
await driver.requestDelete(options.instanceName);
|
|
413
|
+
uiDeleteRequested = true;
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
if (uiDeleteRequested) {
|
|
417
|
+
try {
|
|
418
|
+
const verificationSummary2 = await verify({
|
|
419
|
+
instanceHash: options.instanceHash,
|
|
420
|
+
apiHosts: hosts,
|
|
421
|
+
schedulerUrl: options.schedulerUrl,
|
|
422
|
+
timeoutMs: options.uiGracePeriodMs ?? 2e4,
|
|
423
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
424
|
+
fetch: fetchImpl
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
instanceHash: options.instanceHash,
|
|
428
|
+
uiDeleteRequested,
|
|
429
|
+
fallbackUsed: false,
|
|
430
|
+
eraseSummary: "Relay Button UI requested runtime erase",
|
|
431
|
+
forgetSummary: "Relay Button UI submitted FORGET",
|
|
432
|
+
verificationSummary: verificationSummary2
|
|
433
|
+
};
|
|
434
|
+
} catch {
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const signer = (_sender, payload) => options.account.signMessage({ message: payload });
|
|
438
|
+
let eraseSummary = "CRN erase skipped by configuration";
|
|
439
|
+
if (options.eraseFirst ?? true) {
|
|
440
|
+
let lastError = "No Aleph API host attempted.";
|
|
441
|
+
for (const apiHost of hosts) {
|
|
442
|
+
try {
|
|
443
|
+
const result = await erase({
|
|
444
|
+
sender: options.account.address,
|
|
445
|
+
signer,
|
|
446
|
+
instanceHash: options.instanceHash,
|
|
447
|
+
fetch: fetchImpl,
|
|
448
|
+
apiHost
|
|
449
|
+
});
|
|
450
|
+
eraseSummary = `CRN ${result.status}${result.crnUrl ? ` at ${result.crnUrl}` : ""}`;
|
|
451
|
+
lastError = "";
|
|
452
|
+
break;
|
|
453
|
+
} catch (error) {
|
|
454
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (lastError) eraseSummary = `CRN erase unavailable: ${lastError}`;
|
|
458
|
+
}
|
|
459
|
+
const hasher = (content) => createHash("sha256").update(content).digest("hex");
|
|
460
|
+
let forgetSummary = "";
|
|
461
|
+
let lastForgetError = "No Aleph API host attempted.";
|
|
462
|
+
for (const apiHost of hosts) {
|
|
463
|
+
try {
|
|
464
|
+
const result = await forget({
|
|
465
|
+
sender: options.account.address,
|
|
466
|
+
hashes: [options.instanceHash],
|
|
467
|
+
reason: "Relay Button Playwright cleanup",
|
|
468
|
+
signer,
|
|
469
|
+
hasher,
|
|
470
|
+
fetch: fetchImpl,
|
|
471
|
+
channel: options.channel,
|
|
472
|
+
apiHost,
|
|
473
|
+
sync: true
|
|
474
|
+
});
|
|
475
|
+
if (result.status === "rejected") {
|
|
476
|
+
throw new Error(`Aleph rejected FORGET: ${JSON.stringify(result.response)}`);
|
|
477
|
+
}
|
|
478
|
+
forgetSummary = `FORGET ${result.itemHash} ${result.status} via ${apiHost}`;
|
|
479
|
+
lastForgetError = "";
|
|
480
|
+
break;
|
|
481
|
+
} catch (error) {
|
|
482
|
+
lastForgetError = error instanceof Error ? error.message : String(error);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (lastForgetError) throw new Error(`Relay cleanup FORGET failed: ${lastForgetError}`);
|
|
486
|
+
const verificationSummary = await verify({
|
|
487
|
+
instanceHash: options.instanceHash,
|
|
488
|
+
apiHosts: hosts,
|
|
489
|
+
schedulerUrl: options.schedulerUrl,
|
|
490
|
+
timeoutMs: options.timeoutMs,
|
|
491
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
492
|
+
fetch: fetchImpl
|
|
493
|
+
});
|
|
494
|
+
return {
|
|
495
|
+
instanceHash: options.instanceHash,
|
|
496
|
+
uiDeleteRequested,
|
|
497
|
+
fallbackUsed: true,
|
|
498
|
+
eraseSummary,
|
|
499
|
+
forgetSummary,
|
|
500
|
+
verificationSummary
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function createRelayEvidence(options) {
|
|
504
|
+
return {
|
|
505
|
+
instanceName: options.instanceName,
|
|
506
|
+
ownerAddress: options.ownerAddress,
|
|
507
|
+
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
|
+
)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function updateRelayEvidenceStep(evidence, step, status, detail = "") {
|
|
517
|
+
const current = evidence.steps[step];
|
|
518
|
+
if (!current) throw new Error(`Unknown Relay evidence step: ${step}`);
|
|
519
|
+
evidence.steps[step] = { ...current, status, detail };
|
|
520
|
+
}
|
|
521
|
+
async function writeRelayEvidence(path, evidence) {
|
|
522
|
+
evidence.finishedAt ??= (/* @__PURE__ */ new Date()).toISOString();
|
|
523
|
+
await mkdir(dirname(path), { recursive: true });
|
|
524
|
+
await writeFile(path, `${JSON.stringify(evidence, null, 2)}
|
|
525
|
+
`);
|
|
526
|
+
}
|
|
527
|
+
function formatRelayGithubSummary(evidence, title = "Relay Button E2E") {
|
|
528
|
+
const icons = {
|
|
529
|
+
passed: "\u2705",
|
|
530
|
+
failed: "\u274C",
|
|
531
|
+
pending: "\u23F3",
|
|
532
|
+
skipped: "\u2796"
|
|
533
|
+
};
|
|
534
|
+
const rows = Object.values(evidence.steps).map((step) => {
|
|
535
|
+
const detail = String(step.detail ?? "").replaceAll("|", "\\|").replaceAll("\n", " ");
|
|
536
|
+
return `| ${icons[step.status]} | ${step.label} | ${detail || "\u2014"} |`;
|
|
537
|
+
});
|
|
538
|
+
const failed = Object.values(evidence.steps).some((step) => step.status === "failed") || evidence.error;
|
|
539
|
+
const metadata = [
|
|
540
|
+
`- Instance: \`${evidence.instanceName}\``,
|
|
541
|
+
evidence.instanceHash ? `- Aleph INSTANCE: \`${evidence.instanceHash}\`` : "",
|
|
542
|
+
evidence.error ? `- Error: ${evidence.error.replaceAll("\n", " ")}` : ""
|
|
543
|
+
].filter(Boolean);
|
|
544
|
+
return [
|
|
545
|
+
`## ${title}`,
|
|
546
|
+
"",
|
|
547
|
+
`**Result:** ${failed ? "\u274C Failed" : "\u2705 Passed"}`,
|
|
548
|
+
"",
|
|
549
|
+
"| Status | Test step | Details |",
|
|
550
|
+
"| --- | --- | --- |",
|
|
551
|
+
...rows,
|
|
552
|
+
"",
|
|
553
|
+
...metadata,
|
|
554
|
+
""
|
|
555
|
+
].join("\n") + "\n";
|
|
556
|
+
}
|
|
557
|
+
async function appendRelayGithubSummary(evidence, options = {}) {
|
|
558
|
+
const summary = formatRelayGithubSummary(evidence, options.title);
|
|
559
|
+
const path = options.path ?? process.env.GITHUB_STEP_SUMMARY;
|
|
560
|
+
if (path) await appendFile(path, summary);
|
|
561
|
+
return summary;
|
|
562
|
+
}
|
|
563
|
+
function createRelayTest(options) {
|
|
564
|
+
return playwrightTest.extend({
|
|
565
|
+
relayLifecycle: async ({}, use) => {
|
|
566
|
+
const tracked = [];
|
|
567
|
+
const lifecycle = {
|
|
568
|
+
evidence: options.evidence,
|
|
569
|
+
async provision(page, provisionOptions) {
|
|
570
|
+
const startedAt = provisionOptions.startedAt ?? Date.now();
|
|
571
|
+
try {
|
|
572
|
+
const relay = await provisionRelay(page, {
|
|
573
|
+
...provisionOptions,
|
|
574
|
+
accountAddress: options.account.address,
|
|
575
|
+
startedAt
|
|
576
|
+
});
|
|
577
|
+
tracked.push({ page, relay });
|
|
578
|
+
options.evidence.instanceHash = relay.instanceHash;
|
|
579
|
+
return relay;
|
|
580
|
+
} catch (error) {
|
|
581
|
+
const instanceHash = await findAlephInstanceHash({
|
|
582
|
+
ownerAddress: options.account.address,
|
|
583
|
+
instanceName: provisionOptions.instanceName,
|
|
584
|
+
startedAt,
|
|
585
|
+
apiHosts: provisionOptions.apiHosts,
|
|
586
|
+
fetch: provisionOptions.fetch,
|
|
587
|
+
timeoutMs: 15e3
|
|
588
|
+
}).catch(() => null);
|
|
589
|
+
if (instanceHash) {
|
|
590
|
+
tracked.push({
|
|
591
|
+
page,
|
|
592
|
+
relay: {
|
|
593
|
+
instanceName: provisionOptions.instanceName,
|
|
594
|
+
instanceHash,
|
|
595
|
+
ownerAddress: options.account.address,
|
|
596
|
+
startedAt,
|
|
597
|
+
peerId: "",
|
|
598
|
+
addresses: [],
|
|
599
|
+
registration: {},
|
|
600
|
+
driver: provisionOptions.driver ?? new RelayButtonDriver(page)
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
options.evidence.instanceHash = instanceHash;
|
|
604
|
+
}
|
|
605
|
+
throw error;
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
async cleanupAll() {
|
|
609
|
+
const results = [];
|
|
610
|
+
const errors = [];
|
|
611
|
+
for (const { page, relay } of tracked.reverse()) {
|
|
612
|
+
try {
|
|
613
|
+
results.push(
|
|
614
|
+
await cleanupRelay({
|
|
615
|
+
...options.cleanup,
|
|
616
|
+
page,
|
|
617
|
+
account: options.account,
|
|
618
|
+
instanceName: relay.instanceName,
|
|
619
|
+
instanceHash: relay.instanceHash,
|
|
620
|
+
driver: relay.driver
|
|
621
|
+
})
|
|
622
|
+
);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
tracked.length = 0;
|
|
628
|
+
if (errors.length > 0) {
|
|
629
|
+
throw new AggregateError(errors, "One or more Relay Button cleanups failed");
|
|
630
|
+
}
|
|
631
|
+
return results;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
await use(lifecycle);
|
|
635
|
+
await lifecycle.cleanupAll();
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
export {
|
|
640
|
+
DEFAULT_ALEPH_SCHEDULER_URL,
|
|
641
|
+
RelayButtonDriver,
|
|
642
|
+
SUPPORTED_ALEPH_API_HOSTS,
|
|
643
|
+
appendRelayGithubSummary,
|
|
644
|
+
cleanupRelay,
|
|
645
|
+
createRelayEvidence,
|
|
646
|
+
createRelayTest,
|
|
647
|
+
findAlephInstanceHash,
|
|
648
|
+
formatRelayGithubSummary,
|
|
649
|
+
installEip1193WalletMock,
|
|
650
|
+
provisionRelay,
|
|
651
|
+
resolveAlephApiHosts,
|
|
652
|
+
selectBrowserRelayAddresses,
|
|
653
|
+
updateRelayEvidenceStep,
|
|
654
|
+
waitForAlephInstanceDeletion,
|
|
655
|
+
waitForBootstrapRegistration,
|
|
656
|
+
waitForDeployableManifest,
|
|
657
|
+
waitForPubsubSubscriber,
|
|
658
|
+
writeRelayEvidence
|
|
659
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@le-space/playwright",
|
|
3
|
+
"version": "0.6.27",
|
|
4
|
+
"description": "Reusable Playwright fixtures and Relay Button lifecycle helpers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./index.js",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"import": "./index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/NiKrause/relay-button"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/NiKrause/relay-button",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/NiKrause/relay-button/issues"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@le-space/aleph-bootstrap": "0.6.27",
|
|
28
|
+
"@le-space/core": "0.6.27"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@playwright/test": ">=1.61.1 <1.62.0"
|
|
32
|
+
}
|
|
33
|
+
}
|