UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

466 lines (465 loc) 12.5 kB
import { describe, expect, it, vi } from "vitest"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { OverlayHost, resolveOverlayRender, WebStackView } from "../WebStackView.mjs"; import { StackRenderProvider } from "../ScreenRenderContext.mjs"; function makeRoute(name) { return { key: `${name}-key`, name, params: void 0 }; } function makeState(names, index) { return { key: "stack-1", index, routeNames: names, routes: names.map(makeRoute), type: "stack", stale: false, preloadedRoutes: [] }; } function makeDescriptors(perRoute) { const out = {}; for (const name of Object.keys(perRoute)) { out[`${name}-key`] = { options: perRoute[name].options, render: () => perRoute[name].content ?? null, navigation: {} }; } return out; } describe("resolveOverlayRender", () => { const A = () => null; const B = () => null; it("prefers per-route render over context", () => { const result = resolveOverlayRender({ presentation: "formSheet", render: { web: A } }, { web: B }); expect(result).toBe(A); }); it("falls back to context render when route has none", () => { const result = resolveOverlayRender({ presentation: "formSheet" }, { web: B }); expect(result).toBe(B); }); it("returns undefined when nothing is configured", () => { expect(resolveOverlayRender({ presentation: "formSheet" }, void 0)).toBeUndefined(); expect(resolveOverlayRender({ presentation: "formSheet" }, { ios: A })).toBeUndefined(); }); it("does not pick from ios/android slots on web", () => { expect(resolveOverlayRender({ presentation: "formSheet", render: { ios: A } }, { web: B })).toBe(B); }); }); describe("OverlayHost", () => { it("invokes the render component with route props for overlay presentations", () => { const captured = []; const Render = vi.fn(props => { captured.push(props); return createElement("div", { "data-modal": props.routeKey }, props.children); }); const descriptors = makeDescriptors({ filter: { options: { presentation: "formSheet", sheetAllowedDetents: [0.5, 1], sheetGrabberVisible: true }, content: createElement("span", null, "filter-body") } }); const markup = renderToStaticMarkup(createElement(OverlayHost, { route: makeRoute("filter"), descriptor: descriptors["filter-key"], contextRender: { web: Render }, onDismiss: () => {} })); expect(Render).toHaveBeenCalledTimes(1); expect(captured[0]).toMatchObject({ routeKey: "filter-key", presentation: "formSheet", sheetAllowedDetents: [0.5, 1], sheetGrabberVisible: true, dismissible: true }); expect(typeof captured[0].dismiss).toBe("function"); expect(markup).toContain('data-modal="filter-key"'); expect(markup).toContain("filter-body"); }); it("falls back to inline rendering when no render is configured", () => { const descriptors = makeDescriptors({ filter: { options: { presentation: "formSheet" }, content: createElement("span", null, "inline-content") } }); const markup = renderToStaticMarkup(createElement(OverlayHost, { route: makeRoute("filter"), descriptor: descriptors["filter-key"], contextRender: void 0, onDismiss: () => {} })); expect(markup).toContain("inline-content"); }); it("skips render when presentation is not an overlay", () => { const Render = vi.fn(() => null); const descriptors = makeDescriptors({ home: { options: { presentation: "card" }, content: createElement("span", null, "card-content") } }); const markup = renderToStaticMarkup(createElement(OverlayHost, { route: makeRoute("home"), descriptor: descriptors["home-key"], contextRender: { web: Render }, onDismiss: () => {} })); expect(Render).not.toHaveBeenCalled(); expect(markup).toContain("card-content"); }); it("per-route render overrides context render", () => { const ContextRender = vi.fn(() => null); const PerRoute = vi.fn(() => createElement("em", null, "per-route")); const descriptors = makeDescriptors({ sheet: { options: { presentation: "formSheet", render: { web: PerRoute } }, content: createElement("span", null, "body") } }); const markup = renderToStaticMarkup(createElement(OverlayHost, { route: makeRoute("sheet"), descriptor: descriptors["sheet-key"], contextRender: { web: ContextRender }, onDismiss: () => {} })); expect(PerRoute).toHaveBeenCalledTimes(1); expect(ContextRender).not.toHaveBeenCalled(); expect(markup).toContain("per-route"); }); it("dismiss callback wraps the supplied onDismiss", () => { const onDismiss = vi.fn(); let capturedDismiss; const Render = props => { capturedDismiss = props.dismiss; return null; }; const descriptors = makeDescriptors({ sheet: { options: { presentation: "formSheet" } } }); renderToStaticMarkup(createElement(OverlayHost, { route: makeRoute("sheet"), descriptor: descriptors["sheet-key"], contextRender: { web: Render }, onDismiss })); expect(typeof capturedDismiss).toBe("function"); capturedDismiss(); expect(onDismiss).toHaveBeenCalledTimes(1); }); it("respects gestureEnabled: false as dismissible: false", () => { let captured; const Render = props => { captured = props; return null; }; const descriptors = makeDescriptors({ sheet: { options: { presentation: "formSheet", gestureEnabled: false } } }); renderToStaticMarkup(createElement(OverlayHost, { route: makeRoute("sheet"), descriptor: descriptors["sheet-key"], contextRender: { web: Render }, onDismiss: () => {} })); expect(captured.dismissible).toBe(false); }); }); describe("WebStackView overlay dispatch", () => { it("renders each overlay route via the configured render", () => { const calls = []; const Render = props => { calls.push(props.routeKey); return createElement("div", { "data-route": props.routeKey }); }; const state = makeState(["home", "filter", "help"], 2); const descriptors = makeDescriptors({ home: { options: { presentation: "card" } }, filter: { options: { presentation: "formSheet" } }, help: { options: { presentation: "modal" } } }); const navigation = { dispatch: vi.fn() }; const markup = renderToStaticMarkup(createElement(StackRenderProvider, { value: { web: Render } }, createElement(WebStackView, { state, navigation, descriptors }))); expect(calls).toEqual(["filter-key", "help-key"]); expect(markup).toContain('data-route="filter-key"'); expect(markup).toContain('data-route="help-key"'); }); it("leaves overlay routes in the underlying NativeStackView when no render is configured", () => { const state = makeState(["home", "filter"], 1); const descriptors = makeDescriptors({ home: { options: { presentation: "card" } }, filter: { options: { presentation: "formSheet" }, content: createElement("span", null, "should-not-render-as-overlay") } }); const navigation = { dispatch: vi.fn() }; const markup = renderToStaticMarkup(createElement(WebStackView, { state, navigation, descriptors })); expect(markup).not.toContain("should-not-render-as-overlay"); }); it("passes open: true to the regular overlay render", () => { let captured; const Render = props => { captured = props; return null; }; const state = makeState(["home", "sheet"], 1); const descriptors = makeDescriptors({ home: { options: { presentation: "card" } }, sheet: { options: { presentation: "formSheet" } } }); const navigation = { dispatch: vi.fn() }; renderToStaticMarkup(createElement(StackRenderProvider, { value: { web: Render } }, createElement(WebStackView, { state, navigation, descriptors }))); expect(captured.open).toBe(true); expect(captured.routeName).toBe("sheet"); }); it("keepMounted: keeps rendering the route via the persistent slot after the route is popped", () => { const calls = []; let mountTrackerCalls = 0; const Render = props => { calls.push({ name: props.routeName, open: props.open }); return createElement("div", { "data-route": props.routeName, "data-open": String(props.open) }, props.children); }; const MountTracker = () => { mountTrackerCalls++; return createElement("span", null, `mounted-${mountTrackerCalls}`); }; const initialState = makeState(["home", "settings"], 1); const settingsDescriptor = { options: { presentation: "formSheet", keepMounted: true }, render: () => createElement(MountTracker), navigation: {} }; const descriptors = { "home-key": { options: { presentation: "card" }, render: () => null, navigation: {} }, "settings-key": settingsDescriptor }; const navigation = { dispatch: vi.fn() }; const tree = createElement(StackRenderProvider, { value: { web: Render } }, createElement(WebStackView, { state: initialState, navigation, descriptors })); const out1 = renderToStaticMarkup(tree); expect(calls.some(c => c.name === "settings" && c.open === true)).toBe(true); expect(out1).toContain('data-route="settings"'); expect(out1).toContain('data-open="true"'); expect(calls).toHaveLength(1); }); it("peels off only overlay routes with a render configured, leaving render-less overlays in the underlying view", () => { const PerRoute = vi.fn(() => createElement("div", { "data-route": "filter" })); const state = makeState(["home", "help", "filter"], 2); const descriptors = makeDescriptors({ home: { options: { presentation: "card" } }, help: { options: { presentation: "modal" } }, // overlay-presented, no render filter: { options: { presentation: "formSheet", render: { web: PerRoute } } } }); const navigation = { dispatch: vi.fn() }; renderToStaticMarkup(createElement(WebStackView, { state, navigation, descriptors })); expect(PerRoute).toHaveBeenCalledTimes(1); }); it("dispatches a pop action when an overlay calls dismiss", () => { let captured; const Render = props => { captured = props; return null; }; const state = makeState(["home", "filter"], 1); const descriptors = makeDescriptors({ home: { options: { presentation: "card" } }, filter: { options: { presentation: "formSheet" } } }); const dispatch = vi.fn(); const navigation = { dispatch, isFocused: () => true }; renderToStaticMarkup(createElement(StackRenderProvider, { value: { web: Render } }, createElement(WebStackView, { state, navigation, descriptors }))); captured.dismiss(); expect(dispatch).toHaveBeenCalledTimes(1); const action = dispatch.mock.calls[0][0]; expect(action.type).toBe("POP"); expect(action.source).toBe("filter-key"); expect(action.target).toBe("stack-1"); }); }); //# sourceMappingURL=WebStackView.test.mjs.map