fix(app): restore tabs without layout flicker

This commit is contained in:
Mohamed Boudra
2026-08-18 00:21:51 +02:00
parent ed8371d083
commit 2538dd1e7b
18 changed files with 86 additions and 59 deletions
@@ -28,7 +28,7 @@ async function completeSubmittedTurn(
async function rewindConversation(page: Page, userMessage: Locator, prompt: string): Promise<void> {
await userMessage.getByText(prompt, { exact: true }).hover();
await userMessage.getByRole("button", { name: "Rewind to this message", exact: true }).click();
await page.getByRole("button", { name: "Rewind conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "Rewind conversation", exact: true }).click();
}
async function expectTurnCompletesNormally(
@@ -45,9 +45,9 @@ test("keeps the selected workspace visible in Pure black", async ({ page }, test
}
});
test("applies the interface font size to settings text", async ({ page }) => {
test("applies the base font size to settings text", async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem("@paseo:app-settings", JSON.stringify({ uiFontSize: 24 }));
localStorage.setItem("@paseo:app-settings", JSON.stringify({ uiBaseFontSize: 21 }));
});
await page.goto("/settings");
await expect(page.getByTestId("settings-sidebar")).toBeVisible();
@@ -56,11 +56,11 @@ test("applies the interface font size to settings text", async ({ page }) => {
const sectionTitle = page.getByText("Theme", { exact: true }).first();
await expect(sectionTitle).toHaveCSS("font-size", "18px");
const fontSizeInput = page.getByLabel("Interface font size");
await expect(fontSizeInput).toHaveValue("24");
const fontSizeInput = page.getByLabel("Base font size");
await expect(fontSizeInput).toHaveValue("21");
await fontSizeInput.fill("12");
await fontSizeInput.press("Tab");
await expect(fontSizeInput).toHaveValue("12");
await expect(sectionTitle).toHaveCSS("font-size", "9px");
await expect(sectionTitle).toHaveCSS("font-size", "10px");
});
@@ -12,12 +12,12 @@ test("Settings language selector switches General labels", async ({ page }) => {
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
await page.getByRole("button", { name: "System", exact: true }).click();
await page.getByRole("button", { name: "简体中文 - Simplified Chinese", exact: true }).click();
await page.getByRole("menuitem", { name: "简体中文 - Simplified Chinese", exact: true }).click();
await expect(page.getByText("默认发送", { exact: true }).first()).toBeVisible();
await page.getByRole("button", { name: "简体中文", exact: true }).click();
await page.getByRole("button", { name: "English - 英语", exact: true }).click();
await page.getByRole("menuitem", { name: "English - 英语", exact: true }).click();
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
});
@@ -30,12 +30,12 @@ test("Settings language selector switches to Korean", async ({ page }) => {
await openSettingsSection(page, "general");
await page.getByRole("button", { name: "System", exact: true }).click();
await page.getByRole("button", { name: "한국어 - Korean", exact: true }).click();
await page.getByRole("menuitem", { name: "한국어 - Korean", exact: true }).click();
await expect(page.getByText("기본 전송", { exact: true }).first()).toBeVisible();
await page.getByRole("button", { name: "한국어", exact: true }).click();
await page.getByRole("button", { name: "English - 영어", exact: true }).click();
await page.getByRole("menuitem", { name: "English - 영어", exact: true }).click();
await expect(page.getByText("Default send", { exact: true }).first()).toBeVisible();
});
@@ -2,6 +2,7 @@ import type { Page } from "@playwright/test";
import { test, expect } from "../support/fixtures";
import { TerminalE2EHarness, withTerminalInApp } from "../support/helpers/terminal-dsl";
import { waitForTerminalContent } from "../support/helpers/terminal-perf";
import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions";
interface TerminalSize {
rows: number | null;
@@ -82,7 +83,7 @@ async function verifySplitDownResizesPty(page: Page, harness: TerminalE2EHarness
// Split the pane downward. This focuses the new empty pane, so the terminal
// pane is unfocused at the exact moment its container shrinks.
await page.getByRole("button", { name: "Split pane down" }).first().click();
await runWorkspaceActionFromCommandCenter(page, "Split pane down");
// The local xterm renderer shrinks immediately on split - the part of the
// behaviour that already works and that makes the bug look like nothing changed.
@@ -6,6 +6,7 @@ import { getServerId } from "../support/helpers/server-id";
import { observeTimelineSubscriptions } from "../support/helpers/timeline-delivery";
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
import { installDaemonWebSocketGate } from "../support/helpers/daemon-websocket-gate";
import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions";
import {
expectAgentIdle,
expectInlineWorkingIndicator,
@@ -150,7 +151,7 @@ test.describe("Viewed agent timelines", () => {
try {
await enableMoveTabShortcut(page);
await openAgent(page, scenario, scenario.firstAgentId);
await page.getByRole("button", { name: "Split pane right" }).click();
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
await selectAgent(page, "Second viewed chat");
await moveActiveTabRight(page);
await expect(
@@ -43,7 +43,9 @@ test("sidebar shortcuts exit focus mode and execute their action", async ({
await pressRightSidebarShortcut(page);
await expect(exitFocusModeButton(page)).toHaveCount(0);
await expect(page.getByText("Changes", { exact: true })).toBeVisible();
await expect(
page.getByText("Changes", { exact: true }).filter({ visible: true }).first(),
).toBeVisible();
});
test("sidebar shortcuts outside a workspace preserve its focus mode", async ({
@@ -9,6 +9,7 @@ import { seedWorkspace } from "../support/helpers/seed-client";
import { getServerId } from "../support/helpers/server-id";
import { clickSettingsBackToWorkspace, openCompactSettings } from "../support/helpers/settings";
import { openSettings } from "../support/helpers/app";
import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions";
import {
clickFirstTerminalTab,
waitForWorkspaceTabsVisible,
@@ -65,7 +66,7 @@ test.describe("Workspace pane mounting", () => {
.elementHandle();
expect(originalComposer).not.toBeNull();
await page.getByRole("button", { name: "Split pane right" }).first().click();
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
await expect(page.getByTestId("message-input-root").filter({ visible: true })).toHaveCount(
2,
{ timeout: 30_000 },
@@ -58,7 +58,7 @@ function inFlightTurn(page: Page) {
async function openForkMenu(page: Page, trigger: ReturnType<Page["getByRole"]>): Promise<void> {
await trigger.click();
await expect(page.getByRole("button", { name: "Fork in a new tab" })).toBeVisible({
await expect(page.getByRole("menuitem", { name: "Fork in a new tab" })).toBeVisible({
timeout: 10_000,
});
}
@@ -80,7 +80,7 @@ export async function openMostRecentAssistantForkMenu(page: Page): Promise<void>
export async function forkMostRecentAssistantTurnToNewTab(page: Page): Promise<void> {
await openMostRecentAssistantForkMenu(page);
await page.getByRole("button", { name: "Fork in a new tab" }).click();
await page.getByRole("menuitem", { name: "Fork in a new tab" }).click();
}
export async function expectInFlightForkAvailable(page: Page): Promise<void> {
@@ -92,12 +92,12 @@ export async function expectInFlightForkAvailable(page: Page): Promise<void> {
export async function forkInFlightTurnToNewTab(page: Page): Promise<void> {
const trigger = inFlightTurn(page).getByRole("button", { name: "Fork chat" });
await openForkMenu(page, trigger);
await page.getByRole("button", { name: "Fork in a new tab" }).click();
await page.getByRole("menuitem", { name: "Fork in a new tab" }).click();
}
export async function forkMostRecentAssistantTurnToNewWorkspace(page: Page): Promise<void> {
await openMostRecentAssistantForkMenu(page);
await page.getByRole("button", { name: "Fork in a new workspace" }).click();
await page.getByRole("menuitem", { name: "Fork in a new workspace" }).click();
}
export async function expectChatHistoryAttachment(page: Page): Promise<void> {
@@ -1,6 +1,7 @@
import { expect, type Locator, type Page } from "@playwright/test";
import { openSettings } from "./app";
import { openSettingsSection } from "./settings";
import { runWorkspaceActionFromCommandCenter } from "./command-center-workspace-actions";
export function chatOutlineRail(page: Page): Locator {
return page.getByTestId("chat-outline-rail");
@@ -41,7 +42,7 @@ export async function clickChatOutlineRowEdge(page: Page, position: number): Pro
}
export async function splitCurrentPanelRight(page: Page): Promise<void> {
await page.getByRole("button", { name: "Split pane right" }).first().click();
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
}
export async function disableChatOutlineFromAppearance(page: Page): Promise<void> {
@@ -64,3 +64,12 @@ export async function createTerminalFromCommandCenter(page: Page, panel: Locator
await page.keyboard.press("Enter");
await expectTerminalTabOpen(page);
}
export async function runWorkspaceActionFromCommandCenter(
page: Page,
title: string,
): Promise<void> {
const panel = await openCommandCenter(page);
await panel.getByTestId("command-center-input").fill(title);
await action(panel, title).click();
}
+1 -1
View File
@@ -82,7 +82,7 @@ export async function openAttachmentMenu(page: Page): Promise<void> {
export async function expectAttachmentSheetRowsOnTitleRail(page: Page): Promise<void> {
const title = page.getByText("Add attachment", { exact: true });
const firstItemGlyph = page
.getByRole("button", { name: "Add image", exact: true })
.getByRole("menuitem", { name: "Add image", exact: true })
.locator("svg")
.first();
await waitForSettledPosition(title);
+2 -2
View File
@@ -177,7 +177,7 @@ export async function renameHostFromSettings(page: Page, name: string): Promise<
export async function chooseHostColor(page: Page, colorName: string): Promise<void> {
await page.getByRole("button", { name: /^Color, / }).click();
await page.getByRole("button", { name: colorName, exact: true }).click();
await page.getByRole("menuitem", { name: colorName, exact: true }).click();
await expect(page.getByRole("button", { name: `Color, ${colorName}` })).toBeVisible();
}
@@ -186,7 +186,7 @@ export async function chooseHostBadgeDisplay(
option: "Name" | "Icon only" | "Hidden",
): Promise<void> {
await page.getByTestId("host-appearance-badge-display").click();
await page.getByRole("button", { name: option, exact: true }).click();
await page.getByRole("menuitem", { name: option, exact: true }).click();
await expect(page.getByRole("button", { name: `Sidebar badge, ${option}` })).toBeVisible();
}
@@ -239,7 +239,7 @@ export async function removeProjectScript(page: Page, scriptName: string): Promi
const id = (await row.getAttribute("data-testid"))!.replace("script-row-", "");
await page.getByTestId(`script-row-menu-${id}`).click();
page.once("dialog", (dialog) => void dialog.accept());
await page.getByRole("button", { name: "Remove" }).click();
await page.getByRole("menuitem", { name: "Remove" }).click();
}
// --- File manipulation ---
+5 -5
View File
@@ -77,9 +77,9 @@ export async function expectWorkspaceContextMenuActions(
): Promise<void> {
const workspaceKey = `${getServerId()}:${workspaceId}`;
const menu = page.getByTestId(`sidebar-workspace-context-menu-${workspaceKey}`);
await expect(menu.getByRole("button", { name: "Copy path" })).toBeVisible();
await expect(menu.getByRole("button", { name: "Rename workspace" })).toBeVisible();
await expect(menu.getByRole("button", { name: "Archive" })).toBeVisible();
await expect(menu.getByRole("menuitem", { name: "Copy path" })).toBeVisible();
await expect(menu.getByRole("menuitem", { name: "Rename workspace" })).toBeVisible();
await expect(menu.getByRole("menuitem", { name: "Archive" })).toBeVisible();
}
export async function openProjectContextMenu(page: Page, projectViewKey: string): Promise<void> {
@@ -101,8 +101,8 @@ export async function expectProjectContextMenuActions(
projectViewKey: string,
): Promise<void> {
const menu = page.getByTestId(`sidebar-project-context-menu-${projectViewKey}`);
await expect(menu.getByRole("button", { name: "Open project settings" })).toBeVisible();
await expect(menu.getByRole("button", { name: "Remove project" })).toBeVisible();
await expect(menu.getByRole("menuitem", { name: "Open project settings" })).toBeVisible();
await expect(menu.getByRole("menuitem", { name: "Remove project" })).toBeVisible();
}
export async function selectWorkspaceInSidebar(page: Page, workspaceId: string): Promise<void> {
+5 -1
View File
@@ -1109,7 +1109,11 @@ function SettingsSidebar({
testID="settings-back-to-workspace"
/>
</View>
<ScrollView style={sidebarStyles.scrollBody} showsVerticalScrollIndicator={false}>
<ScrollView
style={sidebarStyles.scrollBody}
showsVerticalScrollIndicator={false}
testID="settings-sidebar-scroll-body"
>
{sidebarBody}
</ScrollView>
</View>
@@ -1167,7 +1167,9 @@ function ResolvedWorkspaceDesktopTabsRow({
const tabScrollOffset = useSharedValue(0);
const tabScrollViewportWidth = useSharedValue(0);
const tabScrollContentWidth = useSharedValue(0);
const labelMeasurementsRef = useRef(new Map<string, WorkspaceTabLabelMeasurement>());
const [labelMeasurements, setLabelMeasurements] = useState(
() => new Map<string, WorkspaceTabLabelMeasurement>(),
);
const [trackSnapshot, setTrackSnapshot] = useState<WorkspaceTabTrackSnapshot | null>(null);
const workspaceRoot = useWorkspaceDirectory(normalizedServerId, normalizedWorkspaceId) ?? "";
const checkoutStatus = useCheckoutStatusQuery({
@@ -1298,24 +1300,29 @@ function ResolvedWorkspaceDesktopTabsRow({
[fallbackTabLabels, tabs],
);
const tabLabelSignature = useMemo(() => workspaceTabLabelSignature(tabLabels), [tabLabels]);
const currentTabLabelKeys = useMemo(() => new Set(tabLabels.map(({ key }) => key)), [tabLabels]);
useEffect(() => {
setLabelMeasurements((current) => {
const removedKeys = [...current.keys()].filter((key) => !currentTabLabelKeys.has(key));
if (removedKeys.length === 0) {
return current;
}
const next = new Map(current);
for (const key of removedKeys) {
next.delete(key);
}
return next;
});
}, [currentTabLabelKeys]);
const publishMeasuredTrack = useCallback(() => {
if (tabsContainerWidth <= 0) {
return;
}
const labelWidths = completeWorkspaceTabLabelWidths(tabLabels, labelMeasurementsRef.current);
const labelWidths = completeWorkspaceTabLabelWidths(tabLabels, labelMeasurements);
if (!labelWidths) {
return;
}
const retainedMeasurements = new Map<string, WorkspaceTabLabelMeasurement>();
for (const { key } of tabLabels) {
const measurement = labelMeasurementsRef.current.get(key);
if (measurement) {
retainedMeasurements.set(key, measurement);
}
}
labelMeasurementsRef.current = retainedMeasurements;
setTrackSnapshot((current) => {
if (
current?.signature === tabLabelSignature &&
@@ -1330,7 +1337,7 @@ function ResolvedWorkspaceDesktopTabsRow({
labelWidths,
};
});
}, [tabLabelSignature, tabLabels, tabs, tabsContainerWidth]);
}, [labelMeasurements, tabLabelSignature, tabLabels, tabs, tabsContainerWidth]);
useLayoutEffect(() => {
publishMeasuredTrack();
@@ -1342,14 +1349,17 @@ function ResolvedWorkspaceDesktopTabsRow({
if (width <= 0) {
return;
}
const current = labelMeasurementsRef.current.get(key);
if (current?.label === label && current.width === width) {
return;
}
labelMeasurementsRef.current.set(key, { label, width });
publishMeasuredTrack();
setLabelMeasurements((current) => {
const measurement = current.get(key);
if (measurement?.label === label && measurement.width === width) {
return current;
}
const next = new Map(current);
next.set(key, { label, width });
return next;
});
},
[publishMeasuredTrack],
[],
);
const displayedTabs = useMemo(() => {
@@ -156,13 +156,13 @@ test("a rebound shortcut lists its new keys in the shortcuts cheat sheet", async
await page.keyboard.press("Alt+Shift+K");
await page.getByText("Done", { exact: true }).click();
const reboundKeys = page.getByText("⌥+Shift+K", { exact: true });
const reboundKeys = page.getByText("⌥⇧K", { exact: true });
await expect(reboundKeys).toBeVisible();
await expect(defaultKeys).toHaveCount(0);
const dialog = await openCheatSheet(page);
const row = dialog.getByTestId(`shortcut-help-row-${SHORTCUTS_ROW}`);
await expect(row.getByText("⌥+Shift+K", { exact: true })).toBeVisible();
await expect(row.getByText("⌥⇧K", { exact: true })).toBeVisible();
// The whole point: the cheat sheet stops advertising the shipped default.
await expect(row.getByText("?", { exact: true })).toHaveCount(0);
});
@@ -25,19 +25,17 @@ test.describe("Settings sidebar scrolling", () => {
await openSettings(page);
const sidebar = page.getByTestId("settings-sidebar");
const scrollBody = page.getByTestId("settings-sidebar-scroll-body");
await expect(sidebar).toBeVisible();
await expect(scrollBody).toBeVisible();
const geometry = await sidebar.evaluate((node) => {
let scroller: HTMLElement | null = null;
for (const element of node.querySelectorAll<HTMLElement>("*")) {
if (element.scrollHeight > element.clientHeight) {
scroller = element;
break;
}
}
if (!scroller) return null;
const scrollBodyElement = node.querySelector<HTMLElement>(
'[data-testid="settings-sidebar-scroll-body"]',
);
if (!scrollBodyElement) return null;
const scrollerRect = scroller.getBoundingClientRect();
const scrollerRect = scrollBodyElement.getBoundingClientRect();
const dragRegions = [];
for (const element of node.querySelectorAll<HTMLElement>("*")) {
if (getComputedStyle(element).getPropertyValue("-webkit-app-region") === "drag") {