repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/strata/__e2e__/keyboard.spec.ts
Rawimport { expect, type Page, test } from "@playwright/test";
import { startReviewServer } from "../server.js";
import type {
AskRequest,
Draft,
Feedback,
Review,
ReviewServer,
} from "../types.js";
const longLine = `const horizontal = "${"x".repeat(360)}";`;
function makeReview(id = "keyboard-snapshot"): Review {
return {
snapshot: {
id,
repoRoot: "/safe/keyboard-project",
source: { kind: "working" },
base: "keyboard-base",
head: "keyboard-head",
hunks: [
{
id: "alpha-long",
path: "src/alpha-long.ts",
header: "@@ -10,112 +10,112 @@ alpha",
lines: [
{ kind: "delete", text: "const mode = 'old';", oldLine: 10 },
{ kind: "add", text: "const mode = 'new';", newLine: 10 },
{
kind: "context",
text: "const firstNeedle = 'Needle';",
oldLine: 11,
newLine: 11,
},
{
kind: "context",
text: longLine,
oldLine: 12,
newLine: 12,
},
...Array.from({ length: 108 }, (_, index) => ({
kind: "context" as const,
text: `alpha filler ${String(index).padStart(3, "0")}`,
oldLine: index + 13,
newLine: index + 13,
})),
],
},
{
id: "alpha-literal",
path: "src/alpha-literal.ts",
header: "@@ -200,2 +200,3 @@ literal",
lines: [
{
kind: "context",
text: "export function literal() {",
oldLine: 200,
newLine: 200,
},
{
kind: "add",
text: ' return "[literal.*]";',
newLine: 201,
},
{
kind: "context",
text: "}",
oldLine: 201,
newLine: 202,
},
],
},
{
id: "beta-change",
path: "src/beta-change.ts",
header: "@@ -300,2 +300,2 @@ beta",
lines: [
{ kind: "delete", text: "const beta = false;", oldLine: 300 },
{ kind: "add", text: "const beta = true;", newLine: 300 },
{
kind: "context",
text: 'const secondNeedle = "needle";',
oldLine: 301,
newLine: 301,
},
],
},
{
id: "beta-final",
path: "src/beta-final.ts",
header: "@@ -400 +400,2 @@ final",
lines: [
{
kind: "context",
text: "export const final = true;",
oldLine: 400,
newLine: 400,
},
{
kind: "add",
text: 'export const thirdNeedle = "NEEDLE";',
newLine: 401,
},
],
},
],
skipped: Array.from({ length: 24 }, (_, index) => ({
path: `generated/omitted-${String(index).padStart(2, "0")}.bin`,
reason: "binary",
})),
},
plan: {
summary: "Review every keyboard fixture change.",
cohorts: [
{
title: "Alpha",
layers: [
{
id: "alpha",
title: "Alpha layer",
summary: "A long hunk followed by a literal-search hunk.",
hunks: [
{
id: "alpha-long",
summary: Array.from(
{ length: 35 },
(_, index) => `Long explanation line ${index + 1}.`,
).join("\n"),
},
{
id: "alpha-literal",
summary: "Literal punctuation search.",
},
],
flow: Array.from(
{ length: 18 },
(_, index) => `Alpha step ${index + 1}`,
),
},
{
id: "beta",
title: "Beta layer",
summary: "Two compact hunks with separate search matches.",
hunks: [
{
id: "beta-change",
summary: "Changes the beta switch.",
},
{
id: "beta-final",
summary: "Adds the final mixed-case search match.",
},
],
},
],
},
],
},
draft: {
reviewed: ["alpha-literal", "beta-final"],
findings: [],
notes: "",
},
};
}
function makeEmptyReview(): Review {
const review = makeReview("keyboard-empty");
review.snapshot.hunks = [];
review.snapshot.skipped = [];
review.plan.cohorts = [];
review.draft = { reviewed: [], findings: [], notes: "" };
return review;
}
function makeMetadataReview(): Review {
const review = makeReview("keyboard-metadata");
review.snapshot.hunks = [
{
id: "metadata-first",
path: "assets/first-mode.sh",
header: "old mode 100644\nnew mode 100755",
lines: [{ kind: "meta", text: "old mode 100644" }],
},
{
id: "code-before",
path: "src/before.ts",
header: "@@ -1 +1 @@",
lines: [{ kind: "add", text: "export const before = 1;", newLine: 1 }],
},
{
id: "metadata-middle",
path: "assets/middle-mode.sh",
header: "old mode 100644\nnew mode 100755",
lines: [{ kind: "meta", text: "new mode 100755" }],
},
{
id: "code-after",
path: "src/after.ts",
header: "@@ -1 +1 @@",
lines: [{ kind: "add", text: "export const after = 1;", newLine: 1 }],
},
];
review.plan.cohorts = [
{
title: "Metadata",
layers: [
{
id: "metadata",
title: "Metadata and code",
summary: "Review metadata-only and text hunks together.",
hunks: review.snapshot.hunks.map((hunk) => ({
id: hunk.id,
summary: `Review ${hunk.path}.`,
})),
},
],
},
];
review.draft = { reviewed: ["code-before"], findings: [], notes: "" };
return review;
}
function makeViewportReview(): Review {
const review = makeReview("keyboard-viewport");
const hunk = (id: string, lineCount: number) => ({
id,
path: `src/${id}.ts`,
header: `@@ -1,${lineCount} +1,${lineCount} @@`,
lines: Array.from({ length: lineCount }, (_, index) => ({
kind: "context" as const,
text: `${id} line ${index + 1}`,
oldLine: index + 1,
newLine: index + 1,
})),
});
review.snapshot.hunks = [
hunk("compact-first", 1),
hunk("huge-middle", 140),
hunk("compact-last", 1),
];
review.plan.cohorts = [
{
title: "Viewport",
layers: [
{
id: "viewport",
title: "Viewport geometry",
summary: "Compact hunks surround one huge hunk.",
hunks: review.snapshot.hunks.map((item) => ({
id: item.id,
summary: item.path,
})),
},
],
},
];
review.draft = { reviewed: [], findings: [], notes: "" };
return review;
}
function currentLine(page: Page) {
return page.locator(".diff-line.keyboard-current");
}
async function expectCurrentLine(
page: Page,
hunkId: string,
side: "old" | "new",
line: number,
) {
const current = currentLine(page);
await expect(current).toHaveCount(1);
await expect(current).toHaveAttribute("data-hunk-id", hunkId);
await expect(current).toHaveAttribute("data-side", side);
await expect(current).toHaveAttribute("data-line", String(line));
}
async function pressSequence(page: Page, prefix: string, continuation: string) {
await page.keyboard.press(prefix);
await page.keyboard.press(continuation);
}
async function closeKeyboardHelp(page: Page) {
await page.locator("#keyboard-help").evaluate((dialog) => {
dialog.removeAttribute("data-close-observed");
dialog.addEventListener(
"close",
() => dialog.setAttribute("data-close-observed", "true"),
{ once: true },
);
});
await page.keyboard.press("Escape");
await expect(page.locator("#keyboard-help")).toHaveAttribute(
"data-close-observed",
"true",
);
}
async function expectNoViewportOverflow(page: Page) {
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);
}
let server: ReviewServer | undefined;
test.afterEach(() => {
server?.close();
server = undefined;
});
test("exposes contextual keyboard UI and complete cancellable prefix help", async ({
page,
}) => {
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async () => "answer",
save: () => {},
submit: async () => {},
});
await page.goto(server.url);
await expect(page.locator("#status")).toHaveText("Review ready");
await expect(page.locator("#keyboard-status")).toContainText("NORMAL");
await expect(page.locator("#keyboard-status")).toContainText("line 10");
await expect(page.locator("#keyboard-help-button")).toBeVisible();
await expect(page.locator("#keyboard-help")).toHaveJSProperty("open", false);
await expect(page.locator("#keyboard-guide")).toBeHidden();
await expect(page.locator('[data-keyboard-pane="layers"]')).toHaveCount(1);
await expect(page.locator('[data-keyboard-pane="diff"]')).toHaveCount(1);
await expect(page.locator('[data-keyboard-pane="drawer"]')).toHaveCount(1);
await expect(
page.locator('[data-keyboard-pane="diff"].keyboard-focused'),
).toBeVisible();
await expectCurrentLine(page, "alpha-long", "old", 10);
await expect(
page.getByRole("button", { name: "Mark reviewed", exact: true }).first(),
).toBeVisible();
await expect(
page
.getByRole("button", { name: "Mark reviewed", exact: true })
.first()
.locator("kbd"),
).toHaveText(["Space", "r"]);
await expect(page.locator("#mark-layer-reviewed")).toBeDisabled();
await expect(page.locator(".layer-nav kbd")).toHaveCount(0);
for (const [prefix, continuations] of [
[
"Space",
[
"Review",
"Writ",
"Workspace",
"r",
"R",
"c",
"a",
"n",
"b",
"d",
"s",
"f",
"q",
],
],
["g", ["d", "a", "f"]],
["[", ["c", "l", "u"]],
["]", ["c", "l", "u"]],
] as const) {
await page.keyboard.press(prefix);
await expect(page.locator("#keyboard-guide")).toBeVisible();
for (const continuation of continuations) {
await expect(page.locator("#keyboard-guide")).toContainText(continuation);
}
await page.keyboard.press("Escape");
await expect(page.locator("#keyboard-guide")).toBeHidden();
}
await page.keyboard.press("g");
await page.keyboard.press("x");
await expect(page.locator("#keyboard-guide")).toBeHidden();
await expect(page.locator("#keyboard-status")).toContainText("NORMAL");
await page.keyboard.press("?");
const help = page.locator("#keyboard-help");
await expect(help).toHaveAttribute("open", "");
const helpKeys = await help.locator("kbd").allTextContents();
for (const key of [
"j",
"k",
"h",
"l",
"Ctrl+d",
"Ctrl+u",
"gg",
"G",
"]c",
"[c",
"]l",
"[l",
"]u",
"[u",
"/",
"n",
"N",
"Space r",
"Space R",
"Space c",
"Space a",
"Space b",
"gd",
"ga",
"gf",
"Space d",
"Space s",
"Space f",
"Space q",
]) {
expect(helpKeys).toContain(key);
}
await expect(help).toContainText(/toggle.*hunk/i);
await expect(help).toContainText(/toggle.*layer/i);
await expect(help).toContainText(/comment/i);
await expect(help).toContainText(/notes/i);
await expect(help).toContainText(/refresh/i);
await expect(help).toContainText(/close/i);
for (let index = 0; index < 20; index++) await page.keyboard.press("Tab");
expect(
await page.evaluate(
() => document.activeElement?.closest("#keyboard-help")?.id,
),
).toBe("keyboard-help");
await page.keyboard.press("Escape");
await expect(help).not.toHaveAttribute("open", "");
await page.locator("#keyboard-help-button").press("Enter");
await expect(help).toHaveAttribute("open", "");
await page.keyboard.press("Escape");
await page.keyboard.press("[");
await expect(page.locator("#keyboard-guide")).toContainText("previous hunk");
await expect(page.locator("#keyboard-guide")).toContainText("previous layer");
await expect(page.locator("#keyboard-guide")).not.toContainText("next hunk");
await page.keyboard.press("Escape");
});
test("navigates lines, hunks, layers, unreviewed work, and large hunks within bounds", async ({
page,
}) => {
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async () => "answer",
save: () => {},
submit: async () => {},
});
await page.goto(server.url);
await expectCurrentLine(page, "alpha-long", "old", 10);
await page.keyboard.press("k");
await expectCurrentLine(page, "alpha-long", "old", 10);
await page.keyboard.press("j");
await expectCurrentLine(page, "alpha-long", "new", 10);
await page.keyboard.press("k");
await expectCurrentLine(page, "alpha-long", "old", 10);
await pressSequence(page, "g", "g");
await expectCurrentLine(page, "alpha-long", "old", 10);
await page.keyboard.press("G");
await expectCurrentLine(page, "beta-final", "new", 401);
await page.keyboard.press("j");
await expectCurrentLine(page, "beta-final", "new", 401);
await pressSequence(page, "]", "l");
await expect(page.locator("#diff-heading")).toHaveText("Alpha layer");
await expectCurrentLine(page, "alpha-long", "old", 10);
await pressSequence(page, "[", "l");
await expect(page.locator("#diff-heading")).toHaveText("Alpha layer");
await pressSequence(page, "]", "c");
await expectCurrentLine(page, "alpha-literal", "new", 200);
await expect(page.locator("#current-path")).toHaveText(
"src/alpha-literal.ts",
);
await expect(page.locator("#ask-focus")).toContainText(
"src/alpha-literal.ts",
);
await page.waitForTimeout(650);
await expectCurrentLine(page, "alpha-literal", "new", 200);
await pressSequence(page, "]", "c");
await expectCurrentLine(page, "alpha-literal", "new", 200);
await pressSequence(page, "[", "c");
await expectCurrentLine(page, "alpha-long", "old", 10);
await page.waitForTimeout(650);
await expectCurrentLine(page, "alpha-long", "old", 10);
await pressSequence(page, "[", "c");
await expectCurrentLine(page, "alpha-long", "old", 10);
await pressSequence(page, "]", "l");
await expect(page.locator("#diff-heading")).toHaveText("Beta layer");
await pressSequence(page, "]", "l");
await expect(page.locator("#diff-heading")).toHaveText("Beta layer");
await pressSequence(page, "[", "l");
await expect(page.locator("#diff-heading")).toHaveText("Alpha layer");
await page.locator("#all-changes").click();
await expectCurrentLine(page, "alpha-long", "old", 10);
await pressSequence(page, "]", "u");
await expectCurrentLine(page, "beta-change", "old", 300);
await pressSequence(page, "[", "u");
await expectCurrentLine(page, "alpha-long", "old", 10);
await pressSequence(page, "[", "u");
await expectCurrentLine(page, "alpha-long", "old", 10);
});
test("navigates and reviews first and middle metadata-only hunks", async ({
page,
}) => {
const saved: Draft[] = [];
server = await startReviewServer({
review: makeMetadataReview(),
isCurrent: async () => true,
refresh: async () => makeMetadataReview(),
ask: async () => "answer",
save: (draft) => saved.push(structuredClone(draft)),
submit: async () => {},
});
await page.goto(server.url);
await expect(page.locator("#current-path")).toHaveText(
"assets/first-mode.sh",
);
await expect(currentLine(page)).toHaveCount(0);
await pressSequence(page, "Space", "c");
await expect(page.locator("#comment-dialog")).not.toHaveAttribute("open", "");
await pressSequence(page, "Space", "r");
await expect.poll(() => saved.length).toBe(1);
expect(saved[0].reviewed).toEqual(["code-before", "metadata-first"]);
await pressSequence(page, "]", "c");
await expectCurrentLine(page, "code-before", "new", 1);
await pressSequence(page, "]", "c");
await expect(page.locator("#current-path")).toHaveText(
"assets/middle-mode.sh",
);
await expect(currentLine(page)).toHaveCount(0);
await pressSequence(page, "[", "c");
await expectCurrentLine(page, "code-before", "new", 1);
await pressSequence(page, "[", "c");
await expect(page.locator("#current-path")).toHaveText(
"assets/first-mode.sh",
);
await expect(currentLine(page)).toHaveCount(0);
await pressSequence(page, "]", "u");
await expect(page.locator("#current-path")).toHaveText(
"assets/middle-mode.sh",
);
await expect(currentLine(page)).toHaveCount(0);
await pressSequence(page, "Space", "r");
await expect.poll(() => saved.length).toBe(2);
expect(saved[1].reviewed).toEqual([
"code-before",
"metadata-first",
"metadata-middle",
]);
});
test("updates scroll observations after explicit-jump suppression by visible area", async ({
page,
}) => {
server = await startReviewServer({
review: makeViewportReview(),
isCurrent: async () => true,
refresh: async () => makeViewportReview(),
ask: async () => "answer",
save: () => {},
submit: async () => {},
});
await page.setViewportSize({ width: 1100, height: 520 });
await page.goto(server.url);
await page.locator("#hunk-huge-middle").click();
await pressSequence(page, "]", "c");
await expectCurrentLine(page, "compact-last", "new", 1);
await page.waitForTimeout(600);
await page.locator(".diff-region").hover();
await page.mouse.wheel(0, -180);
await expect(page.locator("#current-path")).toHaveText("src/huge-middle.ts");
await expect(currentLine(page)).toHaveAttribute(
"data-hunk-id",
"huge-middle",
);
await page.locator(".diff-region").evaluate((element) => {
element.scrollTop = 0;
});
await expect(page.locator("#current-path")).toHaveText("src/huge-middle.ts");
});
test("scrolls and focuses only visible panes", async ({ page }) => {
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async () => "answer",
save: () => {},
submit: async () => {},
});
await page.setViewportSize({ width: 1100, height: 520 });
await page.goto(server.url);
const horizontalBefore = await page
.locator('.hunk[data-hunk-id="alpha-long"] .diff-scroll')
.evaluate((element) => element.scrollLeft);
await page.keyboard.press("l");
await expect
.poll(() =>
page
.locator('.hunk[data-hunk-id="alpha-long"] .diff-scroll')
.evaluate((element) => element.scrollLeft),
)
.toBeGreaterThan(horizontalBefore);
await page.keyboard.press("h");
await expect
.poll(() =>
page
.locator('.hunk[data-hunk-id="alpha-long"] .diff-scroll')
.evaluate((element) => element.scrollLeft),
)
.toBeLessThan(horizontalBefore + 1);
await pressSequence(page, "g", "g");
const diffPane = page.locator('[data-keyboard-pane="diff"]');
await page.keyboard.press("Control+d");
await expect
.poll(() => diffPane.evaluate((element) => element.scrollTop))
.toBeGreaterThan(0);
const halfPage = await diffPane.evaluate((element) => element.scrollTop);
await page.keyboard.press("Control+u");
await expect
.poll(() => diffPane.evaluate((element) => element.scrollTop))
.toBeLessThan(halfPage);
await pressSequence(page, "Space", "h");
const layersPane = page.locator(
'[data-keyboard-pane="layers"].keyboard-focused',
);
await expect(layersPane).toBeVisible();
await page.keyboard.press("G");
await expect
.poll(() => layersPane.evaluate((element) => element.scrollTop))
.toBeGreaterThan(0);
await pressSequence(page, "g", "g");
await expect
.poll(() => layersPane.evaluate((element) => element.scrollTop))
.toBe(0);
await page.keyboard.press("j");
await expect
.poll(() => layersPane.evaluate((element) => element.scrollTop))
.toBeGreaterThan(0);
const layerScroll = await layersPane.evaluate((element) => element.scrollTop);
await page.keyboard.press("k");
await expect
.poll(() => layersPane.evaluate((element) => element.scrollTop))
.toBeLessThan(layerScroll);
await pressSequence(page, "Space", "l");
await expect(
page.locator('[data-keyboard-pane="diff"].keyboard-focused'),
).toBeVisible();
await pressSequence(page, "Space", "l");
const drawerPane = page.locator(
'[data-keyboard-pane="drawer"].keyboard-focused',
);
const drawerScroll = page.locator("#details-panel");
await expect(drawerPane).toBeVisible();
await page.keyboard.press("G");
await expect
.poll(() => drawerScroll.evaluate((element) => element.scrollTop))
.toBeGreaterThan(0);
await pressSequence(page, "g", "g");
await expect
.poll(() => drawerScroll.evaluate((element) => element.scrollTop))
.toBe(0);
await pressSequence(page, "g", "a");
await expect(page.getByRole("tab", { name: "Ask" })).toHaveAttribute(
"aria-selected",
"true",
);
await pressSequence(page, "g", "f");
await expect(page.getByRole("tab", { name: "Feedback" })).toHaveAttribute(
"aria-selected",
"true",
);
await pressSequence(page, "g", "d");
await expect(page.getByRole("tab", { name: "Details" })).toHaveAttribute(
"aria-selected",
"true",
);
await drawerPane.focus();
await pressSequence(page, "Space", "d");
await expect(page.locator('[data-keyboard-pane="drawer"]')).toBeHidden();
await expect(
page.locator('[data-keyboard-pane="drawer"].keyboard-focused'),
).toHaveCount(0);
await expect(
page.locator("[data-keyboard-pane].keyboard-focused:visible"),
).toHaveCount(1);
await pressSequence(page, "Space", "l");
await expect(
page.locator('[data-keyboard-pane="diff"].keyboard-focused'),
).toBeVisible();
await pressSequence(page, "Space", "h");
await expect(
page.locator('[data-keyboard-pane="layers"].keyboard-focused'),
).toBeVisible();
await pressSequence(page, "Space", "b");
await expect(page.locator('[data-keyboard-pane="layers"]')).toBeHidden();
await expect(
page.locator('[data-keyboard-pane="diff"].keyboard-focused'),
).toBeVisible();
});
test("searches visible diff text literally and repeats with wrapping", async ({
page,
}) => {
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async () => "answer",
save: () => {},
submit: async () => {},
});
await page.goto(server.url);
await page.keyboard.press("/");
await expect(page.locator("#keyboard-search")).toBeVisible();
await expect(page.locator("#keyboard-search")).toBeFocused();
await expect(page.locator("#keyboard-status")).toContainText("TEXT");
await page.locator("#keyboard-search").fill("[literal.*]");
await page.locator("#keyboard-search").press("Enter");
await expectCurrentLine(page, "alpha-literal", "new", 201);
await page.keyboard.press("n");
await expectCurrentLine(page, "alpha-literal", "new", 201);
await page.keyboard.press("/");
await page.locator("#keyboard-search").fill("needle");
await page.locator("#keyboard-search").press("Enter");
const first = await currentLine(page).getAttribute("data-hunk-id");
await page.keyboard.press("n");
const second = await currentLine(page).getAttribute("data-hunk-id");
await page.keyboard.press("n");
const third = await currentLine(page).getAttribute("data-hunk-id");
expect(new Set([first, second, third])).toEqual(
new Set(["alpha-long", "beta-change", "beta-final"]),
);
await page.keyboard.press("n");
await expect(currentLine(page)).toHaveAttribute("data-hunk-id", first ?? "");
await page.keyboard.press("N");
await expect(currentLine(page)).toHaveAttribute("data-hunk-id", third ?? "");
await page.locator('[data-layer-id="alpha"]').click();
const beforeMissingSearch = await currentLine(page).getAttribute("data-line");
await page.keyboard.press("/");
await page.locator("#keyboard-search").fill("secondNeedle");
await page.locator("#keyboard-search").press("Enter");
await expect(page.locator("#status")).toContainText("No matches");
await expect(currentLine(page)).toHaveAttribute(
"data-line",
beforeMissingSearch ?? "",
);
await page.keyboard.press("/");
await page.locator("#keyboard-search").fill("unfinished");
await page.locator("#keyboard-search").press("Escape");
await expect(page.locator("#keyboard-search")).toBeHidden();
await expect(page.locator("#keyboard-status")).toContainText("NORMAL");
});
test("routes line, hunk, and whole-layer actions through persisted scope-safe drafts", async ({
page,
}) => {
const saved: Draft[] = [];
let rejectSave = false;
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async () => "answer",
save: (draft) => {
if (rejectSave) throw new Error("persist blocked");
saved.push(structuredClone(draft));
},
submit: async () => {},
});
await page.goto(server.url);
await expect(page.locator("#status")).toHaveText("Review ready");
await pressSequence(page, "]", "l");
await expect(page.locator("#mark-layer-reviewed")).toBeEnabled();
await expect(
page.getByRole("button", { name: "Mark layer reviewed", exact: true }),
).toHaveCount(1);
await expect(page.locator("#mark-layer-reviewed kbd")).toHaveText([
"Space",
"R",
]);
await pressSequence(page, "Space", "n");
await expect(page.locator("#notes")).toBeFocused();
await page
.locator("#notes")
.fill("Keep this note across every review action.");
await expect(page.locator("#status")).toHaveText("Draft saved");
await page.keyboard.press("Escape");
await pressSequence(page, "Space", "a");
await expect(page.locator("#question")).toBeFocused();
await page.locator("#question").fill("Alpha unsent question");
await page.keyboard.press("Escape");
await pressSequence(page, "]", "l");
await pressSequence(page, "Space", "a");
await page.locator("#question").fill("Beta unsent question");
await page.keyboard.press("Escape");
await pressSequence(page, "[", "l");
await pressSequence(page, "Space", "a");
await expect(page.locator("#question")).toHaveValue("Alpha unsent question");
await page.keyboard.press("Escape");
await pressSequence(page, "g", "g");
await expectCurrentLine(page, "alpha-long", "old", 10);
await pressSequence(page, "Space", "c");
await expect(page.locator("#comment-dialog")).toHaveAttribute("open", "");
await expect(page.locator("#comment-anchor")).toContainText("old:10");
await page.locator("#comment-text").fill("Old-side keyboard finding.");
await page.getByRole("button", { name: "Add comment", exact: true }).click();
await expect(page.locator(".finding")).toContainText(
"Old-side keyboard finding.",
);
await currentLine(page).focus();
await page.keyboard.press("j");
await expectCurrentLine(page, "alpha-long", "new", 10);
await pressSequence(page, "Space", "c");
await expect(page.locator("#comment-anchor")).toContainText("new:10");
await page.getByRole("button", { name: "Cancel", exact: true }).click();
const beforeHunkToggle = saved.length;
await pressSequence(page, "Space", "r");
await expect.poll(() => saved.length).toBe(beforeHunkToggle + 1);
expect(saved.at(-1)?.reviewed).toEqual([
"alpha-literal",
"beta-final",
"alpha-long",
]);
await pressSequence(page, "Space", "r");
await expect.poll(() => saved.length).toBe(beforeHunkToggle + 2);
expect(saved.at(-1)?.reviewed).toEqual(["alpha-literal", "beta-final"]);
const beforeWholeLayer = saved.length;
await pressSequence(page, "Space", "R");
await expect.poll(() => saved.length).toBe(beforeWholeLayer + 1);
expect(new Set(saved.at(-1)?.reviewed)).toEqual(
new Set(["alpha-long", "alpha-literal", "beta-final"]),
);
await expect(
page.getByRole("button", { name: "Unmark layer reviewed", exact: true }),
).toHaveCount(1);
await pressSequence(page, "Space", "R");
await expect.poll(() => saved.length).toBe(beforeWholeLayer + 2);
expect(saved.at(-1)?.reviewed).toEqual(["beta-final"]);
rejectSave = true;
await pressSequence(page, "Space", "r");
await expect(page.locator("#status")).toHaveText("persist blocked");
await expect(
page.getByRole("button", { name: "Mark reviewed", exact: true }).first(),
).toBeVisible();
await pressSequence(page, "g", "f");
await expect(page.locator("#notes")).toHaveValue(
"Keep this note across every review action.",
);
await expect(page.locator(".finding")).toContainText(
"Old-side keyboard finding.",
);
await pressSequence(page, "Space", "a");
await expect(page.locator("#question")).toHaveValue("Alpha unsent question");
await page.keyboard.press("Escape");
await page.locator("#all-changes").click();
await expect(page.locator("#mark-layer-reviewed")).toBeDisabled();
const savesAtAllChanges = saved.length;
await pressSequence(page, "Space", "R");
await page.waitForTimeout(100);
expect(saved).toHaveLength(savesAtAllChanges);
});
test("preserves the latest optimistic draft across delayed saves and flushes", async ({
page,
}) => {
const saved: Draft[] = [];
const submitted: Feedback[] = [];
let refreshes = 0;
let requestCount = 0;
const releases = new Map<number, () => void>();
const gates = new Map<number, Promise<void>>();
for (const index of [1, 2, 4, 5]) {
gates.set(
index,
new Promise<void>((resolve) => releases.set(index, resolve)),
);
}
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => {
refreshes++;
return makeReview("keyboard-refreshed");
},
ask: async () => "answer",
save: (draft) => saved.push(structuredClone(draft)),
submit: async (feedback) => {
submitted.push(structuredClone(feedback));
},
});
await page.route("**/api/draft", async (route) => {
const index = ++requestCount;
const response = await route.fetch();
await gates.get(index);
await route.fulfill({ response });
});
await page.goto(server.url);
await page.locator('.mark-reviewed[data-hunk-id="alpha-long"]').click();
await expect.poll(() => requestCount).toBe(1);
await page.locator('.mark-reviewed[data-hunk-id="alpha-literal"]').click();
releases.get(1)?.();
await expect.poll(() => requestCount).toBe(2);
await page.locator('.mark-reviewed[data-hunk-id="beta-change"]').click();
releases.get(2)?.();
await page.getByRole("tab", { name: "Feedback" }).click();
page.once("dialog", (dialog) => dialog.accept());
await page.getByRole("button", { name: "Send feedback to Pi" }).click();
await expect(page.locator("#status")).toHaveText("Feedback sent to Pi");
expect(new Set(submitted[0].reviewed)).toEqual(
new Set(["beta-final", "alpha-long", "beta-change"]),
);
await page.locator('.mark-reviewed[data-hunk-id="alpha-long"]').click();
await expect.poll(() => requestCount).toBe(4);
await page.locator('.mark-reviewed[data-hunk-id="alpha-literal"]').click();
releases.get(4)?.();
await expect.poll(() => requestCount).toBe(5);
await page.locator('.mark-reviewed[data-hunk-id="beta-change"]').click();
releases.get(5)?.();
page.once("dialog", (dialog) => dialog.accept());
await page.getByRole("button", { name: "Refresh", exact: true }).click();
await expect(page.locator("#source-label")).toContainText("keyboard-ref");
expect(refreshes).toBe(1);
expect(new Set(saved.at(-1)?.reviewed)).toEqual(
new Set(["alpha-long", "alpha-literal", "beta-change"]),
);
});
test("guards native controls, text entry, composition, and unrelated modifiers", async ({
page,
}) => {
const requests: AskRequest[] = [];
const saved: Draft[] = [];
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async (request) => {
requests.push(structuredClone(request));
return "answer";
},
save: (draft) => saved.push(structuredClone(draft)),
submit: async () => {},
});
await page.goto(server.url);
await expectCurrentLine(page, "alpha-long", "old", 10);
for (const event of [
{ key: "j", altKey: true },
{ key: "j", ctrlKey: true },
{ key: "J", shiftKey: true },
{ key: "j", metaKey: true },
]) {
await currentLine(page).evaluate((element, init) => {
element.dispatchEvent(
new KeyboardEvent("keydown", { ...init, bubbles: true }),
);
}, event);
await expectCurrentLine(page, "alpha-long", "old", 10);
}
await currentLine(page).evaluate((element) => {
element.dispatchEvent(
new KeyboardEvent("keydown", {
key: "?",
code: "Slash",
shiftKey: true,
bubbles: true,
}),
);
});
await expect(page.locator("#keyboard-help")).toHaveAttribute("open", "");
await page.keyboard.press("Escape");
for (const event of [
{ key: "/", shiftKey: true },
{
key: "/",
altKey: true,
ctrlKey: true,
modifierAltGraph: true,
},
]) {
await currentLine(page).evaluate((element, init) => {
element.dispatchEvent(
new KeyboardEvent("keydown", { ...init, bubbles: true }),
);
}, event);
await expect(page.locator("#keyboard-search")).toBeVisible();
await page.keyboard.press("Escape");
}
await currentLine(page).evaluate((element) => {
element.dispatchEvent(
new KeyboardEvent("keydown", {
key: "[",
altKey: true,
ctrlKey: true,
modifierAltGraph: true,
bubbles: true,
}),
);
});
await expect(page.locator("#keyboard-guide")).toContainText("previous hunk");
await page.keyboard.press("Escape");
await page.locator(".diff-region").focus();
await page.keyboard.press("Space");
await expect(page.locator("#keyboard-guide")).toBeVisible();
await page.keyboard.press("Tab");
await expect(page.locator("#keyboard-guide")).toBeHidden();
await expect(page.locator(".diff-region")).not.toBeFocused();
await page.locator(".diff-region").focus();
await page.locator(".diff-region").evaluate((element) => {
element.scrollTop = 0;
});
await page.keyboard.press("g");
await page.keyboard.press("Control+d");
await expect(page.locator("#keyboard-guide")).toBeHidden();
await expect
.poll(() =>
page.locator(".diff-region").evaluate((element) => element.scrollTop),
)
.toBeGreaterThan(0);
await page.keyboard.press("g");
await page.keyboard.press("Alt+j");
await expect(page.locator("#keyboard-guide")).toBeHidden();
await pressSequence(page, "Space", "a");
const question = page.locator("#question");
await expect(question).toBeFocused();
await question.pressSequentially("jkG? / n Space r");
await question.press("Enter");
await question.press("Space");
await expect(question).toHaveValue("jkG? / n Space r\n ");
await expect(page.locator("#keyboard-guide")).toBeHidden();
await expect(page.locator("#keyboard-help")).not.toHaveAttribute("open", "");
await expectCurrentLine(page, "alpha-long", "old", 10);
await question.evaluate((element) => {
element.dispatchEvent(
new KeyboardEvent("keydown", {
key: "j",
bubbles: true,
isComposing: true,
}),
);
element.dispatchEvent(
new KeyboardEvent("keydown", {
key: "Escape",
bubbles: true,
isComposing: true,
}),
);
element.dispatchEvent(
new KeyboardEvent("keydown", {
key: "Enter",
shiftKey: true,
bubbles: true,
isComposing: true,
}),
);
});
await expect(question).toBeFocused();
expect(requests).toHaveLength(0);
await expectCurrentLine(page, "alpha-long", "old", 10);
await question.press("Escape");
await expect(page.locator("#keyboard-status")).toContainText("NORMAL");
await expect(currentLine(page)).toBeFocused();
await page.evaluate(() => {
const editable = document.createElement("div");
editable.id = "keyboard-editable-fixture";
editable.contentEditable = "true";
editable.tabIndex = 0;
document.body.append(editable);
editable.focus();
});
await page.keyboard.type("jG?");
await expect(page.locator("#keyboard-editable-fixture")).toHaveText("jG?");
await expectCurrentLine(page, "alpha-long", "old", 10);
await page
.locator("#flow .flow-step")
.first()
.evaluate((element) => {
element.dataset.retainedWithinHunk = "true";
});
await currentLine(page).focus();
await page.keyboard.press("j");
await expectCurrentLine(page, "alpha-long", "new", 10);
await expect(
page.locator('#flow .flow-step[data-retained-within-hunk="true"]'),
).toHaveCount(1);
const mark = page.locator('.mark-reviewed[data-hunk-id="alpha-long"]');
await mark.focus();
const beforeNativeSpace = saved.length;
await mark.press("Space");
await expect.poll(() => saved.length).toBe(beforeNativeSpace + 1);
await expect(page.locator("#keyboard-guide")).toBeHidden();
await expect(mark).toHaveAttribute("aria-pressed", "true");
await expect(mark).toBeFocused();
await page.keyboard.press("Tab");
await expect(mark).not.toBeFocused();
await expect(mark).toHaveAttribute("aria-pressed", "true");
await expect(page.locator("#keyboard-guide")).toBeHidden();
await page.locator("#keyboard-help-button").focus();
await page.keyboard.press("Tab");
await expect(page.locator("#keyboard-guide")).toBeHidden();
await page.locator("#keyboard-help-button").press("Enter");
await expect(page.locator("#keyboard-help")).toHaveAttribute("open", "");
await page.keyboard.press("Escape");
await currentLine(page).focus();
await pressSequence(page, "Space", "a");
await question.fill("Shift Enter still submits");
await question.press("Shift+Enter");
await expect.poll(() => requests.length).toBe(1);
expect(requests[0].question).toBe("Shift Enter still submits");
});
test("uses shared confirmations for mouse and keyboard submit, refresh, and close", async ({
page,
}) => {
const submitted: Feedback[] = [];
let refreshes = 0;
let closes = 0;
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => {
refreshes++;
return makeReview("keyboard-refreshed");
},
ask: async () => "answer",
save: () => {},
submit: async (feedback) => {
submitted.push(structuredClone(feedback));
},
onClose: () => closes++,
});
await page.goto(server.url);
await pressSequence(page, "Space", "n");
await page.locator("#notes").fill("Confirm before sending this note.");
await expect(page.locator("#status")).toHaveText("Draft saved");
await page.keyboard.press("Escape");
let submitPrompt = "";
page.once("dialog", async (dialog) => {
submitPrompt = dialog.message();
await dialog.dismiss();
});
await page.getByRole("button", { name: "Send feedback to Pi" }).click();
expect(submitPrompt).toMatch(/send feedback/i);
expect(submitted).toHaveLength(0);
await expect(page.locator("#notes")).toHaveValue(
"Confirm before sending this note.",
);
page.once("dialog", (dialog) => dialog.accept());
await pressSequence(page, "Space", "s");
await expect(page.locator("#status")).toHaveText("Feedback sent to Pi");
expect(submitted).toHaveLength(1);
await expect(page.locator("#notes")).toHaveValue("");
let refreshPrompt = "";
page.once("dialog", async (dialog) => {
refreshPrompt = dialog.message();
await dialog.dismiss();
});
await pressSequence(page, "Space", "f");
expect(refreshPrompt).toMatch(/refresh/i);
expect(refreshes).toBe(0);
await expectCurrentLine(page, "alpha-long", "old", 10);
page.once("dialog", (dialog) => dialog.accept());
await page.getByRole("button", { name: "Refresh", exact: true }).click();
await expect(page.locator("#source-label")).toContainText("keyboard-ref");
expect(refreshes).toBe(1);
await expectCurrentLine(page, "alpha-long", "old", 10);
await expect(page.locator("#keyboard-status")).toContainText("line 10");
let closePrompt = "";
page.once("dialog", async (dialog) => {
closePrompt = dialog.message();
await dialog.dismiss();
});
await page.getByRole("button", { name: "Close", exact: true }).click();
expect(closePrompt).toMatch(/close/i);
expect(closes).toBe(0);
await page.keyboard.press("j");
await expectCurrentLine(page, "alpha-long", "new", 10);
page.once("dialog", (dialog) => dialog.accept());
await pressSequence(page, "Space", "q");
await expect(page.locator("#status")).toContainText("Review closed");
await expect.poll(() => closes).toBe(1);
});
test("blocks stale keyboard actions while busy and handles an empty review", async ({
page,
}) => {
let answer!: (value: string) => void;
let askStarted!: () => void;
const didStart = new Promise<void>((resolve) => {
askStarted = resolve;
});
const saved: Draft[] = [];
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async () =>
new Promise<string>((resolve) => {
answer = resolve;
askStarted();
}),
save: (draft) => saved.push(structuredClone(draft)),
submit: async () => {},
});
await page.goto(server.url);
await pressSequence(page, "Space", "a");
await page.locator("#question").fill("Hold the server busy");
await page.locator("#question").press("Shift+Enter");
await didStart;
const waitingPage = await page.context().newPage();
await waitingPage.goto(server.url);
await expect(waitingPage.locator("#workspace")).toHaveAttribute(
"aria-busy",
"true",
);
await expectCurrentLine(waitingPage, "alpha-long", "old", 10);
const savesWhileBusy = saved.length;
await waitingPage.keyboard.press("j");
await pressSequence(waitingPage, "]", "c");
await pressSequence(waitingPage, "Space", "r");
await expectCurrentLine(waitingPage, "alpha-long", "old", 10);
expect(saved).toHaveLength(savesWhileBusy);
await expect(waitingPage.locator("#keyboard-guide")).toBeHidden();
answer("finished");
await expect(page.locator("#status")).toHaveText("Answer received");
await expect(waitingPage.locator("#workspace")).toHaveAttribute(
"aria-busy",
"false",
);
await waitingPage.close();
server.close();
server = await startReviewServer({
review: makeEmptyReview(),
isCurrent: async () => true,
refresh: async () => makeEmptyReview(),
ask: async () => "answer",
save: (draft) => saved.push(structuredClone(draft)),
submit: async () => {},
});
await page.goto(server.url);
await expect(page.locator("#status")).toHaveText("Review ready");
await expect(page.locator(".diff-line.keyboard-current")).toHaveCount(0);
await expect(page.locator("#keyboard-status")).toContainText("NORMAL");
await expect(page.locator("#keyboard-status")).toContainText("no lines");
await expect(page.locator("#mark-layer-reviewed")).toBeDisabled();
const savesBeforeEmptyActions = saved.length;
await page.keyboard.press("j");
await page.keyboard.press("G");
await pressSequence(page, "]", "c");
await pressSequence(page, "]", "u");
await pressSequence(page, "Space", "r");
await pressSequence(page, "Space", "c");
expect(saved).toHaveLength(savesBeforeEmptyActions);
await expect(page.locator("#comment-dialog")).not.toHaveAttribute("open", "");
});
test("keeps writes disabled when readiness polling exhausts", async ({
page,
}) => {
let answer!: (value: string) => void;
let askStarted!: () => void;
const didStart = new Promise<void>((resolve) => {
askStarted = resolve;
});
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async () =>
new Promise<string>((resolve) => {
answer = resolve;
askStarted();
}),
save: () => {},
submit: async () => {},
});
await page.goto(server.url);
await pressSequence(page, "Space", "a");
await page.locator("#question").fill("Keep the server busy past retries");
await page.locator("#question").press("Shift+Enter");
await didStart;
const waitingPage = await page.context().newPage();
await waitingPage.goto(server.url);
await waitingPage.waitForTimeout(4_300);
await expect(waitingPage.locator("#workspace")).toHaveAttribute(
"aria-busy",
"true",
);
await expect(waitingPage.locator("#status")).toContainText(
"Reload this page to retry",
);
await expect(
waitingPage.getByRole("button", { name: "Refresh" }),
).toBeDisabled();
await expect(
waitingPage.getByRole("button", { name: "Mark reviewed" }).first(),
).toBeDisabled();
await expect(
waitingPage.getByRole("button", { name: "Close" }),
).toBeEnabled();
answer("finished");
await expect(page.locator("#status")).toHaveText("Answer received");
await waitingPage.reload();
await expect(waitingPage.locator("#status")).toHaveText("Review ready");
await expect(
waitingPage.getByRole("button", { name: "Refresh" }),
).toBeEnabled();
await waitingPage.close();
});
test("help close cannot steal focus from a newer question", async ({
page,
}) => {
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview(),
ask: async () => "answer",
save: () => {},
submit: async () => {},
});
await page.goto(server.url);
await page.keyboard.press("?");
await expect(page.locator("#keyboard-help")).toHaveAttribute("open", "");
await closeKeyboardHelp(page);
await pressSequence(page, "Space", "a");
await expect(page.locator("#question")).toBeFocused();
await page
.locator("#keyboard-help")
.evaluate((dialog) => dialog.dispatchEvent(new Event("close")));
await expect(page.locator("#question")).toBeFocused();
await expect(page.locator("#keyboard-status")).toContainText("TEXT");
await expect(page.locator("#keyboard-return-hint")).toBeVisible();
});
test("renders focused normal, leader, help, and typing states responsively", async ({
page,
}, testInfo) => {
server = await startReviewServer({
review: makeReview(),
isCurrent: async () => true,
refresh: async () => makeReview("keyboard-refreshed"),
ask: async () => "answer",
save: () => {},
submit: async () => {},
});
await page.setViewportSize({ width: 1280, height: 800 });
await page.emulateMedia({ colorScheme: "light" });
await page.goto(server.url);
const lightBackground = await page
.locator("body")
.evaluate((element) => getComputedStyle(element).backgroundColor);
await expect(
page.locator('[data-keyboard-pane="diff"].keyboard-focused'),
).toBeVisible();
await expect(currentLine(page)).toBeVisible();
await page.screenshot({
path: testInfo.outputPath("keyboard-desktop-normal-light.png"),
});
await page.keyboard.press("Space");
await expect(page.locator("#keyboard-guide")).toBeVisible();
await page.screenshot({
path: testInfo.outputPath("keyboard-desktop-leader-light.png"),
});
await page.keyboard.press("Escape");
await page.keyboard.press("?");
await expect(page.locator("#keyboard-help")).toHaveAttribute("open", "");
await page.screenshot({
path: testInfo.outputPath("keyboard-desktop-help-light.png"),
});
await closeKeyboardHelp(page);
await currentLine(page).focus();
await pressSequence(page, "Space", "a");
await expect(page.locator("#question")).toBeFocused();
await expect(page.locator("#keyboard-status")).toContainText("TEXT");
await expect(page.locator("#keyboard-context")).toBeHidden();
await expect(page.locator("#keyboard-return-hint")).toBeVisible();
await expect(page.locator("#keyboard-return-hint")).toContainText(
"return to diff",
);
await page.screenshot({
path: testInfo.outputPath("keyboard-desktop-typing-light.png"),
});
await page.keyboard.press("Escape");
await expect(page.locator("#keyboard-return-hint")).toBeHidden();
await page.emulateMedia({ colorScheme: "dark" });
const darkBackground = await page
.locator("body")
.evaluate((element) => getComputedStyle(element).backgroundColor);
expect(darkBackground).not.toBe(lightBackground);
await page.screenshot({
path: testInfo.outputPath("keyboard-desktop-normal-dark.png"),
});
await page.setViewportSize({ width: 390, height: 760 });
await expect(page.locator('[data-keyboard-pane="layers"]')).toBeHidden();
await expect(page.locator('[data-keyboard-pane="drawer"]')).toBeHidden();
await expect(
page.locator('[data-keyboard-pane="diff"].keyboard-focused'),
).toBeVisible();
await expectNoViewportOverflow(page);
await page.screenshot({
path: testInfo.outputPath("keyboard-mobile-normal-dark.png"),
});
await page.keyboard.press("Space");
await expect(page.locator("#keyboard-guide")).toBeVisible();
await expectNoViewportOverflow(page);
await page.screenshot({
path: testInfo.outputPath("keyboard-mobile-leader-dark.png"),
});
await page.keyboard.press("Escape");
await page.keyboard.press("?");
await expect(page.locator("#keyboard-help")).toHaveAttribute("open", "");
await expectNoViewportOverflow(page);
await page.screenshot({
path: testInfo.outputPath("keyboard-mobile-help-dark.png"),
});
await closeKeyboardHelp(page);
await pressSequence(page, "Space", "a");
await expect(page.locator("#keyboard-status")).toContainText("TEXT");
await expect(page.locator("#question")).toBeFocused();
await expect(page.locator('[data-keyboard-pane="drawer"]')).toBeVisible();
await expect(page.locator("#keyboard-context")).toBeHidden();
await expect(page.locator("#keyboard-return-hint")).toBeVisible();
await expectNoViewportOverflow(page);
await page.screenshot({
path: testInfo.outputPath("keyboard-mobile-typing-dark.png"),
});
});