Skip to content
Source
Sign inSign up

PendingUserInputCard.test.tsx

142 lines4.7 KB

1import type { EnvironmentId, RuntimeRequestId, ThreadId } from "@t3tools/contracts";
2import type { ReactNode } from "react";
3import { act } from "react";
4import { createRoot } from "react-dom/client";
5import { expect, it, vi } from "vite-plus/test";
6​
7import type { PendingUserInput } from "../../lib/threadActivity";
8​
9const harness = vi.hoisted(() => ({
10 navigate: [] as Array<[string, object | undefined]>,
11 openedUrls: [] as string[],
12}));
13​
14type Children = { children?: ReactNode };
15​
16vi.mock("react-native", () => ({
17 Platform: { OS: "ios" },
18 View: ({ children }: Children) => <div>{children}</div>,
19 ScrollView: ({ children }: Children) => <div>{children}</div>,
20 Pressable: ({ children, onPress }: Children & { onPress?: () => void }) => (
21 <div onClick={onPress}>{children}</div>
22 ),
23}));
24vi.mock("react-native-reanimated", () => {
25 const animation = { duration: () => ({ easing: () => ({}) }) };
26 return {
27 default: { View: ({ children }: Children) => <div>{children}</div> },
28 Easing: { out: () => () => 0, cubic: () => 0 },
29 FadeInUp: animation,
30 FadeOutDown: animation,
31 useAnimatedStyle: () => ({}),
32 useSharedValue: (value: number) => ({ value, set: () => {} }),
33 };
34});
35vi.mock("../../components/ui/AppSymbol", () => ({ SymbolView: () => null }));
36vi.mock("../../components/ui/AppText", () => ({
37 AppText: ({ children }: Children) => <span>{children}</span>,
38}));
39vi.mock("../../components/ui/ControlPill", () => ({ ControlPill: () => null }));
40vi.mock("../../components/ui/Button", () => ({ Button: () => null }));
41vi.mock("./QuestionAttachments", () => ({ QuestionAttachments: () => null }));
42vi.mock("../files/FileMarkdownPreview", () => ({
43 useMarkdownPreviewStyles: () => ({ nativeTextStyle: {} }),
44}));
45vi.mock("../../native/SelectableMarkdownText", () => ({
46 SelectableMarkdownText: (props: { markdown: string; onLinkPress?: (href: string) => void }) => (
47 <p>
48 {[...props.markdown.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)].map(([, label, href]) => (
49 <button key={href} type="button" onClick={() => props.onLinkPress?.(href!)}>
50 {label}
51 </button>
52 ))}
53 </p>
54 ),
55}));
56vi.mock("../../state/entities", () => ({
57 useLiveThreadLinkLabels: (markdown: string) => markdown,
58}));
59vi.mock("../../native/useOpenPullRequest", () => ({
60 useOpenPullRequest: () => async (url: string) => {
61 harness.openedUrls.push(url);
62 return true;
63 },
64}));
65vi.mock("@react-navigation/native", () => ({
66 useNavigation: () => ({
67 navigate: (name: string, params?: object) => harness.navigate.push([name, params]),
68 }),
69}));
70​
71import { PendingUserInputCard } from "./PendingUserInputCard";
72​
73function setup(questionMarkdown: string) {
74 (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
75 harness.navigate = [];
76 harness.openedUrls = [];
77​
78 const pendingUserInput = {
79 requestId: "request-1" as RuntimeRequestId,
80 responseCapability: "resumable",
81 questions: [
82 {
83 id: "question-1",
84 header: "Review",
85 question: questionMarkdown,
86 options: [],
87 allowCustomAnswer: false,
88 },
89 ],
90 } as unknown as PendingUserInput;
91 const container = document.createElement("div");
92 const root = createRoot(container);
93 act(() =>
94 root.render(
95 <PendingUserInputCard
96 environmentId={"env-1" as EnvironmentId}
97 threadId={"thread-1" as ThreadId}
98 workspaceRoot="/repo"
99 canOperateThread
100 pendingUserInput={pendingUserInput}
101 maxHeight={{ value: 600 } as never}
102 cardProgress={{ value: 1, set: () => {} } as never}
103 cardCoverage={{ value: 0, set: () => {} } as never}
104 collapsed={false}
105 onToggleCollapsed={() => {}}
106 drafts={{}}
107 answers={null}
108 respondingUserInputId={null}
109 onSelectOption={() => {}}
110 onChangeCustomAnswer={() => {}}
111 onSubmit={async () => {}}
112 onDismiss={async () => {}}
113 />,
114 ),
115 );
116 return {
117 async pressLink(label: string) {
118 const link = [...container.querySelectorAll("button")].find(
119 (button) => button.textContent === label,
120 );
121 if (!link) throw new Error(`No link labelled ${label}`);
122 await act(async () => link.click());
123 },
124 [Symbol.dispose]() {
125 act(() => root.unmount());
126 },
127 };
129​
130it("opens a workspace file link in a question in the thread's file viewer", async () => {
131 using screen = setup("Should I update [probe.md](docs/probe.md:12)?");
132​
133 await screen.pressLink("probe.md");
134​
135 expect(harness.openedUrls).toEqual([]);
136 expect(harness.navigate).toEqual([
137 [
138 "ThreadFile",
139 { environmentId: "env-1", threadId: "thread-1", path: ["docs", "probe.md"], line: "12" },
140 ],
141 ]);
142});