From 07bdaf8d3b69173e6e6f91371da1f6fc68947442 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Wed, 12 Aug 2026 00:04:44 -0400 Subject: [PATCH] desktop: add projects v3 PR screenshot spec Adds a mock-mode spec that captures the workspace overview, issues tab, issue detail, and PR detail states for PR screenshots. Mock event ids are now full 64-hex so share-link copy buttons render in mock mode, and post-screenshots.sh no longer needs bash 4 (mapfile/declare -A) so it runs on stock macOS bash 3.2. Signed-off-by: Thomas Petersen --- desktop/playwright.config.ts | 1 + desktop/src/testing/e2eBridge.ts | 4 +- .../tests/e2e/projects-v3-screenshots.spec.ts | 77 +++++++++++++++++++ scripts/post-screenshots.sh | 32 ++++---- 4 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 desktop/tests/e2e/projects-v3-screenshots.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index a9ec17a249..df5956f62b 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -108,6 +108,7 @@ export default defineConfig({ "**/send-channel-binding.spec.ts", "**/project-commit-detail.spec.ts", "**/project-inbox.spec.ts", + "**/projects-v3-screenshots.spec.ts", "**/project-issue-comments.spec.ts", "**/project-pr-review.spec.ts", "**/persona-model-combobox-screenshots.spec.ts", diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 46bda44ffb..ef600669ad 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -5571,7 +5571,9 @@ function createMockEvent( tags: string[][], pubkey = DEFAULT_MOCK_IDENTITY.pubkey, createdAt = Math.floor(Date.now() / 1000), - id = crypto.randomUUID().replace(/-/g, ""), + // 64 hex chars like a real event id — share-link builders reject shorter + // ids, so copy-link buttons only render with full-length ids. + id = (crypto.randomUUID() + crypto.randomUUID()).replace(/-/g, ""), ): RelayEvent { return { id, diff --git a/desktop/tests/e2e/projects-v3-screenshots.spec.ts b/desktop/tests/e2e/projects-v3-screenshots.spec.ts new file mode 100644 index 0000000000..4aa2bb5792 --- /dev/null +++ b/desktop/tests/e2e/projects-v3-screenshots.spec.ts @@ -0,0 +1,77 @@ +import { expect, test } from "@playwright/test"; + +import { waitForAnimations } from "../helpers/animations"; +import { installMockBridge } from "../helpers/bridge"; + +const SHOTS = "test-results/projects-v3-screenshots"; + +async function openBuzzProject(page: import("@playwright/test").Page) { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.getByTestId("open-projects-view").click(); + await page.getByTestId("projects-section-projects").click(); + const projectEntry = page + .locator( + '[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]', + ) + .first(); + await expect(projectEntry).toBeVisible({ timeout: 10_000 }); + await projectEntry.click(); +} + +// Walks the Projects v3 workspace through its headline states so PR +// screenshots capture distinct pixels per feature (overview box, tab-strip +// plus, issue detail with inline copy link + avatar timeline, PR detail). +test("projects v3 workspace screenshot states", async ({ page }) => { + await installMockBridge(page); + await openBuzzProject(page); + + // Single-box workspace: pickers + tab strip + readme overview. + await expect(page.getByRole("tab", { name: "Files" })).toBeVisible(); + await waitForAnimations(page); + await page.screenshot({ path: `${SHOTS}/01-workspace-overview.png` }); + + // Issues tab: the create action lives in the tab strip now. + await page.getByRole("tab", { name: "Issues", exact: true }).click(); + await expect(page.getByRole("button", { name: "New issue" })).toBeVisible(); + const issueRow = page.getByTestId("project-issue-row").first(); + await expect(issueRow).toBeVisible({ timeout: 10_000 }); + await waitForAnimations(page); + await page.screenshot({ path: `${SHOTS}/02-issues-tab.png` }); + + // Issue detail: inline copy link after the title, assignees rail, and the + // avatar comment timeline (seed comments so the timeline renders). + await issueRow.getByRole("button", { name: /^#/ }).click(); + const composer = page.getByTestId("project-issue-comment-composer"); + await expect(composer).toBeVisible(); + for (const comment of [ + "The palette needs the most work — starting there.", + "Agreed. Typography pass can land separately.", + ]) { + await composer.locator('[contenteditable="true"]').fill(comment); + await composer.getByRole("button", { name: "Send message" }).click(); + await expect(page.getByText(comment, { exact: true })).toBeVisible({ + timeout: 10_000, + }); + } + await expect(page.getByTestId("project-issue-copy-link")).toBeVisible(); + await expect( + page.getByTestId("project-issue-comment-timeline-row").first(), + ).toBeVisible(); + // Let the "Comment posted." toast dismiss so it doesn't photobomb. + await expect(page.getByText("Comment posted.")).toHaveCount(0, { + timeout: 10_000, + }); + await waitForAnimations(page); + await page.screenshot({ path: `${SHOTS}/03-issue-detail.png` }); + + // PR detail: inline copy link after the PR title. + await page.getByRole("tab", { name: "Pull Request", exact: true }).click(); + const prRow = page.getByTestId("project-pull-request-row").first(); + await expect(prRow).toBeVisible({ timeout: 10_000 }); + await prRow.getByRole("button", { name: /^#/ }).click(); + await expect( + page.getByTestId("project-pull-request-copy-link"), + ).toBeVisible(); + await waitForAnimations(page); + await page.screenshot({ path: `${SHOTS}/04-pr-detail.png` }); +}); diff --git a/scripts/post-screenshots.sh b/scripts/post-screenshots.sh index c34c496b7a..993628518f 100755 --- a/scripts/post-screenshots.sh +++ b/scripts/post-screenshots.sh @@ -19,7 +19,11 @@ GH_USER=$(gh api user --jq .login) BRANCH="agent-screenshots/${GH_USER}" REPO="block/buzz" -mapfile -t PNGS < <(find "$PNG_DIR" -maxdepth 1 -name "*.png" -type f | sort) +# macOS ships bash 3.2, which lacks mapfile — build the array with read. +PNGS=() +while IFS= read -r PNG; do + PNGS+=("$PNG") +done < <(find "$PNG_DIR" -maxdepth 1 -name "*.png" -type f | sort) if [[ ${#PNGS[@]} -eq 0 ]]; then echo "error: no PNGs found in $PNG_DIR" >&2 exit 1 @@ -51,18 +55,18 @@ PARENT_ARGS=() if git rev-parse "origin/${BRANCH}" >/dev/null 2>&1; then PARENT_ARGS=(-p "origin/${BRANCH}") fi -COMMIT=$(git commit-tree "$TREE" "${PARENT_ARGS[@]}" -m "screenshots: PR #${PR}") +# ${arr[@]+...} guards the empty-array case, which trips set -u on bash 3.2. +COMMIT=$(git commit-tree "$TREE" ${PARENT_ARGS[@]+"${PARENT_ARGS[@]}"} -m "screenshots: PR #${PR}") git push --force-with-lease origin "${COMMIT}:refs/heads/${BRANCH}" RAW_BASE="https://raw.githubusercontent.com/${REPO}/${COMMIT}" -declare -A IMAGE_URL_MAP +# Parallel name/url arrays instead of an associative array (bash 4+ only). +IMAGE_NAMES=() IMAGE_URLS=() for i in "${!PNGS[@]}"; do - ORIG_NAME="$(basename "${PNGS[$i]}" .png)" - URL="${RAW_BASE}/${TREE_PATHS[$i]}" - IMAGE_URLS+=("$URL") - IMAGE_URL_MAP["$ORIG_NAME"]="$URL" + IMAGE_NAMES+=("$(basename "${PNGS[$i]}" .png)") + IMAGE_URLS+=("${RAW_BASE}/${TREE_PATHS[$i]}") done if [[ -n "$BODY_FILE" ]]; then @@ -70,20 +74,20 @@ if [[ -n "$BODY_FILE" ]]; then "$SCRIPT_DIR/check-pr-image-urls.sh" "$BODY_FILE" COMMENT_BODY="$(cat "$BODY_FILE")" UNREFERENCED=() - for NAME in "${!IMAGE_URL_MAP[@]}"; do - URL="${IMAGE_URL_MAP[$NAME]}" + for i in "${!IMAGE_NAMES[@]}"; do + NAME="${IMAGE_NAMES[$i]}" + URL="${IMAGE_URLS[$i]}" PLACEHOLDER="{{${NAME}}}" if [[ "$COMMENT_BODY" == *"$PLACEHOLDER"* ]]; then COMMENT_BODY="${COMMENT_BODY//"$PLACEHOLDER"/![$NAME]($URL)}" else - UNREFERENCED+=("$NAME") + UNREFERENCED+=("${NAME}"$'\t'"${URL}") fi done if [[ ${#UNREFERENCED[@]} -gt 0 ]]; then - IFS=$'\n' SORTED=($(printf '%s\n' "${UNREFERENCED[@]}" | sort)); unset IFS - for NAME in "${SORTED[@]}"; do - COMMENT_BODY+=$'\n\n'"![${NAME}](${IMAGE_URL_MAP[$NAME]})" - done + while IFS=$'\t' read -r NAME URL; do + COMMENT_BODY+=$'\n\n'"![${NAME}](${URL})" + done < <(printf '%s\n' "${UNREFERENCED[@]}" | sort) fi else COMMENT_BODY="## Screenshots"$'\n\n'