Files
paseo/packages/app/e2e/browser/explorer-surface-upgrade.spec.ts
T
Mohamed Boudra 345638dc7f fix(app): retire the docked explorer sidebar
Upgrading to 0.5.0-beta.1 left the old docked explorer sidebar showing
next to the new explorer pane, with no way to dismiss it.

The sidebar rendered off the persisted `desktop.fileExplorerOpen` flag.
#3287 rerouted the explorer toggle to the workspace-layout explorer pane,
so nothing on a wide layout ever wrote that flag false again, and no
migration cleared it. Assistant directory links and review attachments
could still set it. On Windows and Linux the sidebar's close button was
suppressed because the OS window controls own the top-right corner, so
there was no affordance at all.

The explorer is now one surface picked from two facts — is the layout
compact, and can the platform render split panes:

  compact                -> slide-over overlay (panel store)
  non-compact + splits   -> dedicated right pane (layout store)
  non-compact, no splits -> ordinary tabs in the focused pane

`workspace-tabs/explorer-surface.ts` owns that choice and every entry
point routes through it, so the toggle, Ctrl+E, Ctrl+., directory links,
review attachments and side file opens can no longer disagree about where
the explorer lives.

Pane splits stay gated on `isWeb`: they depend on dnd-kit and DOM-backed
accessibility helpers. Native tablets are non-compact without splits, so
they get explorer tabs rather than a second pane — deliberately trading
side-by-side agent and Changes for a surface that only renders what the
single-column tab row can actually reach.

Ctrl+. is handled by the workspace screen rather than the app shell. The
pathname alone cannot identify the active workspace: desktop cold-starts
at "/" and restores the remembered workspace from route params, so a key
derived from the pathname is null exactly when a workspace is mounted,
and the explorer reads as permanently closed.

`desktop.fileExplorerOpen` and `explorerWidth` are dropped by a migration
to panel-state v15 but stay declared in the persisted schema. That schema
is a strict object and the validated storage deletes the whole entry when
parsing fails, so removing the keys outright would wipe every upgrading
user's sidebar width, expanded paths and diff-collapse state.
2026-08-19 15:52:51 +02:00

131 lines
5.0 KiB
TypeScript

// Reported on 0.5.0-beta.1 desktop: after upgrading, the old docked explorer
// sidebar and the new explorer pane were both visible, and on Windows the
// sidebar had no close button because the window controls own that corner.
//
// The sidebar rendered off a persisted `desktop.fileExplorerOpen` flag that no
// migration cleared and that the rerouted explorer toggle no longer wrote. The
// explorer is now one surface per layout, so that flag must not resurrect it.
import { expect, type Locator, type Page } from "@playwright/test";
import { test } from "../support/fixtures";
import { gotoWorkspace } from "../support/helpers/launcher";
import { seedWorkspace } from "../support/helpers/seed-client";
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
function visible(page: Page, testId: string): Locator {
return page.getByTestId(testId).filter({ visible: true });
}
function readPersistedPanelState(page: Page): Promise<string | null> {
return page.evaluate(() => localStorage.getItem("panel-state"));
}
/** Only the docked sidebar ever rendered `explorer-header` on a wide layout. */
function dockedSidebar(page: Page): Locator {
return visible(page, "explorer-header");
}
/** The explorer pane is the tab row carrying the Changes tab. */
function explorerTabRow(page: Page): Locator {
return visible(page, "workspace-tabs-row").filter({
has: page.getByTestId("workspace-tab-working_diff"),
});
}
/** Seed exactly what a pre-pane build wrote after the user opened the sidebar once. */
async function seedRetiredSidebarState(page: Page): Promise<void> {
await page.addInitScript(() => {
localStorage.setItem(
"panel-state",
JSON.stringify({
version: 14,
state: {
desktop: { agentListOpen: true, fileExplorerOpen: true, focusModeEnabled: false },
explorerTab: "changes",
explorerTabByCheckout: {},
expandedPathsByWorkspace: {},
diffCollapsedFoldersByWorkspace: {},
collapsedFilePathsByWorkspace: {},
sidebarWidth: 340,
explorerWidth: 400,
explorerSortOption: "name",
explorerShowHiddenFiles: true,
treeRailWidth: 320,
},
}),
);
});
}
test.describe("explorer surface after upgrading from the docked sidebar", () => {
test("a persisted fileExplorerOpen flag no longer renders a second explorer", async ({
page,
}) => {
const workspace = await seedWorkspace({ repoPrefix: "explorer-surface-upgrade-" });
try {
await seedRetiredSidebarState(page);
await gotoWorkspace(page, workspace.workspaceId);
await waitForWorkspaceTabsVisible(page);
await test.step("no docked sidebar, on load or after toggling", async () => {
await expect(dockedSidebar(page)).toHaveCount(0);
const toggle = visible(page, "workspace-explorer-toggle").first();
await toggle.click();
await expect(explorerTabRow(page)).toHaveCount(1, { timeout: 15_000 });
await expect(dockedSidebar(page)).toHaveCount(0);
await toggle.click();
await expect(explorerTabRow(page)).toHaveCount(0, { timeout: 15_000 });
await expect(dockedSidebar(page)).toHaveCount(0);
});
await test.step("the retired keys are migrated out of storage", async () => {
const persisted = await readPersistedPanelState(page);
expect(persisted).not.toBeNull();
expect(persisted).not.toContain("fileExplorerOpen");
expect(persisted).not.toContain("explorerWidth");
// Everything else the user had must survive the migration.
expect(persisted).toContain('"sidebarWidth":340');
});
} finally {
await workspace.cleanup();
}
});
test("toggle-both-sidebars closes and restores the agent list and the explorer", async ({
page,
}) => {
const workspace = await seedWorkspace({ repoPrefix: "explorer-surface-both-" });
const modifier = process.platform === "darwin" ? "Meta" : "Control";
try {
await gotoWorkspace(page, workspace.workspaceId);
await waitForWorkspaceTabsVisible(page);
const agentList = visible(page, "sidebar-sessions");
const toggle = visible(page, "workspace-explorer-toggle").first();
await test.step("open the explorer so both sides are showing", async () => {
await toggle.click();
await expect(explorerTabRow(page)).toHaveCount(1, { timeout: 15_000 });
await expect(agentList).toHaveCount(1);
});
await test.step("the shortcut collapses both", async () => {
await page.keyboard.press(`${modifier}+.`);
await expect(explorerTabRow(page)).toHaveCount(0, { timeout: 15_000 });
await expect(agentList).toHaveCount(0);
});
await test.step("the shortcut restores both", async () => {
await page.keyboard.press(`${modifier}+.`);
await expect(agentList).toHaveCount(1, { timeout: 15_000 });
await expect(explorerTabRow(page)).toHaveCount(1, { timeout: 15_000 });
});
} finally {
await workspace.cleanup();
}
});
});