fix: top-align turn bars and constrain history boundary
This commit is contained in:
@@ -35,7 +35,7 @@ async function historyRequestCount(baseUrl: string): Promise<number> {
|
|||||||
return state.history_requests;
|
return state.history_requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
Deno.test("production Console pages backward by real turns while preserving both scroll anchors", async () => {
|
async function checkConsoleHistory(viewportHeight: number): Promise<void> {
|
||||||
const build = await new Deno.Command(Deno.execPath(), {
|
const build = await new Deno.Command(Deno.execPath(), {
|
||||||
args: ["task", "build"],
|
args: ["task", "build"],
|
||||||
cwd: workspaceRoot,
|
cwd: workspaceRoot,
|
||||||
@@ -68,10 +68,11 @@ Deno.test("production Console pages backward by real turns while preserving both
|
|||||||
const browser = await chromium.launch({ headless: true });
|
const browser = await chromium.launch({ headless: true });
|
||||||
try {
|
try {
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
viewport: { width: 1440, height: 600 },
|
viewport: { width: 1440, height: viewportHeight },
|
||||||
colorScheme: "dark",
|
colorScheme: "dark",
|
||||||
});
|
});
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
page.setDefaultTimeout(5_000);
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const responses: string[] = [];
|
const responses: string[] = [];
|
||||||
page.on("console", (message) => {
|
page.on("console", (message) => {
|
||||||
@@ -102,6 +103,20 @@ Deno.test("production Console pages backward by real turns while preserving both
|
|||||||
assertEquals(await historyRequestCount(baseUrl), 1);
|
assertEquals(await historyRequestCount(baseUrl), 1);
|
||||||
assertEquals(await transcript.getByText(/^question (7|8|9|10|11|12)$/).count(), 6);
|
assertEquals(await transcript.getByText(/^question (7|8|9|10|11|12)$/).count(), 6);
|
||||||
assertEquals(await navigation.getByRole("button", { name: /^Turn \d+: question (7|8|9|10|11|12)$/ }).count(), 6);
|
assertEquals(await navigation.getByRole("button", { name: /^Turn \d+: question (7|8|9|10|11|12)$/ }).count(), 6);
|
||||||
|
const turnList = navigation.locator(".turn-list");
|
||||||
|
const initialGeometry = await turnList.evaluate((element) => {
|
||||||
|
const boundary = element.querySelector(".history-boundary")!;
|
||||||
|
const control = boundary.firstElementChild!;
|
||||||
|
const first = element.querySelector(".turn-button")!;
|
||||||
|
return {
|
||||||
|
boundaryHeight: boundary.getBoundingClientRect().height,
|
||||||
|
controlHeight: control.getBoundingClientRect().height,
|
||||||
|
boundaryOffset: boundary.getBoundingClientRect().top - element.getBoundingClientRect().top + element.scrollTop,
|
||||||
|
firstOffset: first.getBoundingClientRect().top - boundary.getBoundingClientRect().top,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assertEquals(initialGeometry, { boundaryHeight: 24, controlHeight: 24, boundaryOffset: 0, firstOffset: 24 });
|
||||||
|
assertEquals(await turnList.evaluate((element) => element.scrollHeight > element.clientHeight), viewportHeight === 340);
|
||||||
const currentQuestionTop = await transcript
|
const currentQuestionTop = await transcript
|
||||||
.getByText("question 7", { exact: true })
|
.getByText("question 7", { exact: true })
|
||||||
.evaluate((element) => element.getBoundingClientRect().top);
|
.evaluate((element) => element.getBoundingClientRect().top);
|
||||||
@@ -116,6 +131,8 @@ Deno.test("production Console pages backward by real turns while preserving both
|
|||||||
const question12 = navigation.getByRole("button", {
|
const question12 = navigation.getByRole("button", {
|
||||||
name: /^Turn \d+: question 12$/,
|
name: /^Turn \d+: question 12$/,
|
||||||
});
|
});
|
||||||
|
await question12.scrollIntoViewIfNeeded();
|
||||||
|
await page.waitForTimeout(50);
|
||||||
await question12.hover();
|
await question12.hover();
|
||||||
const preview = navigation.getByRole("tooltip");
|
const preview = navigation.getByRole("tooltip");
|
||||||
await preview.waitFor();
|
await preview.waitFor();
|
||||||
@@ -159,20 +176,39 @@ Deno.test("production Console pages backward by real turns while preserving both
|
|||||||
"a stationary top sentinel must not drain another page",
|
"a stationary top sentinel must not drain another page",
|
||||||
);
|
);
|
||||||
|
|
||||||
const turnList = navigation.locator(".turn-list");
|
|
||||||
const navigationAfterPrepend = await navigation
|
const navigationAfterPrepend = await navigation
|
||||||
.locator('[data-turn-id="user-8"]')
|
.locator('[data-turn-id="user-8"]')
|
||||||
.evaluate((element) => element.getBoundingClientRect().top);
|
.evaluate((element) => element.getBoundingClientRect().top);
|
||||||
assert(
|
const overflowing = await turnList.evaluate((element) => element.scrollHeight > element.clientHeight);
|
||||||
Math.abs(navigationAfterPrepend - navigationAnchoredTop) <= 1,
|
assertEquals(overflowing, viewportHeight < 600);
|
||||||
`turn-bar anchor moved from ${navigationAnchoredTop}px to ${navigationAfterPrepend}px`,
|
if (overflowing) {
|
||||||
);
|
const scroll = await turnList.evaluate((element) => ({
|
||||||
await turnList.evaluate((element) => {
|
top: element.scrollTop, max: element.scrollHeight - element.clientHeight,
|
||||||
element.scrollTop = Math.max(2, element.scrollTop);
|
}));
|
||||||
element.dispatchEvent(new Event("scroll"));
|
// When a short list first overflows, the requested anchor can exceed
|
||||||
element.scrollTop = 0;
|
// the scroll range. Restore as far as possible without artificial space.
|
||||||
element.dispatchEvent(new Event("scroll"));
|
const requested = scroll.top + navigationAfterPrepend - navigationAnchoredTop;
|
||||||
});
|
assert(Math.abs(scroll.top - Math.max(0, Math.min(scroll.max, requested))) <= 1);
|
||||||
|
if (viewportHeight === 340) {
|
||||||
|
assert(
|
||||||
|
Math.abs(navigationAfterPrepend - navigationAnchoredTop) <= 1,
|
||||||
|
`turn-bar anchor moved from ${navigationAnchoredTop}px to ${navigationAfterPrepend}px`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await turnList.evaluate((element) => {
|
||||||
|
element.scrollTop = Math.max(2, element.scrollTop);
|
||||||
|
element.dispatchEvent(new Event("scroll"));
|
||||||
|
element.scrollTop = 0;
|
||||||
|
element.dispatchEvent(new Event("scroll"));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// A short list cannot scroll: older turns take their natural place above
|
||||||
|
// existing bars instead of adding blank space to force a fixed anchor.
|
||||||
|
const firstOffset = await turnList.evaluate((element) =>
|
||||||
|
element.querySelector(".turn-button")!.getBoundingClientRect().top - element.getBoundingClientRect().top);
|
||||||
|
assertEquals(firstOffset, 24);
|
||||||
|
await navigation.getByRole("button", { name: "Earlier conversation available" }).click();
|
||||||
|
}
|
||||||
await transcript.getByText("question 1", { exact: true }).waitFor();
|
await transcript.getByText("question 1", { exact: true }).waitFor();
|
||||||
await navigation.getByRole("button", { name: /^Turn \d+: question 1$/ }).waitFor();
|
await navigation.getByRole("button", { name: /^Turn \d+: question 1$/ }).waitFor();
|
||||||
assertEquals(await historyRequestCount(baseUrl), 3);
|
assertEquals(await historyRequestCount(baseUrl), 3);
|
||||||
@@ -216,4 +252,9 @@ Deno.test("production Console pages backward by real turns while preserving both
|
|||||||
server.kill("SIGTERM");
|
server.kill("SIGTERM");
|
||||||
await server.status;
|
await server.status;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
for (const viewportHeight of [600, 400, 340]) {
|
||||||
|
Deno.test(`production Console keeps compact top-aligned turns and pages history at ${viewportHeight}px`,
|
||||||
|
() => checkConsoleHistory(viewportHeight));
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,15 +113,16 @@
|
|||||||
|
|
||||||
.turn-bars::-webkit-scrollbar { display: none; }
|
.turn-bars::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
/* Bottom-align short lists so prepending older turns does not move existing bars.
|
.history-boundary { flex: 0 0 24px; }
|
||||||
Once overflowing, scroll-anchor restoration keeps those bars in place. */
|
|
||||||
.history-boundary { margin-top: auto; }
|
|
||||||
.history-boundary button, .history-boundary span {
|
.history-boundary button, .history-boundary span {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
min-height: 24px;
|
height: 24px;
|
||||||
padding: var(--space-1);
|
min-height: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
Reference in New Issue
Block a user