Merge pull request #506 from pikasTech/fix/inner-trace-scroll
fix: preserve inner trace scroll
This commit is contained in:
+192
-18
@@ -204,6 +204,8 @@ const state = {
|
||||
conversationScrollPosition: { top: 0, left: 0 },
|
||||
traceScrollPositions: new Map(),
|
||||
traceScrollUserActiveUntil: new Map(),
|
||||
traceBodyScrollPositions: new Map(),
|
||||
traceBodyScrollUserActiveUntil: new Map(),
|
||||
traceProgrammaticScrollWrites: 0,
|
||||
fullTraceReplayInFlight: new Set(),
|
||||
resultReconciliationInFlight: new Set(),
|
||||
@@ -2387,6 +2389,7 @@ function renderConversation() {
|
||||
const renderVersion = ++state.conversationRenderVersion;
|
||||
captureConversationScrollPosition();
|
||||
captureTraceScrollPositions();
|
||||
captureTraceBodyScrollPositions();
|
||||
const introMessages = [
|
||||
{
|
||||
role: "system",
|
||||
@@ -2399,11 +2402,13 @@ function renderConversation() {
|
||||
replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard));
|
||||
restoreConversationScrollPosition();
|
||||
restoreTraceScrollPositions();
|
||||
restoreTraceBodyScrollPositions();
|
||||
scheduleCodeAgentSessionPersist();
|
||||
window.requestAnimationFrame(() => {
|
||||
if (renderVersion !== state.conversationRenderVersion) return;
|
||||
restoreConversationScrollPosition({ deferred: true });
|
||||
restoreTraceScrollPositions(el.conversationList, { deferred: true });
|
||||
restoreTraceBodyScrollPositions(el.conversationList, { deferred: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2468,6 +2473,62 @@ function restoreTraceScrollPositions(root = el.conversationList, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function captureTraceBodyScrollPositions(root = el.conversationList) {
|
||||
for (const body of root.querySelectorAll(".message-trace-body")) {
|
||||
rememberTraceBodyScrollPosition(body);
|
||||
}
|
||||
}
|
||||
|
||||
function traceBodyScrollKey(body) {
|
||||
const list = body?.closest?.(".message-trace-events[data-trace-ui-key]");
|
||||
const row = body?.closest?.(".message-trace-row[data-trace-row-id]");
|
||||
const traceUiKey = list?.dataset?.traceUiKey;
|
||||
const rowId = row?.dataset?.traceRowId;
|
||||
return traceUiKey && rowId ? `${traceUiKey}:${rowId}` : null;
|
||||
}
|
||||
|
||||
function rememberTraceBodyScrollPosition(body) {
|
||||
const key = traceBodyScrollKey(body);
|
||||
if (!key || !body) return;
|
||||
state.traceBodyScrollPositions.set(key, elementScrollPosition(body));
|
||||
}
|
||||
|
||||
function restoreTraceBodyScrollPositions(root = el.conversationList, options = {}) {
|
||||
for (const body of root.querySelectorAll(".message-trace-body")) {
|
||||
const key = traceBodyScrollKey(body);
|
||||
if (!key) continue;
|
||||
if (options.deferred === true && isTraceBodyScrollUserActive(key)) continue;
|
||||
const position = state.traceBodyScrollPositions.get(key);
|
||||
if (!position) continue;
|
||||
restoreElementScrollPosition(body, position);
|
||||
}
|
||||
}
|
||||
|
||||
function markTraceBodyScrollIntent(body) {
|
||||
const key = traceBodyScrollKey(body);
|
||||
if (!key) return;
|
||||
state.traceBodyScrollUserActiveUntil.set(key, Date.now() + SCROLL_USER_ACTIVITY_MS);
|
||||
}
|
||||
|
||||
function isTraceBodyScrollUserActive(key) {
|
||||
if (!key) return false;
|
||||
return Date.now() < Number(state.traceBodyScrollUserActiveUntil.get(key) ?? 0);
|
||||
}
|
||||
|
||||
function elementScrollPosition(element) {
|
||||
return {
|
||||
top: element.scrollTop,
|
||||
left: element.scrollLeft,
|
||||
bottomGap: scrollBottomGap(element)
|
||||
};
|
||||
}
|
||||
|
||||
function restoreElementScrollPosition(element, position) {
|
||||
if (!element || !position) return;
|
||||
element.scrollTop = scrollTopForPosition(element, position);
|
||||
element.scrollLeft = Math.min(position.left, Math.max(0, element.scrollWidth - element.clientWidth));
|
||||
}
|
||||
|
||||
function scrollBottomGap(element) {
|
||||
return Math.max(0, element.scrollHeight - element.clientHeight - element.scrollTop);
|
||||
}
|
||||
@@ -2537,8 +2598,10 @@ function patchTracePanelElement(panel, replacement) {
|
||||
const currentList = panel.querySelector(".message-trace-events[data-trace-ui-key]");
|
||||
const nextList = replacement.querySelector(".message-trace-events[data-trace-ui-key]");
|
||||
if (currentList && nextList) {
|
||||
captureTraceBodyScrollPositions(currentList);
|
||||
currentList.dataset.traceMode = nextList.dataset.traceMode ?? "";
|
||||
patchTraceEventList(currentList, [...nextList.children]);
|
||||
restoreTraceBodyScrollPositions(currentList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4721,31 +4784,72 @@ function messageTraceCountText(trace, rawTotal, loadedTotal, readableTotal) {
|
||||
}
|
||||
|
||||
function renderTraceEventList(list, rows) {
|
||||
patchTraceEventList(list, rows.map(traceEventRowNode));
|
||||
patchTraceEventList(list, rows.map((row, index) => traceEventRowNode(row, index)));
|
||||
}
|
||||
|
||||
function patchTraceEventList(list, nextItems) {
|
||||
const currentItems = [...list.children];
|
||||
const max = Math.max(currentItems.length, nextItems.length);
|
||||
for (let index = 0; index < max; index += 1) {
|
||||
const current = currentItems[index];
|
||||
const currentById = new Map();
|
||||
for (const current of [...list.children]) {
|
||||
const rowId = current.dataset.traceRowId;
|
||||
if (rowId && !currentById.has(rowId)) currentById.set(rowId, current);
|
||||
}
|
||||
const retained = new Set();
|
||||
for (let index = 0; index < nextItems.length; index += 1) {
|
||||
const next = nextItems[index];
|
||||
if (!next) {
|
||||
current?.remove();
|
||||
continue;
|
||||
}
|
||||
if (!current) {
|
||||
list.append(next);
|
||||
continue;
|
||||
}
|
||||
if (current.dataset.traceRowKey === next.dataset.traceRowKey) continue;
|
||||
current.replaceWith(next);
|
||||
const rowId = next.dataset.traceRowId;
|
||||
const current = rowId ? currentById.get(rowId) : null;
|
||||
const item = current && !retained.has(current) ? current : next;
|
||||
if (item === current) patchTraceRowElement(current, next);
|
||||
const before = list.children[index] ?? null;
|
||||
if (before !== item) list.insertBefore(item, before);
|
||||
retained.add(item);
|
||||
}
|
||||
for (const current of [...list.children]) {
|
||||
if (!retained.has(current)) current.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function traceEventRowNode(row) {
|
||||
function patchTraceRowElement(current, next) {
|
||||
current.className = next.className;
|
||||
current.dataset.traceRowKey = next.dataset.traceRowKey ?? "";
|
||||
current.dataset.traceRowId = next.dataset.traceRowId ?? "";
|
||||
const currentHeader = current.querySelector(".message-trace-line");
|
||||
const nextHeader = next.querySelector(".message-trace-line");
|
||||
if (currentHeader && nextHeader) {
|
||||
currentHeader.className = nextHeader.className;
|
||||
if (currentHeader.textContent !== nextHeader.textContent) currentHeader.textContent = nextHeader.textContent;
|
||||
} else if (!currentHeader && nextHeader) {
|
||||
current.prepend(nextHeader);
|
||||
} else if (currentHeader && !nextHeader) {
|
||||
currentHeader.remove();
|
||||
}
|
||||
const currentBody = current.querySelector(".message-trace-body");
|
||||
const nextBody = next.querySelector(".message-trace-body");
|
||||
if (currentBody && nextBody) {
|
||||
patchTraceBodyElement(currentBody, nextBody);
|
||||
} else if (!currentBody && nextBody) {
|
||||
current.append(nextBody);
|
||||
} else if (currentBody && !nextBody) {
|
||||
currentBody.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function patchTraceBodyElement(currentBody, nextBody) {
|
||||
const position = elementScrollPosition(currentBody);
|
||||
currentBody.className = nextBody.className;
|
||||
currentBody.dataset.traceBodyId = nextBody.dataset.traceBodyId ?? "";
|
||||
if (currentBody.textContent !== nextBody.textContent) {
|
||||
currentBody.textContent = nextBody.textContent;
|
||||
}
|
||||
restoreElementScrollPosition(currentBody, position);
|
||||
rememberTraceBodyScrollPosition(currentBody);
|
||||
}
|
||||
|
||||
function traceEventRowNode(row, index = 0) {
|
||||
const item = document.createElement("li");
|
||||
item.className = `message-trace-row tone-border-${toneClass(row.tone)}`;
|
||||
const rowId = traceRowId(row, index);
|
||||
item.dataset.traceRowId = rowId;
|
||||
item.dataset.traceRowKey = traceRowKey(row);
|
||||
const header = document.createElement("div");
|
||||
header.className = "message-trace-line";
|
||||
@@ -4754,12 +4858,30 @@ function traceEventRowNode(row) {
|
||||
if (row.body) {
|
||||
const body = document.createElement("pre");
|
||||
body.className = "message-trace-body";
|
||||
body.dataset.traceBodyId = rowId;
|
||||
installTraceBodyScrollListeners(body);
|
||||
body.textContent = row.body;
|
||||
item.append(body);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function installTraceBodyScrollListeners(body) {
|
||||
for (const eventName of ["wheel", "touchstart", "pointerdown"]) {
|
||||
body.addEventListener(eventName, () => markTraceBodyScrollIntent(body), { passive: true });
|
||||
}
|
||||
body.addEventListener("keydown", (event) => {
|
||||
if (isScrollIntentKey(event.key)) markTraceBodyScrollIntent(body);
|
||||
});
|
||||
body.addEventListener("scroll", () => rememberTraceBodyScrollPosition(body), { passive: true });
|
||||
}
|
||||
|
||||
function traceRowId(row, index = 0) {
|
||||
if (row?.rowId) return String(row.rowId);
|
||||
if (row?.seq !== null && row?.seq !== undefined) return `event:${row.seq}`;
|
||||
return `row:${index}:${traceRowKey(row)}`;
|
||||
}
|
||||
|
||||
function traceRowKey(row) {
|
||||
return `${row.header}\n${row.body ?? ""}\n${row.tone ?? ""}`;
|
||||
}
|
||||
@@ -4774,6 +4896,9 @@ function installWorkbenchTestHooks() {
|
||||
latestAgentMessageText,
|
||||
traceScrollMetrics,
|
||||
traceDomIdentity,
|
||||
traceBodyDomIdentity,
|
||||
setTraceBodyScrollTop,
|
||||
replaceTraceAssistantStream,
|
||||
setTraceScrollTop,
|
||||
setConversationScrollTop,
|
||||
resetProgrammaticScrollWriteCount
|
||||
@@ -4851,6 +4976,22 @@ function appendTraceEvents(count = 1) {
|
||||
patchMessageTracePanel(message);
|
||||
}
|
||||
|
||||
function replaceTraceAssistantStream(stream = {}) {
|
||||
const message = state.chatMessages.find((item) => item.runnerTrace?.traceId);
|
||||
if (!message) return null;
|
||||
const trace = message.runnerTrace;
|
||||
trace.assistantStreams = [{
|
||||
itemId: "assistant-message",
|
||||
chunkCount: Math.max(1, Number(stream.chunkCount ?? 1)),
|
||||
createdAt: stream.createdAt ?? new Date(1779699000000).toISOString(),
|
||||
updatedAt: stream.updatedAt ?? new Date(1779699001000 + Math.max(1, Number(stream.chunkCount ?? 1)) * 1000).toISOString(),
|
||||
waitingFor: stream.waitingFor ?? "turn/completed",
|
||||
text: String(stream.text ?? "assistant stream fixture")
|
||||
}];
|
||||
patchMessageTracePanel(message);
|
||||
return traceBodyDomIdentity(".message-trace-events[data-trace-ui-key] li:last-child pre");
|
||||
}
|
||||
|
||||
async function reconcileTraceResultFixture(result) {
|
||||
const message = state.chatMessages.find((item) => item.runnerTrace?.traceId || item.traceId);
|
||||
if (!message) return null;
|
||||
@@ -4972,6 +5113,24 @@ function traceDomIdentity() {
|
||||
};
|
||||
}
|
||||
|
||||
function traceBodyDomIdentity(selector = ".message-trace-events[data-trace-ui-key] li:last-child pre") {
|
||||
const body = el.conversationList.querySelector(selector);
|
||||
const row = body?.closest(".message-trace-row") ?? null;
|
||||
if (body && !body.__hwlabTraceIdentity) body.__hwlabTraceIdentity = `body-${Date.now()}-${Math.random()}`;
|
||||
if (row && !row.__hwlabTraceIdentity) row.__hwlabTraceIdentity = `row-${Date.now()}-${Math.random()}`;
|
||||
return {
|
||||
selector,
|
||||
body: body?.__hwlabTraceIdentity ?? null,
|
||||
row: row?.__hwlabTraceIdentity ?? null,
|
||||
rowId: row?.dataset.traceRowId ?? null,
|
||||
top: body?.scrollTop ?? null,
|
||||
left: body?.scrollLeft ?? null,
|
||||
scrollHeight: body?.scrollHeight ?? null,
|
||||
clientHeight: body?.clientHeight ?? null,
|
||||
text: body?.textContent ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function resetProgrammaticScrollWriteCount() {
|
||||
state.traceProgrammaticScrollWrites = 0;
|
||||
return state.traceProgrammaticScrollWrites;
|
||||
@@ -4986,6 +5145,15 @@ function setTraceScrollTop(top, { user = true } = {}) {
|
||||
return traceScrollMetrics();
|
||||
}
|
||||
|
||||
function setTraceBodyScrollTop(selector, top, { user = true } = {}) {
|
||||
const body = el.conversationList.querySelector(selector);
|
||||
if (!body) return traceBodyDomIdentity(selector);
|
||||
if (user) markTraceBodyScrollIntent(body);
|
||||
body.scrollTop = top;
|
||||
rememberTraceBodyScrollPosition(body);
|
||||
return traceBodyDomIdentity(selector);
|
||||
}
|
||||
|
||||
function setConversationScrollTop(top, { user = true } = {}) {
|
||||
if (user) markConversationScrollIntent();
|
||||
el.conversationList.scrollTop = top;
|
||||
@@ -5035,15 +5203,16 @@ function traceDisplayRows(trace, events) {
|
||||
|
||||
function traceAssistantStreamRows(trace) {
|
||||
const streams = Array.isArray(trace?.assistantStreams) ? trace.assistantStreams.filter(Boolean) : [];
|
||||
return streams.map((stream) => traceAssistantStreamRow(trace, stream)).filter(Boolean);
|
||||
return streams.map((stream, index) => traceAssistantStreamRow(trace, stream, index)).filter(Boolean);
|
||||
}
|
||||
|
||||
function traceAssistantStreamRow(trace, stream) {
|
||||
function traceAssistantStreamRow(trace, stream, index = 0) {
|
||||
const chunkCount = Math.max(0, Number(stream?.chunkCount ?? 0));
|
||||
if (chunkCount <= 0) return null;
|
||||
const clock = traceClock(stream.updatedAt ?? stream.createdAt);
|
||||
const total = formatTraceDuration(traceRelativeMs(trace, { createdAt: stream.updatedAt ?? stream.createdAt }));
|
||||
return {
|
||||
rowId: `assistant-stream:${stream.itemId ?? stream.messageId ?? index}`,
|
||||
seq: null,
|
||||
tone: "source",
|
||||
header: `${clock} total=${total} stream assistant message x${chunkCount}`,
|
||||
@@ -5089,6 +5258,7 @@ function traceToolOutputSummaryRows(trace, events) {
|
||||
suffix.trim() || null
|
||||
].filter(Boolean).join(" ");
|
||||
return {
|
||||
rowId: `gateway-jsonrpc:${event.seq ?? first.seq ?? "unknown"}:${payload.id ?? index}`,
|
||||
seq: event.seq ?? null,
|
||||
tone: ok ? "ok" : status === "timed_out" ? "warn" : "blocked",
|
||||
header,
|
||||
@@ -5099,6 +5269,7 @@ function traceToolOutputSummaryRows(trace, events) {
|
||||
const clock = traceClock(first.createdAt);
|
||||
const total = formatTraceDuration(traceRelativeMs(trace, first));
|
||||
return [{
|
||||
rowId: `tool-output:${first.seq ?? "unknown"}`,
|
||||
seq: first.seq ?? null,
|
||||
tone: "source",
|
||||
header: `${clock} total=${total} output cmd output chunks=${visibleEvents.length} out=${compactTraceSize(combined)}`,
|
||||
@@ -5121,6 +5292,7 @@ function traceNoiseSummaryRow(trace, events) {
|
||||
? `assistant message x${visibleEvents.length}`
|
||||
: `trace noise x${visibleEvents.length}`;
|
||||
return {
|
||||
rowId: `trace-noise:${visibleEvents[0]?.seq ?? last.seq ?? "unknown"}:${assistantChunks.length > 0 ? "assistant" : "events"}`,
|
||||
seq: last.seq ?? null,
|
||||
tone: "source",
|
||||
header: `${clock} total=${total} stream ${label}`,
|
||||
@@ -5174,6 +5346,7 @@ function traceDisplayRow(trace, event, options = {}) {
|
||||
].filter(Boolean).join(" ");
|
||||
const body = traceDisplayBody(event);
|
||||
return {
|
||||
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "event"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: event.seq ?? null,
|
||||
tone: traceEventTone(event),
|
||||
header: `${clock} total=${total}${meta ? ` ${meta}` : ""}${label ? ` ${label}` : ""}`,
|
||||
@@ -5196,6 +5369,7 @@ function traceCommandExecutionRow(trace, event) {
|
||||
event.itemId ? `item=${event.itemId}` : null
|
||||
].filter(Boolean).join(" ");
|
||||
return {
|
||||
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "command"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: event.seq ?? null,
|
||||
tone: traceEventTone(event),
|
||||
header: `${clock} total=${total} ${meta}`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer } from "node:http";
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
@@ -123,6 +124,70 @@ test("trace scroll stays user controlled while trace updates append", async () =
|
||||
}
|
||||
});
|
||||
|
||||
test("inner trace message body scroll stays stable when trace rows move or stream text updates", async () => {
|
||||
const browser = await launchBrowser();
|
||||
const server = await startStaticServer(webRoot);
|
||||
try {
|
||||
const page = await browser.newPage({
|
||||
viewport: { width: 1366, height: 768 },
|
||||
deviceScaleFactor: 1
|
||||
});
|
||||
await page.goto(`${server.url}?hwlab-test-hooks=1`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await login(page);
|
||||
await page.waitForFunction(() => window.__hwlabWorkbenchTestHooks, null, { timeout: 12000 });
|
||||
|
||||
const longStreamText = (chunk) => Array.from({ length: 90 }, (_, index) => `assistant stream chunk ${chunk} line ${index + 1} ${"message ".repeat(8)}`).join("\n");
|
||||
const longEvents = Array.from({ length: 110 }, (_, index) => ({
|
||||
seq: index + 1,
|
||||
label: "trace:inner-scroll-contract",
|
||||
type: "trace",
|
||||
status: "observed",
|
||||
message: Array.from({ length: 70 }, (_, line) => `event ${index + 1} line ${line + 1} ${"payload ".repeat(8)}`).join("\n")
|
||||
}));
|
||||
|
||||
await page.evaluate(({ events, text }) => window.__hwlabWorkbenchTestHooks.seedTraceMessage({
|
||||
traceId: "trc_inner_scroll_contract",
|
||||
messageId: "msg_inner_scroll_contract",
|
||||
events,
|
||||
assistantStreams: [{ itemId: "assistant-message", chunkCount: 1, waitingFor: "turn/completed", text }]
|
||||
}), { events: longEvents, text: longStreamText(1) });
|
||||
await page.waitForSelector(".message-trace-events[data-trace-ui-key] li:nth-child(97) pre", { timeout: 12000 });
|
||||
|
||||
const eventBodySelector = ".message-trace-events[data-trace-ui-key] li:nth-child(97) pre";
|
||||
const streamBodySelector = ".message-trace-events[data-trace-ui-key] li:last-child pre";
|
||||
const eventBefore = await page.evaluate((selector) => window.__hwlabWorkbenchTestHooks.setTraceBodyScrollTop(selector, 90, { user: true }), eventBodySelector);
|
||||
const streamBefore = await page.evaluate((selector) => window.__hwlabWorkbenchTestHooks.setTraceBodyScrollTop(selector, 90, { user: true }), streamBodySelector);
|
||||
assert.ok(eventBefore.scrollHeight > eventBefore.clientHeight + 80, JSON.stringify(eventBefore));
|
||||
assert.ok(streamBefore.scrollHeight > streamBefore.clientHeight + 800, JSON.stringify(streamBefore));
|
||||
|
||||
await page.evaluate(() => window.__hwlabWorkbenchTestHooks.appendTraceEvents(5));
|
||||
await page.waitForTimeout(120);
|
||||
const eventAfterAppend = await page.evaluate((selector) => window.__hwlabWorkbenchTestHooks.traceBodyDomIdentity(selector), eventBodySelector);
|
||||
const streamAfterAppend = await page.evaluate((selector) => window.__hwlabWorkbenchTestHooks.traceBodyDomIdentity(selector), streamBodySelector);
|
||||
assert.equal(eventAfterAppend.body, eventBefore.body, "stable existing event body node must be reused while appending trace rows");
|
||||
assert.equal(eventAfterAppend.row, eventBefore.row, "stable existing event row node must be reused while appending trace rows");
|
||||
assert.ok(Math.abs(eventAfterAppend.top - eventBefore.top) <= 8, `event body scroll changed: before=${JSON.stringify(eventBefore)} after=${JSON.stringify(eventAfterAppend)}`);
|
||||
assert.equal(streamAfterAppend.body, streamBefore.body, "assistant stream body node must move instead of being replaced when new events append before it");
|
||||
assert.equal(streamAfterAppend.row, streamBefore.row, "assistant stream row node must move instead of being replaced when new events append before it");
|
||||
assert.ok(Math.abs(streamAfterAppend.top - streamBefore.top) <= 8, `assistant stream body scroll changed after row move: before=${JSON.stringify(streamBefore)} after=${JSON.stringify(streamAfterAppend)}`);
|
||||
|
||||
await page.evaluate((selector) => window.__hwlabWorkbenchTestHooks.setTraceBodyScrollTop(selector, 120, { user: true }), streamBodySelector);
|
||||
const streamBeforeTextUpdate = await page.evaluate((selector) => window.__hwlabWorkbenchTestHooks.traceBodyDomIdentity(selector), streamBodySelector);
|
||||
await page.evaluate((text) => window.__hwlabWorkbenchTestHooks.replaceTraceAssistantStream({ chunkCount: 2, text }), longStreamText(2));
|
||||
await page.waitForTimeout(120);
|
||||
const streamAfterTextUpdate = await page.evaluate((selector) => window.__hwlabWorkbenchTestHooks.traceBodyDomIdentity(selector), streamBodySelector);
|
||||
assert.equal(streamAfterTextUpdate.body, streamBeforeTextUpdate.body, "assistant stream body node must survive stream text growth");
|
||||
assert.equal(streamAfterTextUpdate.row, streamBeforeTextUpdate.row, "assistant stream row node must survive stream text growth");
|
||||
assert.ok(Math.abs(streamAfterTextUpdate.top - streamBeforeTextUpdate.top) <= 8, `assistant stream body scroll changed after text update: before=${JSON.stringify(streamBeforeTextUpdate)} after=${JSON.stringify(streamAfterTextUpdate)}`);
|
||||
assert.match(streamAfterTextUpdate.text, /chunk 2/u);
|
||||
|
||||
await page.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("trace result-ready fixture is reconciled into final assistant result", async () => {
|
||||
const browser = await launchBrowser();
|
||||
const server = await startStaticServer(webRoot);
|
||||
@@ -362,10 +427,17 @@ async function traceMetrics(page) {
|
||||
}
|
||||
|
||||
async function launchBrowser() {
|
||||
const executablePath = process.env.HWLAB_PLAYWRIGHT_CHROMIUM || process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || undefined;
|
||||
const executablePath = process.env.HWLAB_PLAYWRIGHT_CHROMIUM || process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH || systemChromiumExecutablePath();
|
||||
return chromium.launch({ headless: true, executablePath });
|
||||
}
|
||||
|
||||
function systemChromiumExecutablePath() {
|
||||
for (const candidate of ["/usr/bin/chromium-browser", "/usr/bin/chromium", "/snap/bin/chromium", "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"]) {
|
||||
if (existsSync(candidate)) return candidate;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function expectCount(page, selector, expected) {
|
||||
assert.equal(await page.locator(selector).count(), expected, selector);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user