mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
fix(app): avoid pane rerenders while resizing Explorer (#7284)
Keep drag widths out of the shared stylesheet and skip unchanged panes during width previews. Preserve the existing resize state and persistence behavior. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Ivan Malison <1246619+colonelpanic8@users.noreply.github.com>
This commit is contained in:
co-authored by
Ivan Malison
parent
a65478d0bf
commit
095dd0316e
@@ -0,0 +1,118 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "../support/fixtures";
|
||||
import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions";
|
||||
|
||||
interface DragMeasurement {
|
||||
observer: MutationObserver;
|
||||
stylesheetMutations: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__PASEO_EXPLORER_DRAG_MEASUREMENT__?: DragMeasurement;
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForResizePaint(page: Page): Promise<void> {
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function beginResizeMeasurement(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
const stylesheet = document.getElementById("unistyles-web");
|
||||
if (!stylesheet) throw new Error("Shared stylesheet is missing");
|
||||
globalThis.__PASEO_RESET_RENDER_PROFILE__!();
|
||||
const measurement: DragMeasurement = {
|
||||
stylesheetMutations: 0,
|
||||
observer: new MutationObserver((records) => {
|
||||
measurement.stylesheetMutations += records.length;
|
||||
}),
|
||||
};
|
||||
measurement.observer.observe(stylesheet, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
window.__PASEO_EXPLORER_DRAG_MEASUREMENT__ = measurement;
|
||||
});
|
||||
}
|
||||
|
||||
async function previewExplorerResize(page: Page, distance: number) {
|
||||
const handle = page
|
||||
.getByTestId("workspace-explorer-sidebar-resize-handle")
|
||||
.getByRole("separator");
|
||||
const explorer = page.getByTestId("workspace-explorer-sidebar");
|
||||
const bounds = await handle.boundingBox();
|
||||
if (!bounds) throw new Error("Explorer resize handle is missing");
|
||||
const before = (await explorer.boundingBox())!.width;
|
||||
const x = bounds.x + bounds.width / 2;
|
||||
const y = bounds.y + 100;
|
||||
await page.mouse.move(x, y);
|
||||
await page.mouse.down();
|
||||
await waitForResizePaint(page);
|
||||
await beginResizeMeasurement(page);
|
||||
for (let move = 1; move <= 20; move++) {
|
||||
await page.mouse.move(x + (distance * move) / 20, y);
|
||||
await waitForResizePaint(page);
|
||||
}
|
||||
const during = (await explorer.boundingBox())!.width;
|
||||
const measurement = await page.evaluate(() => {
|
||||
const state = window.__PASEO_EXPLORER_DRAG_MEASUREMENT__!;
|
||||
state.observer.disconnect();
|
||||
return {
|
||||
stylesheetMutations: state.stylesheetMutations,
|
||||
paneRenders: Object.entries(globalThis.__PASEO_RENDER_PROFILE_REASONS__ ?? {})
|
||||
.filter(([id]) => id.startsWith("SplitPaneView:"))
|
||||
.reduce((sum, [, reasons]) => sum + (reasons.render ?? 0), 0),
|
||||
};
|
||||
});
|
||||
await page.mouse.up();
|
||||
await waitForResizePaint(page);
|
||||
const after = (await explorer.boundingBox())!.width;
|
||||
expect(during).toBeCloseTo(before - distance, 0);
|
||||
expect(after).toBeCloseTo(during, 0);
|
||||
return { ...measurement, width: after };
|
||||
}
|
||||
|
||||
async function expectExplorerWidthSurvivesReload(page: Page, width: number): Promise<void> {
|
||||
await page.reload();
|
||||
const explorer = page.getByTestId("workspace-explorer-sidebar");
|
||||
await expect(explorer).toBeVisible();
|
||||
await expect.poll(async () => (await explorer.boundingBox())?.width).toBeCloseTo(width, 0);
|
||||
}
|
||||
|
||||
test("Explorer drag previews without rerendering panes or rewriting the shared stylesheet", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1600, height: 1000 });
|
||||
await page.addInitScript(() => {
|
||||
globalThis.__PASEO_RENDER_PROFILE_ENABLED__ = true;
|
||||
});
|
||||
const workspace = await withWorkspace();
|
||||
await workspace.navigateTo();
|
||||
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
|
||||
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
|
||||
await page.getByTestId("workspace-explorer-toggle").first().click();
|
||||
await expect(page.getByTestId("workspace-explorer-sidebar")).toBeVisible();
|
||||
// Verify that direct pane renders are being counted before resetting them.
|
||||
expect(
|
||||
await page.evaluate(() =>
|
||||
Object.keys(globalThis.__PASEO_RENDER_PROFILE_REASONS__ ?? {}).some((id) =>
|
||||
id.startsWith("SplitPaneView:"),
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
const grown = await previewExplorerResize(page, -100);
|
||||
const shrunk = await previewExplorerResize(page, 50);
|
||||
expect(grown.stylesheetMutations).toBe(0);
|
||||
expect(grown.paneRenders).toBe(0);
|
||||
expect(shrunk.stylesheetMutations).toBe(0);
|
||||
expect(shrunk.paneRenders).toBe(0);
|
||||
await expectExplorerWidthSurvivesReload(page, shrunk.width);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Fragment,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -88,7 +89,8 @@ import {
|
||||
type WorkspaceLayout,
|
||||
} from "@/stores/workspace-layout-store";
|
||||
import type { WorkspaceTab } from "@/workspace-tabs/model";
|
||||
import { RenderProfile } from "@/utils/render-profiler";
|
||||
import { recordRenderProfileReasons, RenderProfile } from "@/utils/render-profiler";
|
||||
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { panelTargetSupportsHost } from "@/plugins/workspace-panels/locations";
|
||||
|
||||
@@ -435,7 +437,7 @@ export function SplitContainer({
|
||||
: inheritedWindowChromeCorners;
|
||||
const mainColumnStyle = styles.mainColumn;
|
||||
const explorerSidebarDockStyle = useMemo(
|
||||
() => [styles.explorerSidebarDock, { width: explorerSidebarWidth }],
|
||||
() => [styles.explorerSidebarDock, inlineUnistylesStyle({ width: explorerSidebarWidth })],
|
||||
[explorerSidebarWidth],
|
||||
);
|
||||
const handleWorkspaceShellLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
@@ -1121,7 +1123,7 @@ function SplitNodeView({
|
||||
);
|
||||
}
|
||||
|
||||
function SplitPaneView({
|
||||
const SplitPaneView = memo(function SplitPaneView({
|
||||
pane,
|
||||
uiTabs,
|
||||
isFocused,
|
||||
@@ -1158,6 +1160,7 @@ function SplitPaneView({
|
||||
focusModeEnabled,
|
||||
onExitFocusMode,
|
||||
}: SplitPaneViewProps) {
|
||||
recordRenderProfileReasons(`SplitPaneView:${pane.id}`, ["render"]);
|
||||
const paneRef = useRef<View | null>(null);
|
||||
const stableOnFocusPane = useStableEvent(onFocusPane);
|
||||
const paneState = useMemo(
|
||||
@@ -1312,7 +1315,7 @@ function SplitPaneView({
|
||||
</View>
|
||||
</RenderProfile>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function collectPanesById(node: SplitNode): Map<string, SplitPane> {
|
||||
const next = new Map<string, SplitPane>();
|
||||
|
||||
Reference in New Issue
Block a user