one
Version:
One is a new React Framework that makes Vite serve both native and web.
466 lines (465 loc) • 12.5 kB
JavaScript
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