fix(ui): keep PR chip above transcript rows (#130224)

* fix(ui): keep PR chip above transcript rows

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

* fix(ui): keep PR chip above transcript rows

OpenClaw-Publication: d6486664-bbca-4012-ae1d-0b5149319c9e

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
RoboClaw
2026-08-26 10:53:33 -07:00
committed by GitHub
parent f7bd83d1b2
commit f1491a203c
2 changed files with 109 additions and 1 deletions
+105 -1
View File
@@ -1,7 +1,7 @@
// Control UI tests cover session pull request chips above the chat composer.
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { chromium, type Browser, type BrowserContext } from "playwright";
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { CONTROL_UI_SESSION_PULL_REQUESTS_CHANGED_EVENT } from "../../../src/gateway/control-ui-contract.js";
import { SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD } from "../lib/session-pull-requests.ts";
@@ -25,6 +25,12 @@ const publicationProofDir = path.join(
"control-ui-e2e",
"github-publication",
);
const stackingProofDir = path.join(
process.cwd(),
".artifacts",
"control-ui-e2e",
"pr-chip-stacking",
);
let server: ControlUiE2eServer;
// Browser contexts preserve test isolation; keep one process warm for this file.
@@ -47,6 +53,45 @@ async function closeContexts(): Promise<void> {
openContexts.clear();
}
async function expectPullRequestChipOnTop(page: Page): Promise<void> {
const chip = page.locator(".chat-pr").first();
await chip.waitFor();
const uncovered = await chip.evaluate((element) => {
const bounds = element.getBoundingClientRect();
const sampleY = Math.min(bounds.bottom - 1, bounds.top + 10);
return [0.2, 0.5, 0.8].every((ratio) => {
const sampleX = bounds.left + bounds.width * ratio;
return document.elementFromPoint(sampleX, sampleY)?.closest(".chat-pr") === element;
});
});
expect(uncovered).toBe(true);
}
async function overlapLastTranscriptRowWithPullRequestChip(page: Page): Promise<void> {
const row = page.locator(".chat-virtual-row").last();
const chip = page.locator(".chat-pr").first();
await row.waitFor();
await chip.waitFor();
await page.evaluate(() => {
const rows = document.querySelectorAll<HTMLElement>(".chat-virtual-row");
const rowElement = rows.item(rows.length - 1);
const chipElement = document.querySelector<HTMLElement>(".chat-pr");
if (!rowElement || !chipElement) {
throw new Error("Expected a virtual transcript row and pull request chip");
}
const paintedRow = rowElement.querySelector<HTMLElement>(".chat-bubble") ?? rowElement;
const paintedBounds = paintedRow.getBoundingClientRect();
const chipBounds = chipElement.getBoundingClientRect();
rowElement.style.top = `${chipBounds.top + 24 - paintedBounds.bottom}px`;
});
await page.evaluate(
() =>
new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve());
}),
);
}
async function waitForWatchedSessionKey(
gateway: Awaited<ReturnType<typeof installMockGateway>>,
): Promise<string> {
@@ -222,6 +267,65 @@ describeControlUiE2e("session pull request chips", () => {
.toBe("#103469");
});
it.each([
{ label: "desktop", viewport: { width: 1180, height: 800 } },
{ label: "mobile", viewport: { width: 393, height: 852 } },
])(
"keeps the PR chip above an underlapping transcript on $label",
async ({ label, viewport }) => {
const context = await browser.newContext({
colorScheme: "light",
locale: "en-US",
serviceWorkers: "block",
viewport,
});
openContexts.add(context);
const page = await context.newPage();
const gateway = await installMockGateway(page, {
featureMethods: ["chat.metadata", "chat.startup", SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD],
historyMessages: Array.from({ length: 25 }, (_, index) => ({
role: index % 2 === 0 ? "user" : "assistant",
content: `Transcript row ${index + 1}: paint-order regression fixture.`,
timestamp: index + 1,
})),
methodResponses: {
[SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD]: { subscribed: true },
},
});
await page.goto(`${server.baseUrl}chat`);
const watchedKey = await waitForWatchedSessionKey(gateway);
await gateway.emitGatewayEvent(CONTROL_UI_SESSION_PULL_REQUESTS_CHANGED_EVENT, {
sessions: {
[watchedKey]: {
pullRequests: [
{
number: 123456,
owner: "openclaw",
repo: "openclaw",
branch: "fix/pr-chip-stacking",
title: "Keep the PR chip above the transcript",
url: "https://github.com/openclaw/openclaw/pull/123456",
state: "open",
},
],
rateLimited: false,
status: "ok",
},
},
});
await overlapLastTranscriptRowWithPullRequestChip(page);
if (captureUiProof) {
await mkdir(stackingProofDir, { recursive: true });
await page.screenshot({
animations: "disabled",
path: path.join(stackingProofDir, `${label}.png`),
});
}
await expectPullRequestChipOnTop(page);
},
);
it("offers a Publish PR row with the stale warning while rate limited pre-PR", async () => {
const context = await newBrowserContext();
const page = await context.newPage();
+4
View File
@@ -2890,6 +2890,10 @@ button.chat-reply-preview--message:disabled {
}
.chat-prs {
/* The transcript intentionally underlaps the composer stack; keep its
positioned virtual rows from painting over this interactive surface. */
position: relative;
z-index: 1;
display: grid;
gap: 6px;
/* Mirror .agent-chat__composer-shell sizing: the rows belong to the input