git.iko.soy/rowtype-yoga/purescript-yoga-react-native
on v0.2.0 help sign in
479 lines 14 kB history raw
1import { describe, it, expect } from "vitest";
2import React from "react";
4// Serialize a React element tree to a plain object for snapshot comparison.
5// Our mocks return string tags (e.g. "MacOSCheckbox") which react-test-renderer
6// can't render, so we walk the element tree directly.
7function toTree(element) {
8 if (element == null) return null;
9 if (typeof element === "string") return element;
10 if (typeof element === "number") return String(element);
11 if (Array.isArray(element)) return element.map(toTree);
12 const { type, props } = element;
13 const { children, ...rest } = props || {};
14 const name =
15 typeof type === "string"
16 ? type
17 : type?.displayName || type?.name || "Unknown";
18 const node = { type: name };
19 // Strip functions from props (event handlers) for stable snapshots
20 const cleanProps = {};
21 let hasProps = false;
22 for (const [k, v] of Object.entries(rest)) {
23 if (typeof v !== "function") {
24 cleanProps[k] = v;
25 hasProps = true;
26 }
27 }
28 if (hasProps) node.props = cleanProps;
29 if (children != null) {
30 const kids = Array.isArray(children)
31 ? children.map(toTree)
32 : [toTree(children)];
33 const filtered = kids.filter((k) => k != null);
34 if (filtered.length > 0) node.children = filtered;
35 }
36 return node;
39const render = (element) => ({
40 toJSON: () => toTree(element),
41});
43import {
44 createNativeElementNoKidsImpl as el,
45 createNativeElementImpl as elK,
46} from "../src/Yoga/React/Native/Internal.js";
48// Leaf components (no children)
49import { buttonImpl } from "../src/Yoga/React/Native/MacOS/Button.js";
50import { checkboxImpl } from "../src/Yoga/React/Native/MacOS/Checkbox.js";
51import { radioButtonImpl } from "../src/Yoga/React/Native/MacOS/RadioButton.js";
52import { searchFieldImpl } from "../src/Yoga/React/Native/MacOS/SearchField.js";
53import { tokenFieldImpl } from "../src/Yoga/React/Native/MacOS/TokenField.js";
54import { separatorImpl } from "../src/Yoga/React/Native/MacOS/Separator.js";
55import { helpButtonImpl } from "../src/Yoga/React/Native/MacOS/HelpButton.js";
56import { pathControlImpl } from "../src/Yoga/React/Native/MacOS/PathControl.js";
57import { textFieldImpl } from "../src/Yoga/React/Native/MacOS/TextField.js";
58import { sliderImpl } from "../src/Yoga/React/Native/MacOS/Slider.js";
59import { switchImpl } from "../src/Yoga/React/Native/MacOS/Switch.js";
60import { progressImpl } from "../src/Yoga/React/Native/MacOS/Progress.js";
61import { datePickerImpl } from "../src/Yoga/React/Native/MacOS/DatePicker.js";
62import { colorWellImpl } from "../src/Yoga/React/Native/MacOS/ColorWell.js";
63import { popUpImpl } from "../src/Yoga/React/Native/MacOS/PopUp.js";
64import { textEditorImpl } from "../src/Yoga/React/Native/MacOS/TextEditor.js";
65import { levelIndicatorImpl } from "../src/Yoga/React/Native/MacOS/LevelIndicator.js";
66import { segmentedImpl } from "../src/Yoga/React/Native/MacOS/Segmented.js";
67import { stepperImpl } from "../src/Yoga/React/Native/MacOS/Stepper.js";
68import { comboBoxImpl } from "../src/Yoga/React/Native/MacOS/ComboBox.js";
69import { imageImpl } from "../src/Yoga/React/Native/MacOS/Image.js";
70import { animatedImageImpl } from "../src/Yoga/React/Native/MacOS/AnimatedImage.js";
71import { videoPlayerImpl } from "../src/Yoga/React/Native/MacOS/VideoPlayer.js";
72import { tableViewImpl } from "../src/Yoga/React/Native/MacOS/TableView.js";
73import { outlineViewImpl } from "../src/Yoga/React/Native/MacOS/OutlineView.js";
74import { mapViewImpl } from "../src/Yoga/React/Native/MacOS/MapView.js";
75import { pdfViewImpl } from "../src/Yoga/React/Native/MacOS/PDFView.js";
76import { cameraViewImpl } from "../src/Yoga/React/Native/MacOS/CameraView.js";
78// Container components (with children)
79import { sheetImpl } from "../src/Yoga/React/Native/MacOS/Sheet.js";
80import { boxImpl } from "../src/Yoga/React/Native/MacOS/Box.js";
81import { popoverImpl } from "../src/Yoga/React/Native/MacOS/Popover.js";
82import { nativeScrollViewImpl } from "../src/Yoga/React/Native/MacOS/ScrollView.js";
83import { visualEffectImpl } from "../src/Yoga/React/Native/MacOS/VisualEffect.js";
84import { contextMenuImpl } from "../src/Yoga/React/Native/MacOS/ContextMenu.js";
85import { splitViewImpl } from "../src/Yoga/React/Native/MacOS/SplitView.js";
86import { tabViewImpl } from "../src/Yoga/React/Native/MacOS/TabView.js";
87import { patternBackgroundImpl } from "../src/Yoga/React/Native/MacOS/PatternBackground.js";
89const noop = () => {};
91describe("macOS component snapshots", () => {
92 // -- Controls --
94 it("Button", () => {
95 const { toJSON } = render(
96 el(buttonImpl, {
97 title: "Click Me",
98 bezelStyle: "rounded",
99 onPress: noop,
100 }),
101 );
102 expect(toJSON()).toMatchSnapshot();
103 });
105 it("Checkbox checked", () => {
106 const { toJSON } = render(
107 el(checkboxImpl, {
108 checked: true,
109 title: "Accept terms",
110 enabled: true,
111 }),
112 );
113 expect(toJSON()).toMatchSnapshot();
114 });
116 it("Checkbox unchecked", () => {
117 const { toJSON } = render(
118 el(checkboxImpl, {
119 checked: false,
120 title: "Accept terms",
121 enabled: true,
122 }),
123 );
124 expect(toJSON()).toMatchSnapshot();
125 });
127 it("RadioButton selected", () => {
128 const { toJSON } = render(
129 el(radioButtonImpl, {
130 selected: true,
131 title: "Option A",
132 enabled: true,
133 }),
134 );
135 expect(toJSON()).toMatchSnapshot();
136 });
138 it("RadioButton unselected", () => {
139 const { toJSON } = render(
140 el(radioButtonImpl, {
141 selected: false,
142 title: "Option B",
143 enabled: true,
144 }),
145 );
146 expect(toJSON()).toMatchSnapshot();
147 });
149 it("Switch on", () => {
150 const { toJSON } = render(el(switchImpl, { value: true, onChange: noop }));
151 expect(toJSON()).toMatchSnapshot();
152 });
154 it("Slider", () => {
155 const { toJSON } = render(
156 el(sliderImpl, {
157 value: 50,
158 minimumValue: 0,
159 maximumValue: 100,
160 onValueChange: noop,
161 }),
162 );
163 expect(toJSON()).toMatchSnapshot();
164 });
166 it("Stepper", () => {
167 const { toJSON } = render(
168 el(stepperImpl, {
169 value: 5,
170 minValue: 0,
171 maxValue: 10,
172 onValueChange: noop,
173 }),
174 );
175 expect(toJSON()).toMatchSnapshot();
176 });
178 it("Segmented", () => {
179 const { toJSON } = render(
180 el(segmentedImpl, {
181 values: ["A", "B", "C"],
182 selectedIndex: 1,
183 onChange: noop,
184 }),
185 );
186 expect(toJSON()).toMatchSnapshot();
187 });
189 it("HelpButton", () => {
190 const { toJSON } = render(el(helpButtonImpl, { onPress: noop }));
191 expect(toJSON()).toMatchSnapshot();
192 });
194 it("PopUp", () => {
195 const { toJSON } = render(
196 el(popUpImpl, {
197 items: ["One", "Two"],
198 selectedIndex: 0,
199 onChange: noop,
200 }),
201 );
202 expect(toJSON()).toMatchSnapshot();
203 });
205 it("ComboBox", () => {
206 const { toJSON } = render(
207 el(comboBoxImpl, {
208 items: ["foo", "bar"],
209 text: "foo",
210 onChangeText: noop,
211 }),
212 );
213 expect(toJSON()).toMatchSnapshot();
214 });
216 // -- Text input --
218 it("TextField", () => {
219 const { toJSON } = render(
220 el(textFieldImpl, {
221 text: "Hello",
222 placeholder: "Type here",
223 onChangeText: noop,
224 }),
225 );
226 expect(toJSON()).toMatchSnapshot();
227 });
229 it("SearchField", () => {
230 const { toJSON } = render(
231 el(searchFieldImpl, {
232 text: "",
233 placeholder: "Search...",
234 onChangeText: noop,
235 }),
236 );
237 expect(toJSON()).toMatchSnapshot();
238 });
240 it("TokenField", () => {
241 const { toJSON } = render(
242 el(tokenFieldImpl, {
243 tokens: ["swift", "purescript"],
244 placeholder: "Add tag",
245 }),
246 );
247 expect(toJSON()).toMatchSnapshot();
248 });
250 it("TextEditor", () => {
251 const { toJSON } = render(
252 el(textEditorImpl, { text: "Some long text", onChangeText: noop }),
253 );
254 expect(toJSON()).toMatchSnapshot();
255 });
257 // -- Indicators --
259 it("Progress determinate", () => {
260 const { toJSON } = render(el(progressImpl, { value: 0.6, style: "bar" }));
261 expect(toJSON()).toMatchSnapshot();
262 });
264 it("LevelIndicator", () => {
265 const { toJSON } = render(
266 el(levelIndicatorImpl, { value: 3, minValue: 0, maxValue: 5 }),
267 );
268 expect(toJSON()).toMatchSnapshot();
269 });
271 // -- Pickers --
273 it("DatePicker", () => {
274 const { toJSON } = render(
275 el(datePickerImpl, { date: "2025-01-15", onChange: noop }),
276 );
277 expect(toJSON()).toMatchSnapshot();
278 });
280 it("ColorWell", () => {
281 const { toJSON } = render(
282 el(colorWellImpl, { color: "#ff0000", onChange: noop }),
283 );
284 expect(toJSON()).toMatchSnapshot();
285 });
287 it("PathControl", () => {
288 const { toJSON } = render(
289 el(pathControlImpl, {
290 url: "/Users/test/file.txt",
291 pathStyle: "standard",
292 }),
293 );
294 expect(toJSON()).toMatchSnapshot();
295 });
297 // -- Media --
299 it("Image", () => {
300 const { toJSON } = render(
301 el(imageImpl, { source: "test.png", resizeMode: "cover" }),
302 );
303 expect(toJSON()).toMatchSnapshot();
304 });
306 it("AnimatedImage", () => {
307 const { toJSON } = render(el(animatedImageImpl, { source: "anim.gif" }));
308 expect(toJSON()).toMatchSnapshot();
309 });
311 it("VideoPlayer", () => {
312 const { toJSON } = render(
313 el(videoPlayerImpl, { source: "video.mp4", autoPlay: false }),
314 );
315 expect(toJSON()).toMatchSnapshot();
316 });
318 // -- Layout --
320 it("Separator horizontal", () => {
321 const { toJSON } = render(el(separatorImpl, { vertical: false }));
322 expect(toJSON()).toMatchSnapshot();
323 });
325 it("Separator vertical", () => {
326 const { toJSON } = render(el(separatorImpl, { vertical: true }));
327 expect(toJSON()).toMatchSnapshot();
328 });
330 // -- Data --
332 it("TableView", () => {
333 const { toJSON } = render(
334 el(tableViewImpl, {
335 columns: [
336 { title: "Name", width: 100 },
337 { title: "Age", width: 50 },
338 ],
339 rows: [
340 ["Alice", "30"],
341 ["Bob", "25"],
342 ],
343 headerVisible: true,
344 alternatingRows: true,
345 }),
346 );
347 expect(toJSON()).toMatchSnapshot();
348 });
350 it("OutlineView", () => {
351 const { toJSON } = render(
352 el(outlineViewImpl, {
353 items: [
354 {
355 id: "1",
356 title: "Root",
357 children: [
358 { id: "1.1", title: "Child A", sfSymbol: "doc" },
359 { id: "1.2", title: "Child B" },
360 ],
361 },
362 ],
363 headerVisible: false,
364 onSelectItem: noop,
365 }),
366 );
367 expect(toJSON()).toMatchSnapshot();
368 });
370 it("MapView", () => {
371 const { toJSON } = render(
372 el(mapViewImpl, {
373 latitude: 37.7749,
374 longitude: -122.4194,
375 latitudeDelta: 0.05,
376 longitudeDelta: 0.05,
377 mapType: "standard",
378 showsUserLocation: false,
379 annotations: [{ latitude: 37.7749, longitude: -122.4194, title: "SF" }],
380 onRegionChange: noop,
381 onSelectAnnotation: noop,
382 }),
383 );
384 expect(toJSON()).toMatchSnapshot();
385 });
387 it("PDFView", () => {
388 const { toJSON } = render(
389 el(pdfViewImpl, {
390 source: "/tmp/test.pdf",
391 autoScales: true,
392 displayMode: "singlePageContinuous",
393 onPageChange: noop,
394 }),
395 );
396 expect(toJSON()).toMatchSnapshot();
397 });
399 // -- Containers --
401 it("Sheet with child", () => {
402 const { toJSON } = render(
403 elK(sheetImpl, { visible: true, onDismiss: noop }, [
404 el("View", { key: "1" }),
405 ]),
406 );
407 expect(toJSON()).toMatchSnapshot();
408 });
410 it("Box with child", () => {
411 const { toJSON } = render(
412 elK(boxImpl, { title: "Group" }, [el("View", { key: "1" })]),
413 );
414 expect(toJSON()).toMatchSnapshot();
415 });
417 it("Popover with child", () => {
418 const { toJSON } = render(
419 elK(popoverImpl, { visible: true }, [el("View", { key: "1" })]),
420 );
421 expect(toJSON()).toMatchSnapshot();
422 });
424 it("ScrollView with child", () => {
425 const { toJSON } = render(
426 elK(nativeScrollViewImpl, {}, [el("View", { key: "1" })]),
427 );
428 expect(toJSON()).toMatchSnapshot();
429 });
431 it("VisualEffect with child", () => {
432 const { toJSON } = render(
433 elK(visualEffectImpl, { material: "sidebar" }, [
434 el("View", { key: "1" }),
435 ]),
436 );
437 expect(toJSON()).toMatchSnapshot();
438 });
440 it("ContextMenu with child", () => {
441 const { toJSON } = render(
442 elK(contextMenuImpl, { menuItems: [{ title: "Copy" }] }, [
443 el("View", { key: "1" }),
444 ]),
445 );
446 expect(toJSON()).toMatchSnapshot();
447 });
449 it("SplitView with children", () => {
450 const { toJSON } = render(
451 elK(splitViewImpl, { vertical: false }, [
452 el("View", { key: "1" }),
453 el("View", { key: "2" }),
454 ]),
455 );
456 expect(toJSON()).toMatchSnapshot();
457 });
459 it("TabView with children", () => {
460 const { toJSON } = render(
461 elK(tabViewImpl, { selectedIndex: 0 }, [el("View", { key: "1" })]),
462 );
463 expect(toJSON()).toMatchSnapshot();
464 });
466 it("PatternBackground with child", () => {
467 const { toJSON } = render(
468 elK(patternBackgroundImpl, { pattern: "dots" }, [
469 el("View", { key: "1" }),
470 ]),
471 );
472 expect(toJSON()).toMatchSnapshot();
473 });
475 it("CameraView", () => {
476 const { toJSON } = render(el(cameraViewImpl, { active: true }));
477 expect(toJSON()).toMatchSnapshot();
478 });
479});