-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathimage-matrix-selection-clear.test.tsx
More file actions
171 lines (133 loc) · 5.26 KB
/
Copy pathimage-matrix-selection-clear.test.tsx
File metadata and controls
171 lines (133 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import { describe, expect, it } from "@jest/globals";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { useForm } from "react-hook-form";
declare const jest: typeof import("@jest/globals").jest;
jest.mock("@tauri-apps/plugin-dialog", () => ({
open: () => Promise.resolve(null),
}));
jest.mock("@tauri-apps/plugin-fs", () => ({
readDir: () => Promise.resolve([]),
stat: () => Promise.resolve({ isDirectory: false, isFile: true, size: 1024 }),
}));
import {
SelectedImageProvider,
useSelectedImage,
} from "../src/app/pipeline/selected-image-context";
import { ImageMatrixInput } from "../src/components/ui/image-matrix-input";
import type { ImageSet } from "../src/components/ui/image-set-preview";
import { TooltipProvider } from "../src/components/ui/tooltip";
interface FormValues {
inputSets: ImageSet[];
}
// JPEGs rather than RAW files, for the same reason as `image-matrix-lock`: a
// CR2 would drag the TIFF decode worker into a test about which file stays
// selected.
const SCENE1_FIRST = "/photos/scene1/capt01.jpg";
const SCENE1_SECOND = "/photos/scene1/capt02.jpg";
const SCENE2_ONLY = "/photos/scene2/capt03.jpg";
const twoSets: ImageSet[] = [
{ files: [SCENE1_FIRST, SCENE1_SECOND], name: "scene1" },
{ files: [SCENE2_ONLY], name: "scene2" },
];
const NOTHING_SELECTED = "none";
const REMOVE_SCENE1 = /remove image set scene1/i;
const REMOVE_SCENE2 = /remove image set scene2/i;
function SelectionProbe() {
const { selectedImage } = useSelectedImage();
return <p data-testid="selected">{selectedImage ?? NOTHING_SELECTED}</p>;
}
function Harness() {
const { control } = useForm<FormValues>({
defaultValues: { inputSets: twoSets.map((set) => ({ ...set })) },
});
return (
<TooltipProvider>
<SelectedImageProvider>
<ImageMatrixInput control={control} name="inputSets" />
<SelectionProbe />
</SelectedImageProvider>
</TooltipProvider>
);
}
const settle = () => act(() => new Promise((r) => setTimeout(r, 0)));
async function renderPanel() {
// The file statistics resolve through a suspended child, so the first paint
// is awaited rather than taken synchronously.
let view: ReturnType<typeof render> | undefined;
await act(() => {
view = render(<Harness />);
return Promise.resolve();
});
await settle();
if (!view) {
throw new Error("expected the panel to render");
}
return view;
}
function thumbnails(container: HTMLElement): Element[] {
// One per file, in the order the rows render them, which is each set's files
// sorted -- the same order `onRemoveIndex` resolves against.
return Array.from(container.querySelectorAll(".generic-image-container"));
}
function thumbnailAt(container: HTMLElement, index: number): Element {
const thumbnail = thumbnails(container)[index];
if (!thumbnail) {
throw new Error(`expected a thumbnail at ${index}`);
}
return thumbnail;
}
async function selectThumbnail(container: HTMLElement, index: number) {
fireEvent.click(thumbnailAt(container, index));
await settle();
}
async function removeThumbnail(container: HTMLElement, index: number) {
// The context menu's trigger is the thumbnail wrapper, and a contextmenu
// event only bubbles up.
fireEvent.contextMenu(thumbnailAt(container, index));
await settle();
fireEvent.click(screen.getByText("Remove image"));
await settle();
}
function selected(): string {
const probe = screen.getByTestId("selected").textContent;
return probe ?? "";
}
describe("removing the file that is selected", () => {
// Left selected, the mask preview keeps asking for the dimensions of a frame
// the form no longer holds. For a RAW frame that is worse than stale: its
// queued conversion is dropped along with it, so the metadata promise the
// submit handler awaits rejects, and it is memoized on the path -- every
// later run awaits the same permanently rejected promise.
it("clears the selection when its whole set is removed", async () => {
const { container } = await renderPanel();
await selectThumbnail(container, 0);
expect(selected()).toBe(SCENE1_FIRST);
fireEvent.click(screen.getByRole("button", { name: REMOVE_SCENE1 }));
await settle();
expect(selected()).toBe(NOTHING_SELECTED);
});
it("clears the selection when that one frame is removed", async () => {
const { container } = await renderPanel();
await selectThumbnail(container, 0);
expect(selected()).toBe(SCENE1_FIRST);
await removeThumbnail(container, 0);
expect(selected()).toBe(NOTHING_SELECTED);
});
});
describe("removing a file that is not selected", () => {
// The clear has to be as narrow as the removal, or every removal anywhere in
// the panel would empty a preview the user is still working with.
it("keeps the selection when another set is removed", async () => {
const { container } = await renderPanel();
await selectThumbnail(container, 0);
fireEvent.click(screen.getByRole("button", { name: REMOVE_SCENE2 }));
await settle();
expect(selected()).toBe(SCENE1_FIRST);
});
it("keeps the selection when a sibling frame is removed", async () => {
const { container } = await renderPanel();
await selectThumbnail(container, 0);
await removeThumbnail(container, 1);
expect(selected()).toBe(SCENE1_FIRST);
});
});