Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add e2e tests for editing patches, issues, comments, labels and assignees
Sebastian Martinez committed 2 years ago
commit 56c87787ed23cccb4cd965d8944adecfdfa9f908
parent 1188e84c87facb9becc99e4c20e9b330850fe052
11 files changed +759 -426
modified src/views/projects/Cob/AssigneeInput.svelte
@@ -98,6 +98,7 @@
      <div class="actions">
        {#if mode === "readEdit"}
          <IconButton
+
            title="save assignees"
            loading={submitInProgress}
            on:click={() => {
              dispatch("save", updatedAssignees);
@@ -106,6 +107,7 @@
            <IconSmall name="checkmark" />
          </IconButton>
          <IconButton
+
            title="dismiss changes"
            loading={submitInProgress}
            on:click={() => {
              updatedAssignees = assignees;
@@ -116,6 +118,7 @@
          </IconButton>
        {:else if mode !== "readCreate"}
          <IconButton
+
            title="edit assignees"
            loading={submitInProgress}
            on:click={() => (mode = "readEdit")}>
            <IconSmall name="edit" />
@@ -131,11 +134,11 @@
          <div class="assignee">
            <Avatar inline nodeId={assignee} />
            <span>{formatNodeId(assignee)}</span>
-
            <span style:cursor="pointer">
+
            <IconButton title="remove assignee">
              <IconSmall
                name="cross"
                on:click={() => removeAssignee(assignee)} />
-
            </span>
+
            </IconButton>
          </div>
        </Badge>
      {:else}
modified src/views/projects/Cob/LabelInput.svelte
@@ -120,9 +120,9 @@
      {#each updatedLabels as label}
        <Badge variant="neutral">
          <div aria-label="chip" class="label">{label}</div>
-
          <span style:cursor="pointer">
+
          <IconButton title="remove label">
            <IconSmall name="cross" on:click={() => removeLabel(label)} />
-
          </span>
+
          </IconButton>
        </Badge>
      {:else}
        <div class="txt-missing">No labels</div>
modified src/views/projects/Patch.svelte
@@ -675,7 +675,7 @@
                body={newDescription}
                submitCaption="Save"
                submitInProgress={descriptionState === "submit"}
-
                placeholder="Leave your description"
+
                placeholder="Leave a description"
                on:close={() => (descriptionState = "read")}
                on:submit={async ({ detail: { comment } }) => {
                  descriptionState = "submit";
added tests/e2e/project/assignees.spec.ts
@@ -0,0 +1,78 @@
+
import { test, expect } from "@tests/support/fixtures.js";
+
import { createProject } from "@tests/support/project";
+

+
test("add and remove assignees", async ({ page, authenticatedPeer }) => {
+
  await page.goto(authenticatedPeer.uiUrl());
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "handle-assignees",
+
  );
+
  await authenticatedPeer.rad(
+
    [
+
      "issue",
+
      "open",
+
      "--title",
+
      "This is an issue to test assignee handling",
+
      "--description",
+
      "We'll add and remove assignees to them",
+
    ],
+
    { cwd: projectFolder },
+
  );
+
  await page.goto(
+
    `${authenticatedPeer.uiUrl()}/${rid}/issues/4fe061828a067ae52bda43537ca61b7aeeb44563`,
+
  );
+

+
  await expect(page.getByText("No assignees")).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit assignees" }).click();
+
  await page
+
    .getByPlaceholder("Add assignee")
+
    .fill("z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S");
+
  await page.getByRole("button", { name: "dismiss changes" }).click();
+
  await expect(page.getByText("No assignees")).toBeVisible();
+
  await page.getByRole("button", { name: "edit assignees" }).click();
+
  await expect(page.getByPlaceholder("Add assignee")).toHaveValue("");
+

+
  await page
+
    .getByPlaceholder("Add assignee")
+
    .fill("z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S");
+
  await page.keyboard.press("Enter");
+
  await page
+
    .getByPlaceholder("Add assignee")
+
    .fill("z6MktULudTtAsAhRegYPiZ6631RV3viv12qd4GQF8z1xB22S");
+
  await expect(page.getByText("This assignee is already added")).toBeVisible();
+
  await page.getByPlaceholder("Add assignee").clear();
+
  await expect(page.getByText("This assignee is already added")).toBeHidden();
+

+
  await page
+
    .getByPlaceholder("Add assignee")
+
    .fill("z6MkkfM3tPXNPrPevKr3uSiQtHPuwnNhu2yUVjgd2jXVsVz5");
+
  await page.keyboard.press("Enter");
+
  await page.getByRole("button", { name: "save assignees" }).click();
+
  await expect(
+
    page.getByRole("button", { name: "save assignees" }),
+
  ).toBeHidden();
+
  await page.reload();
+
  await expect(
+
    page.getByRole("button", { name: "avatar did:key:z6MktU…1xB22S" }),
+
  ).toBeVisible();
+
  await expect(
+
    page.getByRole("button", { name: "avatar did:key:z6Mkkf…XVsVz5" }),
+
  ).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit assignees" }).click();
+
  await page
+
    .getByRole("button", {
+
      name: "avatar did:key:z6MktU…1xB22S remove assignee",
+
    })
+
    .getByTitle("remove assignee")
+
    .click();
+
  await page.getByRole("button", { name: "save assignees" }).click();
+
  await expect(
+
    page.getByRole("button", { name: "save assignees" }),
+
  ).toBeHidden();
+
  await page.reload();
+
  await expect(
+
    page.getByRole("button", { name: "avatar did:key:z6Mkkf…XVsVz5" }),
+
  ).toBeVisible();
+
});
added tests/e2e/project/issue.spec.ts
@@ -0,0 +1,161 @@
+
import { test, cobUrl, expect } from "@tests/support/fixtures.js";
+
import { createProject, expectReactionsToWork } from "@tests/support/project";
+

+
test("navigate single issue", async ({ page }) => {
+
  await page.goto(`${cobUrl}/issues`);
+
  await page.getByText("This title has markdown").click();
+

+
  await expect(page).toHaveURL(
+
    `${cobUrl}/issues/d72196335761c1d5fa7883f6620e7334b34e38f9`,
+
  );
+
});
+

+
test("test issue editing failing", async ({ page, authenticatedPeer }) => {
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "issue-editing",
+
  );
+
  await authenticatedPeer.rad(
+
    [
+
      "issue",
+
      "open",
+
      "--title",
+
      "This issue is going to fail",
+
      "--description",
+
      "Let's see",
+
    ],
+
    { cwd: projectFolder },
+
  );
+

+
  await page.route(
+
    `**/v1/projects/${rid}/issues/ecd5f103110b08b93bede17163d35de1e1068148`,
+
    route => {
+
      if (route.request().method() !== "PATCH") {
+
        void route.fallback();
+
        return;
+
      }
+
      void route.fulfill({ status: 500 });
+
    },
+
  );
+

+
  await page.goto(
+
    `${authenticatedPeer.uiUrl()}/${rid}/issues/ecd5f103110b08b93bede17163d35de1e1068148`,
+
  );
+

+
  await page.getByRole("button", { name: "Leave your comment" }).click();
+
  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
+
  await page.getByRole("button", { name: "Comment" }).first().click();
+
  await expect(page.getByText("Comment creation failed")).toBeVisible();
+
});
+

+
test("edit title", async ({ page, authenticatedPeer }) => {
+
  await page.goto(authenticatedPeer.uiUrl());
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "edit-title",
+
  );
+
  await authenticatedPeer.rad(
+
    [
+
      "issue",
+
      "open",
+
      "--title",
+
      "This is an issue to edit its title",
+
      "--description",
+
      "We'll give it a title and edit it.",
+
    ],
+
    { cwd: projectFolder },
+
  );
+
  await page.goto(
+
    `${authenticatedPeer.uiUrl()}/${rid}/issues/616e240787b6527780636caae581ca9975060733`,
+
  );
+

+
  await expect(
+
    page.getByText("This is an issue to edit its title"),
+
  ).toBeVisible();
+
  await expect(page.getByPlaceholder("Title")).toBeHidden();
+

+
  await page.getByRole("button", { name: "edit title" }).click();
+
  await page
+
    .getByPlaceholder("Title")
+
    .fill("This is a modified issue title to be dismissed");
+
  await page.getByRole("button", { name: "dismiss changes" }).click();
+
  await expect(
+
    page.getByText("This is an issue to edit its title"),
+
  ).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit title" }).click();
+
  await page.getByPlaceholder("Title").fill("This is a modified issue title");
+
  await page.getByRole("button", { name: "save title" }).click();
+
  await expect(page.getByRole("button", { name: "save title" })).toBeHidden();
+
  await page.reload();
+
  await expect(page.getByText("This is a modified issue title")).toBeVisible();
+
});
+

+
test("edit description", async ({ page, authenticatedPeer }) => {
+
  await page.goto(authenticatedPeer.uiUrl());
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "edit-description",
+
  );
+
  await authenticatedPeer.rad(
+
    [
+
      "issue",
+
      "open",
+
      "--title",
+
      "This is an issue to edit its description",
+
      "--description",
+
      "We'll give it a description and edit it.",
+
    ],
+
    { cwd: projectFolder },
+
  );
+
  await page.goto(
+
    `${authenticatedPeer.uiUrl()}/${rid}/issues/335e2823a71ac91203913a484dd771fd79f75139`,
+
  );
+

+
  await expect(
+
    page.getByText("We'll give it a description and edit it."),
+
  ).toBeVisible();
+
  await expect(page.getByPlaceholder("Leave a description")).toBeHidden();
+

+
  await page.getByRole("button", { name: "edit description" }).click();
+
  await page
+
    .getByPlaceholder("Leave a description")
+
    .fill("This is a modified issue description to be dismissed");
+
  await page.getByRole("button", { name: "Cancel" }).click();
+
  await expect(
+
    page.getByText("We'll give it a description and edit it."),
+
  ).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit description" }).click();
+
  await page
+
    .getByPlaceholder("Leave a description")
+
    .fill("This is a modified issue description");
+
  await page.getByRole("button", { name: "Save" }).click();
+
  await expect(page.getByRole("button", { name: "Save" })).toBeHidden();
+
  await page.reload();
+
  await expect(
+
    page.getByText("This is a modified issue description"),
+
  ).toBeVisible();
+
});
+

+
test("add and remove reactions", async ({ page, authenticatedPeer }) => {
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "handle-reactions",
+
  );
+
  await authenticatedPeer.rad(
+
    [
+
      "issue",
+
      "open",
+
      "--title",
+
      "This is an issue to test reactions",
+
      "--description",
+
      "We'll add and remove reactions to them",
+
    ],
+
    { cwd: projectFolder },
+
  );
+
  await page.goto(
+
    `${authenticatedPeer.uiUrl()}/${rid}/issues/cb5f9b2de24ecfdd293a607c96d78aacc911b589`,
+
  );
+
  await expectReactionsToWork(page);
+
});
modified tests/e2e/project/issues.spec.ts
@@ -1,6 +1,5 @@
import { test, cobUrl, expect } from "@tests/support/fixtures.js";
-
import { createProject } from "@tests/support/project";
-
import { readFile } from "node:fs/promises";
+
import { addEmbed, createProject } from "@tests/support/project";

test("navigate issue listing", async ({ page }) => {
  await page.goto(cobUrl);
@@ -12,60 +11,7 @@ test("navigate issue listing", async ({ page }) => {
  await expect(page).toHaveURL(`${cobUrl}/issues?state=closed`);
});

-
test("navigate single issue", async ({ page }) => {
-
  await page.goto(`${cobUrl}/issues`);
-
  await page.getByText("This title has markdown").click();
-

-
  await expect(page).toHaveURL(
-
    `${cobUrl}/issues/d72196335761c1d5fa7883f6620e7334b34e38f9`,
-
  );
-
});
-

-
test("adding and removing reactions", async ({ page, authenticatedPeer }) => {
-
  await page.goto(authenticatedPeer.uiUrl());
-
  const { rid, projectFolder } = await createProject(
-
    authenticatedPeer,
-
    "handle-reactions",
-
  );
-
  await authenticatedPeer.rad(
-
    [
-
      "issue",
-
      "open",
-
      "--title",
-
      "This is an issue to test reactions",
-
      "--description",
-
      "We'll write some comments and add and remove reactions to them",
-
    ],
-
    { cwd: projectFolder },
-
  );
-
  await page.goto(
-
    `${authenticatedPeer.uiUrl()}/${rid}/issues/a9d83773ac2fb8f5f654640477b9225a684cb53f`,
-
  );
-
  const commentReactionToggle = page
-
    .getByTitle("toggle-reaction-popover")
-
    .last();
-
  await page.getByRole("button", { name: "Leave your comment" }).click();
-
  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
-
  await page.getByRole("button", { name: "Comment" }).first().click();
-
  await commentReactionToggle.click();
-
  await page.getByRole("button", { name: "👍" }).click();
-
  await expect(page.getByRole("button", { name: "👍 1" })).toBeVisible();
-

-
  await commentReactionToggle.click();
-
  await page.getByRole("button", { name: "🎉" }).click();
-
  await expect(page.getByRole("button", { name: "🎉 1" })).toBeVisible();
-
  await expect(page.locator(".reaction")).toHaveCount(2);
-

-
  await page.getByRole("button", { name: "👍" }).click();
-
  await expect(page.locator("span").filter({ hasText: "👍 1" })).toBeHidden();
-
  await expect(page.locator(".reaction")).toHaveCount(1);
-

-
  await page.getByRole("button", { name: "🎉" }).click();
-
  await expect(page.locator("span").filter({ hasText: "🎉 1" })).toBeHidden();
-
  await expect(page.locator(".reaction")).toHaveCount(0);
-
});
-

-
test("test issue counters", async ({ page, authenticatedPeer }) => {
+
test("issue counters", async ({ page, authenticatedPeer }) => {
  const { rid, projectFolder } = await createProject(
    authenticatedPeer,
    "issue-counters",
@@ -108,48 +54,7 @@ test("test issue counters", async ({ page, authenticatedPeer }) => {
  await expect(page.getByRole("button", { name: "1 issue" })).toBeVisible();
});

-
test("test issue editing failing", async ({ page, authenticatedPeer }) => {
-
  const { rid, projectFolder } = await createProject(
-
    authenticatedPeer,
-
    "issue-editing",
-
  );
-
  await authenticatedPeer.rad(
-
    [
-
      "issue",
-
      "open",
-
      "--title",
-
      "This issue is going to fail",
-
      "--description",
-
      "Let's see",
-
    ],
-
    { cwd: projectFolder },
-
  );
-

-
  await page.route(
-
    `**/v1/projects/${rid}/issues/ecd5f103110b08b93bede17163d35de1e1068148`,
-
    route => {
-
      if (route.request().method() !== "PATCH") {
-
        void route.fallback();
-
        return;
-
      }
-
      void route.fulfill({ status: 500 });
-
    },
-
  );
-

-
  await page.goto(
-
    `${authenticatedPeer.uiUrl()}/${rid}/issues/ecd5f103110b08b93bede17163d35de1e1068148`,
-
  );
-

-
  await page.getByRole("button", { name: "Leave your comment" }).click();
-
  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
-
  await page.getByRole("button", { name: "Comment" }).first().click();
-
  await expect(page.getByText("Comment creation failed")).toBeVisible();
-
});
-

-
test("go through the entire ui issue flow", async ({
-
  page,
-
  authenticatedPeer,
-
}) => {
+
test("create a new issue", async ({ page, authenticatedPeer }) => {
  const { rid } = await createProject(authenticatedPeer, "commenting");

  await page.goto(
@@ -185,60 +90,23 @@ test("go through the entire ui issue flow", async ({
    page.locator(".badge").filter({ hasText: "documentation" }),
  ).toBeVisible();
  await expect(page.locator(".badge").filter({ hasText: "bug" })).toBeVisible();
-

-
  await page.getByRole("button", { name: "edit title" }).click();
-
  await page.getByPlaceholder("Title").fill("This is a new title");
-
  await page.getByRole("button", { name: "save title" }).click();
-
  await expect(page.getByText("This is a new title")).toBeVisible();
-

-
  await page.getByRole("button", { name: "Leave your comment" }).click();
-
  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
-
  await page.getByRole("button", { name: "Comment" }).first().click();
-
  await expect(page.getByText("This is a comment")).toBeVisible();
-

-
  await page.getByRole("button", { name: "Reply to comment" }).click();
-
  await page.getByPlaceholder("Reply to comment").fill("This is a reply");
-
  await page.getByRole("button", { name: "Comment", exact: true }).click();
-
  await expect(page.getByText("This is a reply")).toBeVisible();
-

-
  await page.getByRole("button", { name: "Close issue as solved" }).click();
-
  await expect(page.getByText("closed as solved")).toBeVisible();
-

-
  await page.getByRole("button", { name: "Reopen issue" }).click();
-
  await expect(page.getByText("open", { exact: true })).toBeVisible();
-

-
  await page.getByRole("button", { name: "stateToggle" }).first().click();
-
  await page.getByText("Close issue as other").click();
-
  await page.getByRole("button", { name: "Close issue as other" }).click();
-
  await expect(page.getByText("closed as other")).toBeVisible();
});

test("handling embeds", async ({ page, authenticatedPeer }) => {
-
  const buffer = await readFile("./public/images/radicle-228x228.png");
-
  const base64Data = buffer.toString("base64");
  const { rid } = await createProject(authenticatedPeer, "embeds");
-

  await page.goto(
    `/nodes/${authenticatedPeer.httpdBaseUrl.hostname}:${authenticatedPeer.httpdBaseUrl.port}/${rid}/issues/new`,
  );
-

-
  const dataTransfer = await page.evaluateHandle(data => {
-
    const arrayBuffer = Uint8Array.from(atob(data), c => c.charCodeAt(0));
-
    const dt = new DataTransfer();
-
    const file = new File([arrayBuffer.buffer], "radicle-228x228.png", {
-
      type: "image/png",
-
    });
-
    dt.items.add(file);
-
    return dt;
-
  }, base64Data);
-

  await page.getByPlaceholder("Title").fill("This is a title");
  await page
    .getByPlaceholder("Write a description")
    .fill("Here is some text\n\n");
-
  await page.dispatchEvent("textarea[aria-label=textarea-comment]", "drop", {
-
    dataTransfer,
-
  });
+
  await addEmbed(
+
    page,
+
    "./public/images/radicle-228x228.png",
+
    "radicle-228x228.png",
+
    "image/png",
+
  );
  await expect(page.getByPlaceholder("Write a description")).toHaveValue(
    "Here is some text\n\n![radicle-228x228.png](bae036309c2182c7304c97956969369823b5c6ad)\n",
  );
added tests/e2e/project/labels.spec.ts
@@ -0,0 +1,46 @@
+
import { test } from "@tests/support/fixtures.js";
+
import {
+
  createProject,
+
  expectLabelEditingToWork,
+
  extractPatchId,
+
} from "@tests/support/project";
+

+
test("add and remove labels", async ({ page, authenticatedPeer }) => {
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "handle-labels",
+
  );
+
  await authenticatedPeer.rad(
+
    [
+
      "issue",
+
      "open",
+
      "--title",
+
      "This is an issue to test label handling",
+
      "--description",
+
      "We'll add and remove labels to them",
+
    ],
+
    { cwd: projectFolder },
+
  );
+

+
  await page.goto(
+
    `${authenticatedPeer.uiUrl()}/${rid}/issues/5129d02476bc8c85f35e06a7d19dde487b0a8b13`,
+
  );
+
  await expectLabelEditingToWork(page);
+

+
  await authenticatedPeer.git(["switch", "-c", "handle-labels"], {
+
    cwd: projectFolder,
+
  });
+
  await authenticatedPeer.git(
+
    ["commit", "--allow-empty", "-m", "Some patch title"],
+
    {
+
      cwd: projectFolder,
+
    },
+
  );
+
  const patchId = extractPatchId(
+
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
+
      cwd: projectFolder,
+
    }),
+
  );
+
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
+
  await expectLabelEditingToWork(page);
+
});
added tests/e2e/project/patch.spec.ts
@@ -0,0 +1,246 @@
+
import { test, cobUrl, expect } from "@tests/support/fixtures.js";
+
import { createProject, extractPatchId } from "@tests/support/project";
+

+
test("navigate patch details", async ({ page }) => {
+
  await page.goto(`${cobUrl}/patches`);
+
  await page.getByText("Add subtitle to README").click();
+
  await expect(page).toHaveURL(
+
    `${cobUrl}/patches/1cd7fe9598c0a877c32c516bddb3de70dfb53366`,
+
  );
+
  await page.getByRole("link", { name: "Add subtitle to README" }).click();
+
  await expect(page).toHaveURL(
+
    `${cobUrl}/commits/8c900d6cb38811e099efb3cbbdbfaba817bcf970`,
+
  );
+
  await page.goBack();
+
  {
+
    await page.getByRole("link", { name: "Changes" }).click();
+
    await expect(page).toHaveURL(
+
      `${cobUrl}/patches/1cd7fe9598c0a877c32c516bddb3de70dfb53366?tab=changes`,
+
    );
+
  }
+
});
+

+
test("use revision selector", async ({ page }) => {
+
  await page.goto(`${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a`);
+
  await page.getByRole("link", { name: "Changes" }).click();
+

+
  // Validating the latest revision state
+
  await expect(
+
    page.getByRole("cell", { name: "Had to push a new revision" }),
+
  ).toBeVisible();
+
  await page.getByRole("link", { name: "Activity" }).click();
+
  await expect(page.locator(".commits .teaser")).toHaveCount(2);
+
  await expect(page.getByRole("link", { name: "Add more text" })).toBeVisible();
+

+
  // Open the first revision and close the latest one
+
  await page.getByLabel("expand").first().click();
+
  await page.getByLabel("expand").last().click();
+

+
  // Validating the initial revision
+
  await expect(page.locator(".commits .teaser")).toHaveCount(1);
+
  await expect(
+
    page.getByRole("link", { name: "Rewrite subtitle to README" }),
+
  ).toBeVisible();
+

+
  await page.getByRole("link", { name: "Changes" }).click();
+
  // Switching to the initial revision
+
  await page.getByText("Revision 2c2f036").click();
+
  await expect(page.locator(".dropdown")).toBeVisible();
+
  await page.getByRole("link", { name: "Revision 679b2c8" }).click();
+
  await expect(page.locator(".dropdown")).toBeHidden();
+

+
  await expect(
+
    page.getByRole("cell", { name: "Had to push a new revision" }),
+
  ).toBeHidden();
+

+
  await expect(page).toHaveURL(
+
    `${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a/679b2c84a8e15ce1f73c4c231b55431b89b2559a?tab=changes`,
+
  );
+
});
+

+
test("navigate through revision diffs", async ({ page }) => {
+
  await page.goto(`${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a`);
+

+
  const firstRevision = page.locator(".revision").first();
+
  const secondRevision = page.locator(".revision").nth(1);
+

+
  // Second revision
+
  {
+
    await secondRevision
+
      .getByRole("button", { name: "toggle-context-menu" })
+
      .first()
+
      .click();
+
    await secondRevision
+
      .getByRole("link", { name: "Compare to main: 38c225e" })
+
      .click();
+
    await expect(
+
      page.getByRole("link", { name: "Compare 38c225..9898da" }),
+
    ).toBeVisible();
+
    await expect(page).toHaveURL(
+
      `${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec..9898da6155467adad511f63bf0fb5aa4156b92ef`,
+
    );
+
    await page.goBack();
+
    await secondRevision
+
      .getByRole("button", { name: "toggle-context-menu" })
+
      .first()
+
      .click();
+
    await secondRevision
+
      .getByRole("link", { name: "Compare to previous revision: 679b2c8" })
+
      .click();
+
    await expect(
+
      page.getByRole("link", { name: "Compare 0dc373..9898da" }),
+
    ).toBeVisible();
+

+
    await expect(page).toHaveURL(
+
      `${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a?diff=0dc373db601ccbcffa80dec932e4006516709ca6..9898da6155467adad511f63bf0fb5aa4156b92ef`,
+
    );
+
    await page.goBack();
+

+
    await secondRevision
+
      .getByRole("link", { name: "Compare 0dc373d..9898da6" })
+
      .click();
+
    await expect(
+
      page.getByRole("link", { name: "Compare 0dc373..9898da" }),
+
    ).toBeVisible();
+
    await page.goBack();
+
  }
+
  // First revision
+
  {
+
    await firstRevision
+
      .getByRole("button", { name: "toggle-context-menu" })
+
      .first()
+
      .click();
+
    await firstRevision
+
      .getByRole("link", { name: "Compare to main: 38c225e" })
+
      .click();
+
    await expect(
+
      page.getByRole("link", { name: "Compare 38c225..0dc373" }),
+
    ).toBeVisible();
+
    await expect(page).toHaveURL(
+
      `${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec..0dc373db601ccbcffa80dec932e4006516709ca6`,
+
    );
+
  }
+
});
+

+
test("view file navigation from changes tab", async ({ page }) => {
+
  await page.goto(`${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a`);
+
  await page.getByRole("button", { name: "Changes" }).click();
+
  await page.getByRole("button", { name: "View file" }).click();
+
  await expect(page).toHaveURL(
+
    `${cobUrl}/tree/9898da6155467adad511f63bf0fb5aa4156b92ef/README.md`,
+
  );
+
});
+

+
test("change patch state", async ({ page, authenticatedPeer }) => {
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "lifecycle",
+
  );
+
  await authenticatedPeer.git(["switch", "-c", "feature-1"], {
+
    cwd: projectFolder,
+
  });
+
  await authenticatedPeer.git(
+
    ["commit", "--allow-empty", "-m", "Some patch title"],
+
    {
+
      cwd: projectFolder,
+
    },
+
  );
+
  const patchId = extractPatchId(
+
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
+
      cwd: projectFolder,
+
    }),
+
  );
+
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
+
  await page.getByRole("button", { name: "Archive patch" }).first().click();
+
  await expect(page.getByText("archived", { exact: true })).toBeVisible();
+
  await expect(page.getByRole("button", { name: "0 patches" })).toBeVisible();
+

+
  await page.getByLabel("stateToggle").first().click();
+
  await page.getByText("Convert to draft").click();
+
  await page.getByText("Convert to draft").click();
+
  await expect(page.getByText("draft", { exact: true })).toBeVisible();
+
  await expect(page.getByRole("button", { name: "0 patches" })).toBeVisible();
+
});
+

+
test("edit title", async ({ page, authenticatedPeer }) => {
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "edit-title",
+
  );
+
  await authenticatedPeer.git(["switch", "-c", "edit-title"], {
+
    cwd: projectFolder,
+
  });
+
  await authenticatedPeer.git(
+
    ["commit", "--allow-empty", "-m", "Some patch title"],
+
    {
+
      cwd: projectFolder,
+
    },
+
  );
+
  const patchId = extractPatchId(
+
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
+
      cwd: projectFolder,
+
    }),
+
  );
+
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
+

+
  const titleLocator = page.getByText("Some patch title").first();
+
  await expect(titleLocator).toBeVisible();
+
  await expect(page.getByPlaceholder("Title")).toBeHidden();
+

+
  await page.getByRole("button", { name: "edit title" }).click();
+
  await page
+
    .getByPlaceholder("Title")
+
    .fill("This is a modified patch title to be dismissed");
+
  await page.getByRole("button", { name: "dismiss changes" }).click();
+
  await expect(titleLocator).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit title" }).click();
+
  await page.getByPlaceholder("Title").fill("This is a modified patch title");
+
  await page.getByRole("button", { name: "save title" }).click();
+
  await expect(page.getByRole("button", { name: "save title" })).toBeHidden();
+
  await page.reload();
+
  await expect(page.getByText("This is a modified patch title")).toBeVisible();
+
});
+

+
test("edit description", async ({ page, authenticatedPeer }) => {
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "edit-description",
+
  );
+
  await authenticatedPeer.git(["switch", "-c", "edit-description"], {
+
    cwd: projectFolder,
+
  });
+
  await authenticatedPeer.git(
+
    ["commit", "--allow-empty", "-m", "Some patch title"],
+
    {
+
      cwd: projectFolder,
+
    },
+
  );
+
  const patchId = extractPatchId(
+
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
+
      cwd: projectFolder,
+
    }),
+
  );
+
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
+

+
  await expect(page.getByText("No description available")).toBeVisible();
+
  await expect(page.getByPlaceholder("Leave a description")).toBeHidden();
+

+
  await page.getByRole("button", { name: "edit description" }).click();
+
  await page
+
    .getByPlaceholder("Leave a description")
+
    .fill("This is a modified patch description to be dismissed");
+
  await page.getByRole("button", { name: "Cancel" }).click();
+
  await expect(page.getByText("No description available")).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit description" }).click();
+
  await page
+
    .getByPlaceholder("Leave a description")
+
    .fill("This is a modified patch description");
+
  await page.getByRole("button", { name: "Save" }).click();
+
  await expect(page.getByRole("button", { name: "Save" })).toBeHidden();
+
  await page.reload();
+
  await expect(
+
    page.getByText("This is a modified patch description"),
+
  ).toBeVisible();
+
});
modified tests/e2e/project/patches.spec.ts
@@ -1,16 +1,6 @@
-
import type { ExecaReturnValue } from "execa";
import { test, cobUrl, expect } from "@tests/support/fixtures.js";
import { createProject } from "@tests/support/project";

-
function extractPatchId(cmdOutput: ExecaReturnValue<string>) {
-
  const match = cmdOutput.stderr.match(/[0-9a-f]{40}/);
-
  if (match) {
-
    return match[0];
-
  } else {
-
    throw new Error("Could not get patch id");
-
  }
-
}
-

test("navigate patch listing", async ({ page }) => {
  await page.goto(cobUrl);
  await page.getByRole("link", { name: "2 patches" }).click();
@@ -24,165 +14,6 @@ test("navigate patch listing", async ({ page }) => {
  ).toBeVisible();
});

-
test("navigate patch details", async ({ page }) => {
-
  await page.goto(`${cobUrl}/patches`);
-
  await page.getByText("Add subtitle to README").click();
-
  await expect(page).toHaveURL(
-
    `${cobUrl}/patches/1cd7fe9598c0a877c32c516bddb3de70dfb53366`,
-
  );
-
  await page.getByRole("link", { name: "Add subtitle to README" }).click();
-
  await expect(page).toHaveURL(
-
    `${cobUrl}/commits/8c900d6cb38811e099efb3cbbdbfaba817bcf970`,
-
  );
-
  await page.goBack();
-
  {
-
    await page.getByRole("link", { name: "Changes" }).click();
-
    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/1cd7fe9598c0a877c32c516bddb3de70dfb53366?tab=changes`,
-
    );
-
  }
-
});
-

-
test("edit a patch", async ({ page, authenticatedPeer }) => {
-
  const { rid, projectFolder } = await createProject(
-
    authenticatedPeer,
-
    "commenting",
-
  );
-
  await authenticatedPeer.git(["switch", "-c", "feature-1"], {
-
    cwd: projectFolder,
-
  });
-
  await authenticatedPeer.git(
-
    ["commit", "--allow-empty", "-m", "Some patch title"],
-
    {
-
      cwd: projectFolder,
-
    },
-
  );
-
  const patchId = extractPatchId(
-
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
-
      cwd: projectFolder,
-
    }),
-
  );
-
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
-
  await expect(page.getByRole("button", { name: "1 patch" })).toBeVisible();
-
  await expect(page.getByText("open", { exact: true })).toBeVisible();
-

-
  await page.getByRole("button", { name: "edit labels" }).click();
-
  await page.getByPlaceholder("Add label").fill("bug");
-
  await page.getByPlaceholder("Add label").press("Enter");
-
  await page.getByPlaceholder("Add label").fill("documentation");
-
  await page.getByPlaceholder("Add label").press("Enter");
-
  await page.getByRole("button", { name: "save labels" }).click();
-

-
  await expect(
-
    page.locator(".badge").filter({ hasText: "documentation" }),
-
  ).toBeVisible();
-
  await expect(page.locator(".badge").filter({ hasText: "bug" })).toBeVisible();
-
});
-

-
test("leave a comment and reply", async ({ page, authenticatedPeer }) => {
-
  const { rid, projectFolder } = await createProject(
-
    authenticatedPeer,
-
    "commenting",
-
  );
-
  await authenticatedPeer.git(["switch", "-c", "feature-1"], {
-
    cwd: projectFolder,
-
  });
-
  await authenticatedPeer.git(
-
    ["commit", "--allow-empty", "-m", "Some patch title"],
-
    {
-
      cwd: projectFolder,
-
    },
-
  );
-
  const patchId = extractPatchId(
-
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
-
      cwd: projectFolder,
-
    }),
-
  );
-
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
-
  await page.getByRole("button", { name: "Leave your comment" }).click();
-
  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
-
  await page.getByRole("button", { name: "Comment" }).click();
-
  await expect(page.getByText("This is a comment")).toBeVisible();
-

-
  await page.getByRole("button", { name: "Reply to comment" }).click();
-
  await page.getByPlaceholder("Reply to comment").fill("This is a reply");
-
  await page.getByRole("button", { name: "Comment", exact: true }).click();
-
  await expect(page.getByText("This is a reply")).toBeVisible();
-
});
-

-
test("add and remove reactions", async ({ page, authenticatedPeer }) => {
-
  const { rid, projectFolder } = await createProject(
-
    authenticatedPeer,
-
    "reactions",
-
  );
-
  await authenticatedPeer.git(["switch", "-c", "feature-1"], {
-
    cwd: projectFolder,
-
  });
-
  await authenticatedPeer.git(
-
    ["commit", "--allow-empty", "-m", "Some patch title"],
-
    {
-
      cwd: projectFolder,
-
    },
-
  );
-
  const patchId = extractPatchId(
-
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
-
      cwd: projectFolder,
-
    }),
-
  );
-
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
-
  await page.getByRole("button", { name: "Leave your comment" }).click();
-
  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
-
  await page.getByRole("button", { name: "Comment" }).click();
-
  const commentReactionToggle = page.getByTitle("toggle-reaction").first();
-
  await commentReactionToggle.click();
-
  await page.getByRole("button", { name: "👍" }).click();
-
  await expect(page.getByRole("button", { name: "👍 1" })).toBeVisible();
-

-
  await commentReactionToggle.click();
-
  await page.getByRole("button", { name: "🎉" }).click();
-
  await expect(page.getByRole("button", { name: "🎉 1" })).toBeVisible();
-
  await expect(page.locator(".reaction")).toHaveCount(2);
-

-
  await page.getByRole("button", { name: "👍" }).click();
-
  await expect(page.locator("span").filter({ hasText: "👍 1" })).toBeHidden();
-
  await expect(page.locator(".reaction")).toHaveCount(1);
-

-
  await commentReactionToggle.click();
-
  await page.getByRole("button", { name: "🎉" }).first().click();
-
  await expect(page.getByRole("button", { name: "🎉 1" })).toBeHidden();
-
  await expect(page.locator(".reaction")).toHaveCount(0);
-
});
-
test("change patch state", async ({ page, authenticatedPeer }) => {
-
  const { rid, projectFolder } = await createProject(
-
    authenticatedPeer,
-
    "lifecycle",
-
  );
-
  await authenticatedPeer.git(["switch", "-c", "feature-1"], {
-
    cwd: projectFolder,
-
  });
-
  await authenticatedPeer.git(
-
    ["commit", "--allow-empty", "-m", "Some patch title"],
-
    {
-
      cwd: projectFolder,
-
    },
-
  );
-
  const patchId = extractPatchId(
-
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
-
      cwd: projectFolder,
-
    }),
-
  );
-
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
-
  await page.getByRole("button", { name: "Archive patch" }).first().click();
-
  await expect(page.getByText("archived", { exact: true })).toBeVisible();
-
  await expect(page.getByRole("button", { name: "0 patches" })).toBeVisible();
-

-
  await page.getByLabel("stateToggle").first().click();
-
  await page.getByText("Convert to draft").click();
-
  await page.getByText("Convert to draft").click();
-
  await expect(page.getByText("draft", { exact: true })).toBeVisible();
-
  await expect(page.getByRole("button", { name: "0 patches" })).toBeVisible();
-
});
-

test("patches counters", async ({ page, authenticatedPeer }) => {
  const { rid, projectFolder, defaultBranch } = await createProject(
    authenticatedPeer,
@@ -218,114 +49,3 @@ test("patches counters", async ({ page, authenticatedPeer }) => {
  ).toHaveText("2 open");
  await expect(page.locator(".list .patch-teaser")).toHaveCount(2);
});
-

-
test("use revision selector", async ({ page }) => {
-
  await page.goto(`${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a`);
-
  await page.getByRole("link", { name: "Changes" }).click();
-

-
  // Validating the latest revision state
-
  await expect(
-
    page.getByRole("cell", { name: "Had to push a new revision" }),
-
  ).toBeVisible();
-
  await page.getByRole("link", { name: "Activity" }).click();
-
  await expect(page.locator(".commits .teaser")).toHaveCount(2);
-
  await expect(page.getByRole("link", { name: "Add more text" })).toBeVisible();
-

-
  // Open the first revision and close the latest one
-
  await page.getByLabel("expand").first().click();
-
  await page.getByLabel("expand").last().click();
-

-
  // Validating the initial revision
-
  await expect(page.locator(".commits .teaser")).toHaveCount(1);
-
  await expect(
-
    page.getByRole("link", { name: "Rewrite subtitle to README" }),
-
  ).toBeVisible();
-

-
  await page.getByRole("link", { name: "Changes" }).click();
-
  // Switching to the initial revision
-
  await page.getByText("Revision 2c2f036").click();
-
  await expect(page.locator(".dropdown")).toBeVisible();
-
  await page.getByRole("link", { name: "Revision 679b2c8" }).click();
-
  await expect(page.locator(".dropdown")).toBeHidden();
-

-
  await expect(
-
    page.getByRole("cell", { name: "Had to push a new revision" }),
-
  ).toBeHidden();
-

-
  await expect(page).toHaveURL(
-
    `${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a/679b2c84a8e15ce1f73c4c231b55431b89b2559a?tab=changes`,
-
  );
-
});
-

-
test("navigate through revision diffs", async ({ page }) => {
-
  await page.goto(`${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a`);
-

-
  const firstRevision = page.locator(".revision").first();
-
  const secondRevision = page.locator(".revision").nth(1);
-

-
  // Second revision
-
  {
-
    await secondRevision
-
      .getByRole("button", { name: "toggle-context-menu" })
-
      .first()
-
      .click();
-
    await secondRevision
-
      .getByRole("link", { name: "Compare to main: 38c225e" })
-
      .click();
-
    await expect(
-
      page.getByRole("link", { name: "Compare 38c225..9898da" }),
-
    ).toBeVisible();
-
    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec..9898da6155467adad511f63bf0fb5aa4156b92ef`,
-
    );
-
    await page.goBack();
-
    await secondRevision
-
      .getByRole("button", { name: "toggle-context-menu" })
-
      .first()
-
      .click();
-
    await secondRevision
-
      .getByRole("link", { name: "Compare to previous revision: 679b2c8" })
-
      .click();
-
    await expect(
-
      page.getByRole("link", { name: "Compare 0dc373..9898da" }),
-
    ).toBeVisible();
-

-
    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a?diff=0dc373db601ccbcffa80dec932e4006516709ca6..9898da6155467adad511f63bf0fb5aa4156b92ef`,
-
    );
-
    await page.goBack();
-

-
    await secondRevision
-
      .getByRole("link", { name: "Compare 0dc373d..9898da6" })
-
      .click();
-
    await expect(
-
      page.getByRole("link", { name: "Compare 0dc373..9898da" }),
-
    ).toBeVisible();
-
    await page.goBack();
-
  }
-
  // First revision
-
  {
-
    await firstRevision
-
      .getByRole("button", { name: "toggle-context-menu" })
-
      .first()
-
      .click();
-
    await firstRevision
-
      .getByRole("link", { name: "Compare to main: 38c225e" })
-
      .click();
-
    await expect(
-
      page.getByRole("link", { name: "Compare 38c225..0dc373" }),
-
    ).toBeVisible();
-
    await expect(page).toHaveURL(
-
      `${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a?diff=38c225e2a0b47ba59def211f4e4825c31d9463ec..0dc373db601ccbcffa80dec932e4006516709ca6`,
-
    );
-
  }
-
});
-

-
test("view file navigation from changes tab", async ({ page }) => {
-
  await page.goto(`${cobUrl}/patches/679b2c84a8e15ce1f73c4c231b55431b89b2559a`);
-
  await page.getByRole("button", { name: "Changes" }).click();
-
  await page.getByRole("button", { name: "View file" }).click();
-
  await expect(page).toHaveURL(
-
    `${cobUrl}/tree/9898da6155467adad511f63bf0fb5aa4156b92ef/README.md`,
-
  );
-
});
added tests/e2e/project/threads.spec.ts
@@ -0,0 +1,69 @@
+
import { test } from "@tests/support/fixtures.js";
+
import {
+
  createProject,
+
  expectReactionsToWork,
+
  expectThreadCommentingToWork,
+
  extractPatchId,
+
} from "@tests/support/project";
+

+
test("leave comments and replies", async ({ page, authenticatedPeer }) => {
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "commenting",
+
  );
+
  await authenticatedPeer.rad(
+
    [
+
      "issue",
+
      "open",
+
      "--title",
+
      "This is an issue to write comments and replies",
+
      "--description",
+
      "We'll give it a few comments and replies.",
+
    ],
+
    { cwd: projectFolder },
+
  );
+
  await page.goto(
+
    `${authenticatedPeer.uiUrl()}/${rid}/issues/017f0c6827fb19e4cfd5103e452ba58665f01798`,
+
  );
+
  await expectThreadCommentingToWork(page);
+

+
  await authenticatedPeer.git(["switch", "-c", "feature-1"], {
+
    cwd: projectFolder,
+
  });
+
  await authenticatedPeer.git(
+
    ["commit", "--allow-empty", "-m", "Some patch title"],
+
    {
+
      cwd: projectFolder,
+
    },
+
  );
+
  const patchId = extractPatchId(
+
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
+
      cwd: projectFolder,
+
    }),
+
  );
+
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
+
  await expectThreadCommentingToWork(page);
+
});
+

+
test("add and remove reactions", async ({ page, authenticatedPeer }) => {
+
  const { rid, projectFolder } = await createProject(
+
    authenticatedPeer,
+
    "reactions",
+
  );
+
  await authenticatedPeer.git(["switch", "-c", "feature-1"], {
+
    cwd: projectFolder,
+
  });
+
  await authenticatedPeer.git(
+
    ["commit", "--allow-empty", "-m", "Some patch title"],
+
    {
+
      cwd: projectFolder,
+
    },
+
  );
+
  const patchId = extractPatchId(
+
    await authenticatedPeer.git(["push", "rad", "HEAD:refs/patches"], {
+
      cwd: projectFolder,
+
    }),
+
  );
+
  await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
+
  await expectReactionsToWork(page);
+
});
modified tests/support/project.ts
@@ -1,6 +1,10 @@
+
import type { Page } from "@playwright/test";
import type { RadiclePeer } from "@tests/support/peerManager";
+
import type { ExecaReturnValue } from "execa";

import * as Path from "node:path";
+
import { expect } from "@playwright/test";
+
import { readFileSync } from "node:fs";

// Create a project using the rad CLI.
export async function createProject(
@@ -39,3 +43,141 @@ export async function createProject(

  return { rid, projectFolder, defaultBranch };
}
+

+
export function extractPatchId(cmdOutput: ExecaReturnValue<string>) {
+
  const match = cmdOutput.stderr.match(/[0-9a-f]{40}/);
+
  if (match) {
+
    return match[0];
+
  } else {
+
    throw new Error("Could not get patch id");
+
  }
+
}
+

+
export async function expectThreadCommentingToWork(page: Page) {
+
  await page.getByRole("button", { name: "Leave your comment" }).click();
+
  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
+
  await page.getByRole("button", { name: "Comment" }).click();
+
  await expect(
+
    page.getByRole("button", { name: "Comment", exact: true }),
+
  ).toBeHidden();
+
  await page.reload();
+
  await expect(page.getByText("This is a comment")).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit comment" }).first().click();
+
  await page
+
    .getByPlaceholder("Leave your comment")
+
    .fill("This is an edited comment");
+
  await page.getByRole("button", { name: "Save" }).click();
+
  await expect(page.getByRole("button", { name: "Save" })).toBeHidden();
+
  await page.reload();
+
  await expect(page.getByText("This is an edited comment")).toBeVisible();
+

+
  await page.getByRole("button", { name: "Reply to comment" }).click();
+
  await page.getByPlaceholder("Reply to comment").fill("This is a reply");
+
  await page.getByRole("button", { name: "Comment", exact: true }).click();
+
  await expect(
+
    page.getByRole("button", { name: "Comment", exact: true }),
+
  ).toBeHidden();
+
  await page.reload();
+
  await expect(page.getByText("This is a reply")).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit comment" }).nth(1).click();
+
  await page
+
    .getByPlaceholder("Leave your comment")
+
    .fill("This is an edited reply");
+
  await page.getByRole("button", { name: "Save" }).click();
+
  await expect(page.getByRole("button", { name: "Save" })).toBeHidden();
+
  await page.reload();
+
  await expect(page.getByText("This is an edited reply")).toBeVisible();
+
}
+

+
export async function expectLabelEditingToWork(page: Page) {
+
  await expect(page.getByText("No labels")).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit labels" }).click();
+
  await page.getByPlaceholder("Add label").fill("bug");
+
  await page.getByRole("button", { name: "dismiss changes" }).click();
+
  await expect(page.getByText("No labels")).toBeVisible();
+
  await page.getByRole("button", { name: "edit labels" }).click();
+
  await expect(page.getByPlaceholder("Add label")).toHaveValue("");
+

+
  await page.getByPlaceholder("Add label").fill("bug");
+
  await page.keyboard.press("Enter");
+
  await page.getByPlaceholder("Add label").fill("bug");
+
  await expect(page.getByText("This label is already added")).toBeVisible();
+
  await page.getByPlaceholder("Add label").clear();
+
  await expect(page.getByText("This label is already added")).toBeHidden();
+

+
  await page.getByPlaceholder("Add label").fill("documentation");
+
  await page.keyboard.press("Enter");
+
  await page.getByRole("button", { name: "save labels" }).click();
+
  await expect(page.getByRole("button", { name: "save labels" })).toBeHidden();
+
  await page.reload();
+
  await expect(page.getByRole("button", { name: "bug" })).toBeVisible();
+
  await expect(
+
    page.getByRole("button", { name: "documentation" }),
+
  ).toBeVisible();
+

+
  await page.getByRole("button", { name: "edit labels" }).click();
+
  await page
+
    .locator("span")
+
    .filter({ hasText: "documentation" })
+
    .getByTitle("remove label")
+
    .click();
+
  await page.getByRole("button", { name: "save labels" }).click();
+
  await expect(page.getByRole("button", { name: "save labels" })).toBeHidden();
+
  await page.reload();
+
  await expect(page.getByRole("button", { name: "bug" })).toBeVisible();
+
}
+

+
export async function expectReactionsToWork(page: Page) {
+
  await page.getByRole("button", { name: "Leave your comment" }).click();
+
  await page.getByPlaceholder("Leave your comment").fill("This is a comment");
+
  await page.getByRole("button", { name: "Comment" }).click();
+
  const commentReactionToggle = page.getByTitle("toggle-reaction").first();
+
  await commentReactionToggle.click();
+
  await page.getByRole("button", { name: "👍" }).click();
+
  await expect(page.getByRole("button", { name: "👍 1" })).toBeVisible();
+

+
  await commentReactionToggle.click();
+
  await page.getByRole("button", { name: "🎉" }).click();
+
  await expect(page.getByRole("button", { name: "🎉 1" })).toBeVisible();
+
  await expect(page.locator(".reaction")).toHaveCount(2);
+

+
  await page.getByRole("button", { name: "👍" }).click();
+
  await expect(page.locator("span").filter({ hasText: "👍 1" })).toBeHidden();
+
  await expect(page.locator(".reaction")).toHaveCount(1);
+

+
  await commentReactionToggle.click();
+
  await page.getByRole("button", { name: "🎉" }).first().click();
+
  await expect(page.getByRole("button", { name: "🎉 1" })).toBeHidden();
+
  await expect(page.locator(".reaction")).toHaveCount(0);
+
}
+

+
export async function addEmbed(
+
  page: Page,
+
  url: string,
+
  fileName: string,
+
  fileType: string,
+
) {
+
  const buffer = readFileSync(url).toString("base64");
+
  const dataTransfer = await page.evaluateHandle(
+
    ({ buffer, localFileName, localFileType }) => {
+
      const arrayBuffer = Uint8Array.from(atob(buffer), c => c.charCodeAt(0));
+
      const dt = new DataTransfer();
+
      const file = new File([arrayBuffer.buffer], localFileName, {
+
        type: localFileType,
+
      });
+
      dt.items.add(file);
+
      return dt;
+
    },
+
    {
+
      buffer,
+
      localFileName: fileName,
+
      localFileType: fileType,
+
    },
+
  );
+
  await page.dispatchEvent("textarea[aria-label=textarea-comment]", "drop", {
+
    dataTransfer,
+
  });
+
}