Test the remaining write paths and permissions

Reactions, review edits, patch comments, title, description and
assignees, stopping seeding, creating an identity, and that a
contributor can't resolve someone else's review comment.
This commit is contained in:
Rūdolfs Ošiņš
2026-10-01 18:20:25 +02:00
parent 47d55c4418
commit 49cf789cbc
6 changed files with 313 additions and 3 deletions
+39 -1
View File
@@ -1,4 +1,12 @@
import { expect, test } from "@tests/support/fixtures.js";
import * as Fs from "node:fs/promises";
import * as Path from "node:path";
import {
expect,
gitOptions,
test,
useBackend,
} from "@tests/support/fixtures.js";
test("explains why an alias is invalid", async ({ page }) => {
await page.route(
@@ -31,3 +39,33 @@ test("explains why an alias is invalid", async ({ page }) => {
page.getByText("Max 32 characters, no whitespace."),
).toBeVisible();
});
test("creates an identity and opens the guide", async ({
page,
peerManager,
sshAuthSock,
stateDir,
}) => {
const peer = await peerManager.createPeer({
name: "newcomer",
gitOptions: gitOptions["alice"],
sshAuthSock,
});
// Start from a node that has never had an identity.
const home = Path.join(stateDir, "peers", "newcomer", "home");
await Fs.rm(home, { recursive: true, force: true });
await Fs.mkdir(home, { recursive: true });
await peer.startHttpd();
await useBackend(page, peer);
await page.goto("/");
await page.getByPlaceholder("Enter desired alias").fill("newcomer");
await page
.getByPlaceholder("Enter passphrase to protect your keys")
.fill("secret");
await page.getByPlaceholder("Repeat passphrase").fill("secret");
await page.getByRole("button", { name: /Create new identity/ }).click();
await expect(page).toHaveURL(/\/guide/);
await expect(page.getByText("newcomer").first()).toBeVisible();
});
+12
View File
@@ -153,3 +153,15 @@ test("an issue can be deleted", async ({ page, peer }) => {
await expect(page.getByRole("link", { name: /Closed/ })).toBeVisible();
await expect(page.getByText("The notes are too quiet")).toBeHidden();
});
test("a reaction can be added to the description", async ({ page, peer }) => {
await openIssue(page, peer);
await page.getByText("Nobody reads them.").hover();
await page.getByTitle("React").first().click();
await waitForCommand(page, "edit_issue", () =>
page.getByRole("button", { name: "👍" }).click(),
);
await reload(page);
await expect(page.getByTitle("Remove reaction")).toBeVisible();
});
+144
View File
@@ -73,3 +73,147 @@ test("a patch can be deleted", async ({ page, peer }) => {
await expect(page.getByRole("link", { name: /Merged/ })).toBeVisible();
await expect(page.getByText("Shout the third line")).toBeHidden();
});
// Items in the menu behind a comment's "Comment actions" button.
async function commentAction(page: Page, body: string, action: string) {
await page.getByText(body).hover();
const toggle = page.getByTitle("Comment actions").last();
await toggle.click();
await toggle
.locator(
`xpath=following::*[@role="button"][normalize-space()="${action}"][1]`,
)
.click();
}
test("the title and description can be edited", async ({ page, peer }) => {
const { rid, patchId } = await openPatch(page, peer);
await page.getByRole("button", { name: "Shout the third line" }).click();
const input = page.locator("input:focus");
await input.fill("Shout the third line louder");
await waitForCommand(page, "edit_patch", () => input.press("Enter"));
const title = page.getByRole("button", {
name: "Shout the third line louder",
});
await expect(title).toBeVisible();
await page.goto(`/repos/${rid}/patches/${patchId}?view=changes`);
await page.getByText("Readers skim").hover();
await page.getByTitle("Edit description").click();
const editor = page.getByRole("textbox", { name: "textarea-comment" });
await editor.fill("Readers skim, so make it loud.");
await waitForCommand(page, "edit_patch", () =>
page.getByRole("button", { name: /^Save/ }).click(),
);
await expect(page.getByText("Readers skim, so make it loud.")).toBeVisible();
await reload(page);
await expect(title).toBeVisible();
await expect(page.getByText("Readers skim, so make it loud.")).toBeVisible();
});
test("a patch comment can be edited and deleted", async ({ page, peer }) => {
await openPatch(page, peer);
await page.getByPlaceholder("Leave a comment").fill("Needs more shouting");
await page.getByRole("button", { name: /^Comment/ }).click();
await expect(page.getByText("Needs more shouting")).toBeVisible();
await commentAction(page, "Needs more shouting", "Edit");
await page
.getByPlaceholder("Leave a comment")
.first()
.fill("Needs less shouting");
await waitForCommand(page, "edit_patch", () =>
page.getByRole("button", { name: /^Save/ }).click(),
);
await reload(page);
await expect(page.getByText("Needs less shouting")).toBeVisible();
await waitForCommand(page, "edit_patch", () =>
commentAction(page, "Needs less shouting", "Delete"),
);
await reload(page);
await expect(page.getByText("Needs less shouting")).toBeHidden();
});
test("an assignee can be added to a patch", async ({ page, peer }) => {
await openPatch(page, peer);
await page.getByRole("button", { name: "Add assignees" }).click();
const input = page.getByPlaceholder(/^Assignee DID/);
await input.fill(peer.nodeId);
await waitForCommand(page, "edit_patch", () => input.press("Enter"));
await reload(page);
await expect(
page.getByRole("button", {
name: "icon-avatar-incognito Assignees",
exact: true,
}),
).toBeVisible();
});
test("a reaction can be added to a patch comment", async ({ page, peer }) => {
await openPatch(page, peer);
await page.getByPlaceholder("Leave a comment").fill("Loud and clear");
await page.getByRole("button", { name: /^Comment/ }).click();
await page.getByText("Loud and clear").hover();
await page.getByTitle("React").last().click();
await waitForCommand(page, "edit_patch", () =>
page.getByRole("button", { name: "👍" }).click(),
);
await reload(page);
await expect(page.getByTitle("Remove reaction")).toBeVisible();
});
test("a published review's verdict and summary can be changed", async ({
page,
peer,
}) => {
const { rid, patchId } = await createProject(peer);
await page.goto(`/repos/${rid}/patches/${patchId}?view=changes`);
// Only reviews with code comments get a page of their own.
await page
.locator(
'[data-unified] [data-column-number="3"]:not([data-line-type="change-deletion"])',
)
.hover();
await page.locator("[data-utility-button]").click();
await page.getByPlaceholder("Leave a comment").fill("Loud enough");
await page
.getByLabel("extended-textarea")
.filter({ has: page.getByPlaceholder("Leave a comment") })
.getByRole("button", { name: /^Start review/ })
.click();
await page
.getByPlaceholder("Add an optional review summary")
.fill("Looks fine");
await waitForCommand(page, "create_patch_review", () =>
page.getByRole("button", { name: "Accept revision", exact: true }).click(),
);
// Publishing opens the review's own page.
await page.getByRole("button", { name: /Accepted/ }).click();
await waitForCommand(page, "edit_patch", () =>
page.getByRole("button", { name: /Reject$/ }).click(),
);
const rejected = page.getByRole("button", { name: /Rejected/ });
await expect(rejected).toBeVisible();
await page.getByText("Looks fine").hover();
await page.getByTitle("Edit summary").click();
await page
.getByRole("textbox", { name: "textarea-comment" })
.first()
.fill("Too loud");
await waitForCommand(page, "edit_patch", () =>
page.getByRole("button", { name: /^Save/ }).click(),
);
await reload(page);
await expect(rejected).toBeVisible();
await expect(page.getByText("Too loud")).toBeVisible();
});
+45 -1
View File
@@ -6,7 +6,12 @@ import {
createCollaborators,
createProject,
} from "@tests/support/collaboration.js";
import { expect, test, useBackend } from "@tests/support/fixtures.js";
import {
expect,
test,
useBackend,
waitForCommand,
} from "@tests/support/fixtures.js";
// Opens bob's project as eve, who cloned it but isn't a delegate.
async function asContributor(page: Page, peerManager: PeerManager) {
@@ -86,3 +91,42 @@ test("a contributor can't change a patch's state or delete it", async ({
await expect(page.getByRole("button", { name: "Add labels" })).toBeDisabled();
await expect(page.getByTitle("Delete patch from your node")).toBeHidden();
});
test("a contributor can't resolve someone else's review comment", async ({
page,
peerManager,
}) => {
const { bob, eve } = await createCollaborators(peerManager);
const { rid, patchId } = await createProject(bob);
await clone(eve, rid);
await bob.startHttpd();
await useBackend(page, bob);
// Bob reviews his own patch with a comment on the changed line.
await page.goto(`/repos/${rid}/patches/${patchId}?view=changes`);
await page
.locator(
'[data-unified] [data-column-number="3"]:not([data-line-type="change-deletion"])',
)
.hover();
await page.locator("[data-utility-button]").click();
await page.getByPlaceholder("Leave a comment").fill("Keep it loud");
await page
.getByLabel("extended-textarea")
.filter({ has: page.getByPlaceholder("Leave a comment") })
.getByRole("button", { name: /^Start review/ })
.click();
await waitForCommand(page, "create_patch_review", () =>
page.getByRole("button", { name: "Accept revision", exact: true }).click(),
);
await eve.rad(["sync", rid, "--fetch"]);
// Now as eve.
await eve.startHttpd();
await useBackend(page, eve);
await page.goto(`/repos/${rid}/patches/${patchId}?view=changes`);
const thread = page.getByRole("group").filter({ hasText: "Keep it loud" });
await thread.hover();
await expect(thread.getByTitle("Reply")).toBeVisible();
await expect(thread.getByTitle("Mark as resolved")).toHaveCount(0);
});
+72
View File
@@ -1,6 +1,13 @@
import type { Page } from "@playwright/test";
import type { PeerManager } from "@tests/support/peerManager.js";
import * as Fs from "node:fs/promises";
import * as Path from "node:path";
import { createCollaborators } from "@tests/support/collaboration.js";
import {
expect,
reload,
test,
useBackend,
waitForCommand,
@@ -35,3 +42,68 @@ test("seeds a repo from the network by its bare RID", async ({
page.getByText(/This repo is already (seeded|queued for fetching)/),
).toBeVisible();
});
async function stopSeeding(page: Page, name: string, deleteFiles: boolean) {
await page.getByRole("link", { name: new RegExp(name) }).click({
button: "right",
});
await page.getByRole("menuitem", { name: /Stop seeding/ }).click();
if (deleteFiles) {
await page.getByText("Delete the files from local storage").click();
}
await waitForCommand(page, deleteFiles ? "clean" : "unseed", () =>
page.getByRole("button", { name: /Stop seeding$/ }).click(),
);
await expect(page.getByRole("link", { name: new RegExp(name) })).toBeHidden();
}
async function seededFrom(peerManager: PeerManager, page: Page) {
const { bob, eve } = await createCollaborators(peerManager);
const { rid } = await createRepo(eve, { name: "garden" });
await bob.rad(["seed", rid]);
await bob.startHttpd();
await useBackend(page, bob);
await page.goto("/inbox");
return rid;
}
function storedRepo(stateDir: string, rid: string) {
return Path.join(
stateDir,
"peers",
"bob",
"home",
"storage",
rid.replace(/^rad:/, ""),
);
}
async function exists(path: string) {
return Fs.stat(path).then(
() => true,
() => false,
);
}
test("stopping seeding keeps the repo's files", async ({
page,
peerManager,
stateDir,
}) => {
const rid = await seededFrom(peerManager, page);
await stopSeeding(page, "garden", false);
await reload(page);
await expect(page.getByRole("link", { name: /garden/ })).toBeHidden();
expect(await exists(storedRepo(stateDir, rid))).toBe(true);
});
test("stopping seeding can delete the repo's files", async ({
page,
peerManager,
stateDir,
}) => {
const rid = await seededFrom(peerManager, page);
expect(await exists(storedRepo(stateDir, rid))).toBe(true);
await stopSeeding(page, "garden", true);
expect(await exists(storedRepo(stateDir, rid))).toBe(false);
});
+1 -1
View File
@@ -46,7 +46,7 @@ export async function createProject(peer: RadiclePeer, name = "project") {
["Shout the third line"],
"feature/shout",
() => Fs.writeFile(file, "one\ntwo\nTHREE\nfour\nfive\n"),
[],
["Shout the third line", "Readers skim, so the key line should stand out."],
{ cwd: repoFolder },
);
return { rid, repoFolder, issueId, patchId };