11import React from "react";
12import { act, create } from "react-test-renderer";
13import { SafeAreaProvider } from "react-native-safe-area-context";
14import { Animated } from "react-native";
16vi.mock("@react-navigation/native", async () => {
17 const React = await import("react");
19 NavigationContainer: ({ children }) =>
20 React.createElement(React.Fragment, null, children),
24vi.mock("@react-navigation/native-stack", async () => {
25 const React = await import("react");
27 const Screen = ({ children: _children, ...props }) =>
28 React.createElement("NativeStackScreen", props);
29 const Navigator = ({ children, initialRouteName }) => {
30 const [route, setRoute] = React.useState(initialRouteName);
31 const screens = React.Children.toArray(children);
32 const activeScreen = screens.find((screen) => screen.props.name === route);
34 if (activeScreen == null) return null;
36 const renderedScreens = screens.map((screen) =>
37 React.cloneElement(screen, {
38 key: screen.props.name,
43 return React.createElement(
47 activeScreen.props.children({
50 goBack: () => setRoute("landing"),
57 createNativeStackNavigator: () => ({ Navigator, Screen }),
61vi.mock("react-native-screens", () => ({ enableScreens: vi.fn() }));
63import { iosDemo } from "../output/Demo.IOSBindings/index.js";
65const press = (renderer, accessibilityLabel) => {
66 const target = renderer.root.findByProps({ accessibilityLabel });
67 act(() => target.props.onPress());
70const visibleText = (renderer) =>
71 renderer.root.findAllByType("Text").map(({ children }) => children.join(""));
73const expectVisible = (renderer, text) => {
74 expect(visibleText(renderer)).toContain(text);
77const expectProfile = (renderer, position, name) => {
79 renderer.root.findByProps({ accessibilityLabel: `Profile ${position} of 4` }),
81 expectVisible(renderer, name);
84const profileCard = (renderer, name) =>
85 renderer.root.findByProps({
87 accessibilityRole: "adjustable",
88 accessibilityLabel: `Profile card for ${name}`,
91const interpolationNode = (source, config) => ({
95 interpolate(nextConfig) {
96 return interpolationNode(this, nextConfig);
100const interpolateAt = ({ _config: config }, input) => {
101 const { inputRange, outputRange } = config;
102 const clamped = Math.max(
104 Math.min(inputRange.at(-1), input),
106 const upperIndex = inputRange.findIndex(
107 (boundary) => boundary >= clamped,
109 if (upperIndex <= 0) return outputRange[0];
110 const lowerIndex = upperIndex - 1;
111 const distance = inputRange[upperIndex] - inputRange[lowerIndex];
113 distance === 0 ? 0 : (clamped - inputRange[lowerIndex]) / distance;
115 outputRange[lowerIndex] +
116 progress * (outputRange[upperIndex] - outputRange[lowerIndex])
120const profileImage = (renderer, name) =>
121 renderer.root.findByProps({
122 accessibilityLabel: {
123 Mara: "Portrait of Mara outdoors in warm evening light. Mara, 29",
124 Sol: "Portrait of Sol smiling against a soft city background. Sol, 31",
128const loadingIndicators = (image) => image.findAllByType("ActivityIndicator");
130const renderCatalogue = () => {
134 React.createElement(SafeAreaProvider, null, iosDemo({})),
140describe("Lumen dating demo integration", () => {
141 const hadActEnvironment = Object.hasOwn(
143 "IS_REACT_ACT_ENVIRONMENT",
145 const previousActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
150 globalThis.IS_REACT_ACT_ENVIRONMENT = true;
154 vi.spyOn(Animated.Value.prototype, "interpolate").mockImplementation(
155 function interpolate(config) {
156 return interpolationNode(this, config);
159 consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
163 if (renderer != null) {
164 act(() => renderer.unmount());
165 renderer = undefined;
168 const unexpectedErrors = consoleError.mock.calls.filter(
170 typeof message !== "string" ||
171 !message.includes("react-test-renderer is deprecated"),
173 expect(unexpectedErrors).toEqual([]);
174 vi.restoreAllMocks();
178 if (hadActEnvironment) {
179 globalThis.IS_REACT_ACT_ENVIRONMENT = previousActEnvironment;
181 delete globalThis.IS_REACT_ACT_ENVIRONMENT;
185 it("drives the behind card opacity, lift, and scale from shared swipe progress", () => {
186 renderer = renderCatalogue();
187 press(renderer, "Open Lumen dating demo examples");
189 const animatedViews = renderer.root.findAllByType("Animated.View");
190 const behindCard = animatedViews.find(
191 ({ props }) => props.style?.opacity?._config?.outputRange?.[0] === 0.58,
193 const scaledCard = animatedViews.find(
194 ({ props }) => props.style?.transform?.[0]?.scale?._config != null,
197 expect(behindCard).toBeDefined();
198 expect(scaledCard).toBeDefined();
200 const opacity = behindCard.props.style.opacity;
201 const translateY = behindCard.props.style.transform[0].translateY;
202 const scale = scaledCard.props.style.transform[0].scale;
203 const progress = opacity._source;
205 expect(progress._config).toEqual({
206 inputRange: [-110, 0, 110],
207 outputRange: [1, 0, 1],
208 extrapolate: "clamp",
210 expect(translateY._source).toBe(progress);
211 expect(scale._source).toBe(progress);
212 expect(opacity._config).toEqual({
214 outputRange: [0.58, 1],
215 extrapolate: "clamp",
217 expect(translateY._config).toEqual({
219 outputRange: [12, 0],
220 extrapolate: "clamp",
222 expect(scale._config).toEqual({
224 outputRange: [0.94, 1],
225 extrapolate: "clamp",
228 interpolateAt(opacity, 0),
229 interpolateAt(translateY, 0),
230 interpolateAt(scale, 0),
231 ]).toEqual([0.58, 12, 0.94]);
233 interpolateAt(opacity, 1),
234 interpolateAt(translateY, 1),
235 interpolateAt(scale, 1),
236 ]).toEqual([1, 0, 1]);
239 it("shows image loading feedback through completion and promotes a preloaded profile without a flash", () => {
240 renderer = renderCatalogue();
241 press(renderer, "Open Lumen dating demo examples");
242 const loadingMara = profileImage(renderer, "Mara");
243 expect(loadingIndicators(loadingMara)).toHaveLength(1);
245 act(() => loadingMara.props.onError({}));
246 expect(loadingIndicators(profileImage(renderer, "Mara"))).toHaveLength(0);
247 const preloadedSol = profileImage(renderer, "Sol");
248 expect(loadingIndicators(preloadedSol)).toHaveLength(1);
250 act(() => preloadedSol.props.onLoad({}));
251 expect(loadingIndicators(profileImage(renderer, "Sol"))).toHaveLength(0);
253 const maraCard = profileCard(renderer, "Mara");
254 act(() => maraCard.props.onResponderRelease({}, { dx: -110, vx: 0 }));
256 expectProfile(renderer, 2, "Sol");
257 expect(loadingIndicators(profileImage(renderer, "Sol"))).toHaveLength(0);
260 it("advances and matches through the library swipe responder attached to the profile card", () => {
261 renderer = renderCatalogue();
262 press(renderer, "Open Lumen dating demo examples");
264 const maraCard = renderer.root.findByProps({
265 accessibilityLabel: "Profile card for Mara",
267 expect(maraCard.props.onMoveShouldSetResponder({}, { dx: -6, dy: 2 })).toBe(
271 act(() => maraCard.props.onResponderRelease({}, { dx: -110, vx: 0 }));
272 expectProfile(renderer, 2, "Sol");
274 press(renderer, "Undo last choice");
275 const restoredMaraCard = renderer.root.findByProps({
276 accessibilityLabel: "Profile card for Mara",
279 restoredMaraCard.props.onResponderRelease({}, { dx: 110, vx: 0 }),
283 renderer.root.findByProps({ accessibilityLabel: "You and Mara matched" }),
285 expectProfile(renderer, 2, "Sol");
288 it("completes three consecutive left swipes through one responder after each card rerender", () => {
289 const pendingAnimations = [];
290 const spring = vi.spyOn(Animated, "spring").mockImplementation((value, options) => {
293 start: vi.fn((callback) => {
294 completion = callback;
297 complete: (finished = true) => completion?.({ finished }),
299 pendingAnimations.push({ value, options, animation });
303 renderer = renderCatalogue();
304 press(renderer, "Open Lumen dating demo examples");
306 const profiles = ["Mara", "Sol", "Noor"];
307 const nextProfiles = [
308 { position: 2, name: "Sol" },
309 { position: 3, name: "Noor" },
310 { position: 4, name: "Inez" },
315 profiles.forEach((name, index) => {
316 const card = renderer.root.findByProps({
317 accessibilityLabel: `Profile card for ${name}`,
319 const position = card.props.style.transform[0].translateX;
322 sharedPosition = position;
323 sharedRelease = card.props.onResponderRelease;
325 expect(position).toBe(sharedPosition);
326 expect(card.props.onResponderRelease).toBe(sharedRelease);
329 expect(card.props.onMoveShouldSetResponder({}, { dx: -6, dy: 2 })).toBe(
332 act(() => card.props.onResponderRelease({}, { dx: -110, vx: 0 }));
334 expect(sharedPosition._value).toBe(-110);
335 expect(card.props.onMoveShouldSetResponder({}, { dx: -6, dy: 2 })).toBe(
338 expect(pendingAnimations).toHaveLength(index + 1);
339 expect(pendingAnimations[index]).toMatchObject({
340 value: sharedPosition,
341 options: { toValue: -485, useNativeDriver: false },
344 act(() => pendingAnimations[index].animation.complete(true));
346 expect(sharedPosition._value).toBe(0);
349 nextProfiles[index].position,
350 nextProfiles[index].name,
354 expect(spring).toHaveBeenCalledTimes(3);
357 it("keeps the responder reusable while a right-swipe match overlay is rendered", () => {
358 renderer = renderCatalogue();
359 press(renderer, "Open Lumen dating demo examples");
361 const maraCard = renderer.root.findByProps({
362 accessibilityLabel: "Profile card for Mara",
364 act(() => maraCard.props.onResponderRelease({}, { dx: 110, vx: 0 }));
367 renderer.root.findByProps({ accessibilityLabel: "You and Mara matched" }),
369 const coveredSolCard = renderer.root.findByProps({
370 accessibilityLabel: "Profile card for Sol",
373 coveredSolCard.props.onMoveShouldSetResponder({}, { dx: -6, dy: 2 }),
376 press(renderer, "Dismiss match");
377 const exposedSolCard = renderer.root.findByProps({
378 accessibilityLabel: "Profile card for Sol",
381 exposedSolCard.props.onResponderRelease({}, { dx: -110, vx: 0 }),
384 expectProfile(renderer, 3, "Noor");
387 it("navigates from category 08 and preserves discovery, match, and tab transitions", () => {
388 renderer = renderCatalogue();
390 expectVisible(renderer, "08");
391 expectVisible(renderer, "Lumen dating demo");
392 press(renderer, "Open Lumen dating demo examples");
395 renderer.root.findByProps({ accessibilityLabel: "Lumen dating discovery" }),
397 expectVisible(renderer, "LUMEN");
398 expectProfile(renderer, 1, "Mara");
400 press(renderer, "Pass on this profile");
401 expectProfile(renderer, 2, "Sol");
403 press(renderer, "Undo last choice");
404 expectProfile(renderer, 1, "Mara");
407 renderer.root.findByProps({ accessibilityLabel: "Discover, selected" }),
409 press(renderer, "Signals");
411 renderer.root.findByProps({ accessibilityLabel: "Signals, selected" }),
414 renderer.root.findAllByProps({ accessibilityLabel: "Discover, selected" }),
417 press(renderer, "Like this profile");
419 renderer.root.findByProps({ accessibilityLabel: "You and Mara matched" }),
421 expectVisible(renderer, "The feeling is mutual.");
422 press(renderer, "Keep browsing");
424 renderer.root.findAllByProps({ accessibilityLabel: "You and Mara matched" }),
426 expectProfile(renderer, 2, "Sol");
428 press(renderer, "Undo last choice");
429 press(renderer, "Like this profile");
431 renderer.root.findByProps({ accessibilityLabel: "You and Mara matched" }),
433 press(renderer, "Dismiss match");
435 renderer.root.findAllByProps({ accessibilityLabel: "You and Mara matched" }),
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list