diff --git a/tests/e2e/createIdentity.spec.ts b/tests/e2e/createIdentity.spec.ts index afeb2ce63..a335ff637 100644 --- a/tests/e2e/createIdentity.spec.ts +++ b/tests/e2e/createIdentity.spec.ts @@ -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(); +}); diff --git a/tests/e2e/repo/issueEditing.spec.ts b/tests/e2e/repo/issueEditing.spec.ts index 5e6ffa27a..0b8f1a88f 100644 --- a/tests/e2e/repo/issueEditing.spec.ts +++ b/tests/e2e/repo/issueEditing.spec.ts @@ -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(); +}); diff --git a/tests/e2e/repo/patchEditing.spec.ts b/tests/e2e/repo/patchEditing.spec.ts index 1f06afe92..453971483 100644 --- a/tests/e2e/repo/patchEditing.spec.ts +++ b/tests/e2e/repo/patchEditing.spec.ts @@ -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(); +}); diff --git a/tests/e2e/repo/permissions.spec.ts b/tests/e2e/repo/permissions.spec.ts index 6f3bb3de3..f34e09e84 100644 --- a/tests/e2e/repo/permissions.spec.ts +++ b/tests/e2e/repo/permissions.spec.ts @@ -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); +}); diff --git a/tests/e2e/seeding.spec.ts b/tests/e2e/seeding.spec.ts index 389fd1cf8..79b271696 100644 --- a/tests/e2e/seeding.spec.ts +++ b/tests/e2e/seeding.spec.ts @@ -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); +}); diff --git a/tests/support/collaboration.ts b/tests/support/collaboration.ts index 459098ed0..c9871d078 100644 --- a/tests/support/collaboration.ts +++ b/tests/support/collaboration.ts @@ -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 };