Add e2e helpers for writes and a second peer

The UI updates before a write lands, so a reload right after could
cancel it; tests now wait for the command. WebKit also reports such
a cancelled fetch as "Load failed". A delegate and a connected
contributor let tests cover what non-delegates may do.
This commit is contained in:
Rūdolfs Ošiņš
2026-10-01 18:20:25 +02:00
parent 6608856eb2
commit 3edd0306b2
2 changed files with 92 additions and 2 deletions
+76
View File
@@ -0,0 +1,76 @@
import type { PeerManager, RadiclePeer } from "@tests/support/peerManager.js";
import * as Fs from "node:fs/promises";
import * as Path from "node:path";
import * as issue from "@tests/support/cobs/issue.js";
import * as patch from "@tests/support/cobs/patch.js";
import { defaultConfig, gitOptions } from "@tests/support/fixtures.js";
import { createRepo } from "@tests/support/repo.js";
// Bob maintains the repo; eve is connected to him and can clone it, but isn't
// a delegate.
export async function createCollaborators(peerManager: PeerManager) {
const bob = await peerManager.createPeer({
name: "bob",
gitOptions: gitOptions["bob"],
});
await bob.startNode();
const eve = await peerManager.createPeer({
name: "eve",
gitOptions: gitOptions["eve"],
});
await eve.startNode({
node: { ...defaultConfig.node, connect: [bob.address], alias: "eve" },
});
return { bob, eve };
}
// A repo with one file, one issue and one patch, all by `peer`.
export async function createProject(peer: RadiclePeer, name = "project") {
const { rid, repoFolder } = await createRepo(peer, { name });
const file = Path.join(repoFolder, "notes.txt");
await Fs.writeFile(file, "one\ntwo\nthree\nfour\nfive\n");
await peer.git(["add", "."], { cwd: repoFolder });
await peer.git(["commit", "-m", "Add notes"], { cwd: repoFolder });
await peer.git(["push", "rad", "main"], { cwd: repoFolder });
const issueId = await issue.create(
peer,
"The notes are too quiet",
"Nobody reads them.",
[],
{ cwd: repoFolder },
);
const patchId = await patch.create(
peer,
["Shout the third line"],
"feature/shout",
() => Fs.writeFile(file, "one\ntwo\nTHREE\nfour\nfive\n"),
[],
{ cwd: repoFolder },
);
return { rid, repoFolder, issueId, patchId };
}
export async function clone(peer: RadiclePeer, rid: string) {
await peer.rad(["clone", rid], { cwd: peer.checkoutPath });
}
// Resolves once `peer` has fetched `remote`'s changes to the COB `cobId`.
export async function waitForCobFrom(
peer: RadiclePeer,
remote: RadiclePeer,
rid: string,
cobId: string,
) {
await peer.waitForEvent(
event =>
event.type === "refsFetched" &&
event.rid === rid &&
event.remote === remote.nodeId &&
event.updated.some(update =>
Object.values(update).some(ref => ref.name.endsWith(`/${cobId}`)),
),
15_000,
);
}
+16 -2
View File
@@ -173,9 +173,9 @@ const reloading = new WeakSet<Page>();
const tearingDown = new WeakSet<Page>();
// WebKit reports a fetch that was cancelled because the page went away as an
// access control failure.
// access control failure, or as a failed load.
function isCancelledFetch(error: Error) {
return /Fetch API cannot load .* due to access control checks/.test(
return /Fetch API cannot load .* due to access control checks|Load failed/.test(
error.message + error.stack,
);
}
@@ -192,6 +192,20 @@ export async function reload(page: Page) {
}
}
// Runs `action` and waits for the backend command it sends to finish. The UI
// often updates before the write lands, and a reload would cancel it.
export async function waitForCommand(
page: Page,
command: string,
action: () => Promise<void>,
) {
const response = page.waitForResponse(
response => new URL(response.url()).pathname === `/${command}`,
);
await action();
await response;
}
// Call before the first `page.goto`.
export async function useBackend(page: Page, peer: RadiclePeer) {
await page.addInitScript(port => {