Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions apps/web/src/browser/BrowserSurfaceSlot.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { act } from "react";
import { create, type ReactTestRenderer } from "react-test-renderer";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";

import { BrowserSurfaceSlot } from "./BrowserSurfaceSlot";
import { useBrowserSurfaceStore } from "./browserSurfaceStore";

interface MeasuredElement {
getBoundingClientRect: () => { x: number; y: number; width: number; height: number };
}

class LayoutResizeObserver {
static observers = new Set<LayoutResizeObserver>();
readonly sizes = new Map<MeasuredElement, string>();

constructor(readonly callback: () => void) {
LayoutResizeObserver.observers.add(this);
}

observe(element: MeasuredElement) {
this.sizes.set(element, this.size(element));
}

disconnect() {
LayoutResizeObserver.observers.delete(this);
}

size(element: MeasuredElement) {
const { width, height } = element.getBoundingClientRect();
return `${width}:${height}`;
}

static deliverResizes() {
for (const observer of this.observers) {
let resized = false;
for (const [element, previous] of observer.sizes) {
const next = observer.size(element);
if (next === previous) continue;
observer.sizes.set(element, next);
resized = true;
}
if (resized) observer.callback();
}
}
}

let renderer: ReactTestRenderer | undefined;

beforeEach(() => {
useBrowserSurfaceStore.setState({ activityByTabId: {}, byTabId: {} });
vi.stubGlobal("window", new EventTarget());
vi.stubGlobal("ResizeObserver", LayoutResizeObserver);
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
vi.spyOn(console, "error").mockImplementation(() => undefined);
});

afterEach(async () => {
await act(() => renderer?.unmount());
renderer = undefined;
LayoutResizeObserver.observers.clear();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

describe("BrowserSurfaceSlot", () => {
it("follows an opening panel when the full-width slot moves without resizing", async () => {
const tabId = "reopened-browser";
let panelWidth = 0;
const panel = {
getBoundingClientRect: () => ({
x: 1_280 - panelWidth,
y: 0,
width: panelWidth,
height: 800,
}),
};
const slot = {
closest: () => panel,
getBoundingClientRect: () => ({ x: 1_280 - panelWidth, y: 92, width: 539, height: 708 }),
};

await act(() => {
renderer = create(<BrowserSurfaceSlot tabId={tabId} visible />, {
createNodeMock: () => slot,
});
});
expect(useBrowserSurfaceStore.getState().byTabId[tabId]?.rect?.x).toBe(1_280);

for (const width of [180, 360, 540]) {
await act(() => {
panelWidth = width;
LayoutResizeObserver.deliverResizes();
});
expect(useBrowserSurfaceStore.getState().byTabId[tabId]).toMatchObject({
visible: true,
rect: { x: 1_280 - width, y: 92, width: 539, height: 708 },
});
}

await act(() => renderer?.unmount());
renderer = undefined;
panelWidth = 0;
LayoutResizeObserver.deliverResizes();
expect(useBrowserSurfaceStore.getState().byTabId[tabId]).toMatchObject({
visible: false,
rect: { x: 740, y: 92, width: 539, height: 708 },
});
expect(LayoutResizeObserver.observers.size).toBe(0);
});
});
4 changes: 4 additions & 0 deletions apps/web/src/browser/BrowserSurfaceSlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export function BrowserSurfaceSlot(props: {
update();
const observer = new ResizeObserver(update);
observer.observe(element);
// Inline panels animate their outer width while keeping the content at
// full width. The slot moves without resizing, so measure on shell resizes too.
const panel = element.closest('[data-preview-panel-mode="inline"]');
if (panel) observer.observe(panel);
window.addEventListener("resize", update);
window.addEventListener("scroll", update, true);
return () => {
Expand Down
Loading