fix(web-shell): hide turn navigation without sufficient gutters (#15604)

* fix(web-shell): hide turn navigation without sufficient gutters

* test(web-shell): cover navigation visibility across desktop layouts

---------

Co-authored-by: 钉萁 <[email protected]>
This commit is contained in:
ytahdnand钉萁 authored and GitHub committed 2026-10-26 17:17:17 +08:00
1 parent b63b733642
commit 9df10f873b
5 files changed
+60 -23

No files matched your search

@@ -51,7 +51,7 @@ test('search locates persisted history outside the live window without changing
for (const theme of ['light', 'dark']) {
for (const language of ['en', 'zh-CN']) {
for (const width of [1440, 390]) {
for (const width of [1600, 1440, 390]) {
test(`conversation search content and layout ${theme} ${language} ${width} @smoke`, async ({
page,
baseURL,
@@ -74,19 +74,20 @@ for (const theme of ['light', 'dark']) {
exact: true,
});
await expect(bottom).toBeVisible();
await expect(button).toBeVisible();
const searchBox = await button.boundingBox();
const timeline = page.getByRole('navigation', {
name: /Session timeline|会话时间线/,
});
await expect(
timeline.getByRole('button', {
name: /Search this conversation|搜索当前会话/,
exact: true,
}),
).toBeVisible();
expect(searchBox!.width).toBeLessThanOrEqual(20);
expect(searchBox!.height).toBe(24);
if (width === 1440) {
if (width === 1600) {
await expect(
timeline.getByRole('button', {
name: /Search this conversation|搜索当前会话/,
exact: true,
}),
).toBeVisible();
const tick = await timeline
.locator('li button span')
.first()
@@ -96,6 +97,8 @@ for (const theme of ['light', 'dark']) {
searchBox!.x + searchBox!.width / 2 - (tick!.x + tick!.width / 2),
),
).toBeLessThan(1);
} else {
await expect(timeline).toBeHidden();
}
await page.screenshot({
path: `/tmp/qwen-12231-entry-${theme}-${language}-${width}.png`,
@@ -50,7 +50,7 @@ async function historyScenario(
baseURL: string | undefined,
pageRecords: number,
) {
await page.setViewportSize({ width: 1440, height: 900 });
await page.setViewportSize({ width: 1600, height: 900 });
const count = pageRecords * 12;
const sessionId = `history-navigation-${pageRecords}`;
const live = Array.from({ length: 40 }, (_, index) =>
@@ -29,7 +29,7 @@ for (const navigation of [true, false]) {
page,
baseURL,
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.setViewportSize({ width: 1600, height: 900 });
const scenario = createWebShellDaemonScenario({
events: Array.from({ length: 4 }, (_, index) => [
userTextEvent('What is the weather?', { id: index * 3 + 1 }),
@@ -117,7 +117,9 @@ for (const navigation of [true, false]) {
'color(srgb 1 1 1 / 0.96)',
);
await scrollToBottom.click();
for (const width of [1440, 1300, 1261, 1260, 1259, 1000, 802, 700, 599]) {
for (const width of [
1600, 1440, 1405, 1404, 1403, 1300, 1000, 802, 700, 599,
]) {
await page.setViewportSize({ width, height: 900 });
await expectSameEdges(message, composer);
const viewport = page.locator('[data-history-viewport]');
@@ -132,7 +134,7 @@ for (const navigation of [true, false]) {
})
.toBeLessThanOrEqual(1);
const viewportBox = await viewport.boundingBox();
if (viewportBox && viewportBox.width >= 1000) {
if (viewportBox && viewportBox.width >= 1128) {
await expect(rail).toBeVisible();
const railBox = await rail.boundingBox();
const messageBox = await message.boundingBox();
@@ -142,7 +144,7 @@ for (const navigation of [true, false]) {
}
}
{
await page.setViewportSize({ width: 1440, height: 900 });
await page.setViewportSize({ width: 1600, height: 900 });
const shell = page.locator('[data-web-shell-root]');
const sidebar = page.getByRole('complementary', {
name: 'Workspace sidebar',
@@ -155,17 +157,37 @@ for (const navigation of [true, false]) {
'--chat-regular-content-width',
`${width}px`,
);
element.style.setProperty('--chat-content-width', `${width}px`);
element.style.setProperty('--chat-shell-width', `${width + 40}px`);
}, minWidth);
for (const offset of [-1, 0, 1]) {
await page.setViewportSize({
width: minWidth + sidebarWidth + offset,
width: minWidth + 128 + sidebarWidth + offset,
height: 900,
});
if (offset < 0) await expect(rail).toBeHidden();
else await expect(rail).toBeVisible();
else {
await expect(rail).toBeVisible();
const button = rail.locator('button').first();
const tick = button.locator(':scope > span').first();
await button.hover();
await expect
.poll(async () => (await tick.boundingBox())?.width ?? 0)
.toBeGreaterThan(27);
await expect
.poll(async () => {
const tickBox = await tick.boundingBox();
const messageBox = await message.boundingBox();
return tickBox && messageBox
? messageBox.x - tickBox.x - tickBox.width
: -Infinity;
})
.toBeGreaterThanOrEqual(16);
await page.mouse.move(500, 10);
}
}
}
await page.setViewportSize({ width: 1000 + sidebarWidth, height: 900 });
await page.setViewportSize({ width: 1128 + sidebarWidth, height: 900 });
await shell.evaluate((element) => {
element.style.setProperty('--chat-regular-content-width', '1200px');
});
@@ -174,8 +196,17 @@ for (const navigation of [true, false]) {
element.style.setProperty('--chat-regular-content-width', '1000px');
});
await expect(rail).toBeVisible();
await shell.evaluate((element) => {
element.style.setProperty('--chat-content-width', '100%');
});
await expect(rail).toBeHidden();
await shell.evaluate((element) => {
element.style.setProperty('--chat-content-width', '1000px');
element.style.setProperty('--chat-shell-width', '1040px');
});
await expect(rail).toBeVisible();
}
for (const width of [1440, 1360]) {
for (const width of [1600, 1500]) {
await page.setViewportSize({ width, height: 900 });
const button = rail.locator('button').first();
const tick = button.locator(':scope > span').first();
@@ -82,7 +82,7 @@ test('global turn navigation follows transcript scrolling @smoke', async ({
page,
baseURL,
}) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.setViewportSize({ width: 1600, height: 900 });
const live = Array.from({ length: TURNS * 2 }, (_, index) =>
recordEvent(
index,
@@ -178,7 +178,7 @@ test('global turn navigation follows transcript scrolling @smoke', async ({
await expect(rail).toBeHidden();
await scrollTranscriptTo(page, 0.5);
await expect.poll(() => ariaOrdinal(page)).toBeNull();
await page.setViewportSize({ width: 1440, height: 900 });
await page.setViewportSize({ width: 1600, height: 900 });
await expect(rail).toBeVisible();
await expect.poll(() => ariaOrdinal(page)).not.toBeNull();
const ratio = await markerOffsetRatio(page);
@@ -13,13 +13,16 @@ export function useChatNavigationVisible(
}
const container = element.closest('[data-history-viewport]') ?? element;
const update = () => {
const style = getComputedStyle(element);
const minWidth =
Number.parseFloat(
getComputedStyle(element).getPropertyValue(
'--chat-regular-content-width',
),
style.getPropertyValue('--chat-regular-content-width'),
) || 1000;
setVisible(container.getBoundingClientRect().width >= minWidth);
// Reserve 64px gutters so hover-expanded ticks stay clear of the text.
setVisible(
style.getPropertyValue('--chat-content-width').trim() !== '100%' &&
container.getBoundingClientRect().width >= minWidth + 128,
);
};
update();
const resize =