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 <thomasp@squareup.com>
This commit is contained in:
Thomas Petersen
2026-08-12 00:04:44 -04:00
parent 5c9dded8c8
commit 07bdaf8d3b
4 changed files with 99 additions and 15 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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` });
});

View File

@@ -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'