fix(app): restore tabs without layout flicker
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
setLabelMeasurements((current) => {
|
||||
const measurement = current.get(key);
|
||||
if (measurement?.label === label && measurement.width === width) {
|
||||
return current;
|
||||
}
|
||||
labelMeasurementsRef.current.set(key, { label, width });
|
||||
publishMeasuredTrack();
|
||||
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") {
|
||||
|
||||
Reference in New Issue
Block a user