@gradio/gallery 0.17.5 → 0.17.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/Gallery.test.ts +992 -122
- package/Index.svelte +4 -6
- package/dist/Index.svelte +4 -6
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @gradio/gallery
|
|
2
2
|
|
|
3
|
+
## 0.17.7
|
|
4
|
+
|
|
5
|
+
### Dependency updates
|
|
6
|
+
|
|
7
|
+
- @gradio/atoms@0.24.0
|
|
8
|
+
- @gradio/statustracker@0.14.1
|
|
9
|
+
- @gradio/upload@0.17.9
|
|
10
|
+
- @gradio/file@0.14.8
|
|
11
|
+
- @gradio/image@0.26.3
|
|
12
|
+
- @gradio/video@0.20.8
|
|
13
|
+
|
|
14
|
+
## 0.17.6
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- [#13213](https://github.com/gradio-app/gradio/pull/13213) [`457f130`](https://github.com/gradio-app/gradio/commit/457f130b310ac087fdc6ba7dbbc7fa78e0bcccf8) - Gallery Unit Tests. Thanks @freddyaboulton!
|
|
19
|
+
|
|
20
|
+
### Dependency updates
|
|
21
|
+
|
|
22
|
+
- @gradio/atoms@0.23.1
|
|
23
|
+
- @gradio/statustracker@0.14.0
|
|
24
|
+
- @gradio/client@2.2.0
|
|
25
|
+
- @gradio/image@0.26.2
|
|
26
|
+
- @gradio/file@0.14.7
|
|
27
|
+
- @gradio/video@0.20.7
|
|
28
|
+
|
|
3
29
|
## 0.17.5
|
|
4
30
|
|
|
5
31
|
### Fixes
|
package/Gallery.test.ts
CHANGED
|
@@ -1,147 +1,1017 @@
|
|
|
1
|
-
import { test, describe,
|
|
2
|
-
import { cleanup, render } from "@self/tootils/render";
|
|
3
|
-
import {
|
|
1
|
+
import { test, describe, afterEach, expect, vi } from "vitest";
|
|
2
|
+
import { cleanup, render, fireEvent, waitFor } from "@self/tootils/render";
|
|
3
|
+
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
|
|
4
4
|
|
|
5
5
|
import Gallery from "./Index.svelte";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
|
|
7
|
+
const fake_image = (id: string) => ({
|
|
8
|
+
path: `${id}.png`,
|
|
9
|
+
url: `https://example.com/${id}.png`,
|
|
10
|
+
orig_name: `${id}.png`,
|
|
11
|
+
size: 1024,
|
|
12
|
+
mime_type: "image/png",
|
|
13
|
+
is_stream: false
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const fake_video = (id: string) => ({
|
|
17
|
+
path: `${id}.mp4`,
|
|
18
|
+
url: `https://example.com/${id}.mp4`,
|
|
19
|
+
orig_name: `${id}.mp4`,
|
|
20
|
+
size: 2048,
|
|
21
|
+
mime_type: "video/mp4",
|
|
22
|
+
is_stream: false
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const img = (id: string, caption: string | null = null) => ({
|
|
26
|
+
image: fake_image(id),
|
|
27
|
+
caption
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const vid = (id: string, caption: string | null = null) => ({
|
|
31
|
+
video: fake_video(id),
|
|
32
|
+
caption
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const default_props = {
|
|
36
|
+
label: "Gallery",
|
|
37
|
+
show_label: true,
|
|
38
|
+
preview: false,
|
|
39
|
+
allow_preview: true,
|
|
40
|
+
selected_index: null as number | null,
|
|
41
|
+
object_fit: "cover" as const,
|
|
42
|
+
buttons: ["fullscreen"] as (
|
|
43
|
+
| string
|
|
44
|
+
| { value: string; id: number; icon: null }
|
|
45
|
+
)[],
|
|
46
|
+
value: null as any,
|
|
47
|
+
columns: 2,
|
|
48
|
+
rows: undefined as number | undefined,
|
|
49
|
+
height: "auto",
|
|
50
|
+
fit_columns: true,
|
|
51
|
+
interactive: false,
|
|
52
|
+
sources: ["upload"] as ("upload" | "webcam" | "clipboard")[]
|
|
17
53
|
};
|
|
18
54
|
|
|
55
|
+
const three_images = [img("img1"), img("img2"), img("img3")];
|
|
56
|
+
|
|
57
|
+
run_shared_prop_tests({
|
|
58
|
+
component: Gallery,
|
|
59
|
+
name: "Gallery",
|
|
60
|
+
base_props: {
|
|
61
|
+
...default_props
|
|
62
|
+
},
|
|
63
|
+
has_label: false
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("Props: label", () => {
|
|
67
|
+
afterEach(() => cleanup());
|
|
68
|
+
|
|
69
|
+
test("label text is rendered when show_label=true", async () => {
|
|
70
|
+
const { getByText } = await render(Gallery, {
|
|
71
|
+
...default_props,
|
|
72
|
+
label: "My Gallery",
|
|
73
|
+
show_label: true,
|
|
74
|
+
value: three_images
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(getByText("My Gallery")).toBeVisible();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("show_label=false removes label from DOM entirely", async () => {
|
|
81
|
+
const { queryByText } = await render(Gallery, {
|
|
82
|
+
...default_props,
|
|
83
|
+
label: "My Gallery",
|
|
84
|
+
show_label: false,
|
|
85
|
+
value: three_images
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(queryByText("My Gallery")).not.toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
19
92
|
describe("Gallery", () => {
|
|
20
|
-
afterEach(() =>
|
|
21
|
-
|
|
22
|
-
|
|
93
|
+
afterEach(() => cleanup());
|
|
94
|
+
|
|
95
|
+
test("renders empty state when value is null", async () => {
|
|
96
|
+
const { container } = await render(Gallery, {
|
|
97
|
+
...default_props,
|
|
98
|
+
value: null
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Empty gallery shows an SVG icon placeholder, no thumbnails
|
|
102
|
+
const thumbnails = container.querySelectorAll("[aria-label^='Thumbnail']");
|
|
103
|
+
expect(thumbnails.length).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("renders empty state when value is empty array", async () => {
|
|
107
|
+
const { container } = await render(Gallery, {
|
|
108
|
+
...default_props,
|
|
109
|
+
value: []
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const thumbnails = container.querySelectorAll("[aria-label^='Thumbnail']");
|
|
113
|
+
expect(thumbnails.length).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("renders image thumbnails in grid", async () => {
|
|
117
|
+
const { getByRole } = await render(Gallery, {
|
|
118
|
+
...default_props,
|
|
119
|
+
value: three_images
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(getByRole("button", { name: "Thumbnail 1 of 3" })).toBeVisible();
|
|
123
|
+
expect(getByRole("button", { name: "Thumbnail 2 of 3" })).toBeVisible();
|
|
124
|
+
expect(getByRole("button", { name: "Thumbnail 3 of 3" })).toBeVisible();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("renders video thumbnails in grid", async () => {
|
|
128
|
+
const { getByRole } = await render(Gallery, {
|
|
129
|
+
...default_props,
|
|
130
|
+
value: [vid("clip1"), vid("clip2")]
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(getByRole("button", { name: "Thumbnail 1 of 2" })).toBeVisible();
|
|
134
|
+
expect(getByRole("button", { name: "Thumbnail 2 of 2" })).toBeVisible();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("renders captions on thumbnails", async () => {
|
|
138
|
+
const { getByText } = await render(Gallery, {
|
|
139
|
+
...default_props,
|
|
140
|
+
value: [img("cat", "A cute cat"), img("dog", "A good dog")]
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(getByText("A cute cat")).toBeVisible();
|
|
144
|
+
expect(getByText("A good dog")).toBeVisible();
|
|
23
145
|
});
|
|
24
|
-
|
|
25
|
-
|
|
146
|
+
|
|
147
|
+
test("mixed images and videos render correctly", async () => {
|
|
148
|
+
const { getByRole } = await render(Gallery, {
|
|
149
|
+
...default_props,
|
|
150
|
+
value: [img("photo"), vid("clip"), img("photo2")]
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(getByRole("button", { name: "Thumbnail 1 of 3" })).toBeVisible();
|
|
154
|
+
expect(getByRole("button", { name: "Thumbnail 2 of 3" })).toBeVisible();
|
|
155
|
+
expect(getByRole("button", { name: "Thumbnail 3 of 3" })).toBeVisible();
|
|
26
156
|
});
|
|
27
|
-
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("Props: preview / allow_preview", () => {
|
|
160
|
+
afterEach(() => cleanup());
|
|
161
|
+
|
|
162
|
+
test("preview=true with selected_index shows detailed view", async () => {
|
|
163
|
+
// Gallery.svelte auto-selects index 0 when preview=true and
|
|
164
|
+
// selected_index is null, but the Gradio prop framework doesn't
|
|
165
|
+
// propagate the internal binding back synchronously. Test the
|
|
166
|
+
// explicit selected_index=0 case which is the effective behavior.
|
|
28
167
|
const { getByTestId } = await render(Gallery, {
|
|
29
|
-
|
|
30
|
-
label: "Gallery",
|
|
31
|
-
loading_status: loading_status,
|
|
168
|
+
...default_props,
|
|
32
169
|
preview: true,
|
|
33
|
-
buttons: ["share", "download", "fullscreen"],
|
|
34
170
|
selected_index: 0,
|
|
35
|
-
value: [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
171
|
+
value: [img("cat"), img("dog")]
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const detail = getByTestId("detailed-image") as HTMLImageElement;
|
|
175
|
+
expect(detail.src).toBe("https://example.com/cat.png");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("preview=false does not show detailed view", async () => {
|
|
179
|
+
const { queryByTestId } = await render(Gallery, {
|
|
180
|
+
...default_props,
|
|
181
|
+
preview: false,
|
|
182
|
+
value: three_images
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(queryByTestId("detailed-image")).not.toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("allow_preview=false hides preview even when selected_index is set", async () => {
|
|
189
|
+
const { queryByTestId } = await render(Gallery, {
|
|
190
|
+
...default_props,
|
|
191
|
+
allow_preview: false,
|
|
192
|
+
selected_index: 0,
|
|
193
|
+
value: three_images
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(queryByTestId("detailed-image")).not.toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("allow_preview=true shows preview when thumbnail clicked", async () => {
|
|
200
|
+
const { getByRole, getByTestId } = await render(Gallery, {
|
|
201
|
+
...default_props,
|
|
202
|
+
allow_preview: true,
|
|
203
|
+
preview: false,
|
|
204
|
+
value: three_images
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await fireEvent.click(getByRole("button", { name: "Thumbnail 2 of 3" }));
|
|
208
|
+
|
|
209
|
+
const detail = getByTestId("detailed-image") as HTMLImageElement;
|
|
210
|
+
expect(detail.src).toBe("https://example.com/img2.png");
|
|
50
211
|
});
|
|
51
212
|
|
|
52
|
-
test("renders the video provided", async () => {
|
|
213
|
+
test("renders the video provided in preview", async () => {
|
|
53
214
|
const { getByTestId } = await render(Gallery, {
|
|
54
|
-
|
|
55
|
-
label: "Gallery",
|
|
56
|
-
loading_status: loading_status,
|
|
215
|
+
...default_props,
|
|
57
216
|
preview: true,
|
|
58
|
-
buttons: ["share", "download", "fullscreen"],
|
|
59
217
|
selected_index: 0,
|
|
60
|
-
value: [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
},
|
|
66
|
-
caption: null
|
|
67
|
-
}
|
|
68
|
-
]
|
|
69
|
-
});
|
|
70
|
-
let item = getByTestId("detailed-video") as HTMLVideoElement;
|
|
71
|
-
assert.equal(
|
|
72
|
-
item.src,
|
|
73
|
-
"https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/gallery_component/files/world.mp4"
|
|
74
|
-
);
|
|
218
|
+
value: [vid("clip")]
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const item = getByTestId("detailed-video") as HTMLVideoElement;
|
|
222
|
+
expect(item.src).toBe("https://example.com/clip.mp4");
|
|
75
223
|
});
|
|
224
|
+
});
|
|
76
225
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
226
|
+
describe("Props: selected_index", () => {
|
|
227
|
+
afterEach(() => cleanup());
|
|
228
|
+
|
|
229
|
+
test("selected_index shows the correct image in preview", async () => {
|
|
230
|
+
const { getByTestId } = await render(Gallery, {
|
|
231
|
+
...default_props,
|
|
83
232
|
preview: true,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
assert.equal(change_event.callCount, 0);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test.skip("triggers preview_close event when pressing Escape key", async () => {
|
|
111
|
-
const { listen, getByTestId } = await render(Gallery, {
|
|
112
|
-
show_label: true,
|
|
113
|
-
label: "Gallery",
|
|
114
|
-
loading_status: loading_status,
|
|
233
|
+
selected_index: 1,
|
|
234
|
+
allow_preview: true,
|
|
235
|
+
value: three_images
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const detail = getByTestId("detailed-image") as HTMLImageElement;
|
|
239
|
+
expect(detail.src).toBe("https://example.com/img2.png");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("selected_index=null shows no preview", async () => {
|
|
243
|
+
const { queryByTestId } = await render(Gallery, {
|
|
244
|
+
...default_props,
|
|
245
|
+
preview: false,
|
|
246
|
+
selected_index: null,
|
|
247
|
+
value: three_images
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(queryByTestId("detailed-image")).not.toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("changing selected_index via set_data updates preview", async () => {
|
|
254
|
+
const { getByTestId, set_data } = await render(Gallery, {
|
|
255
|
+
...default_props,
|
|
115
256
|
preview: true,
|
|
116
|
-
buttons: ["share", "download", "fullscreen"],
|
|
117
257
|
selected_index: 0,
|
|
118
|
-
value:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
258
|
+
value: three_images
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await set_data({ selected_index: 2 });
|
|
262
|
+
|
|
263
|
+
const detail = getByTestId("detailed-image") as HTMLImageElement;
|
|
264
|
+
expect(detail.src).toBe("https://example.com/img3.png");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("Props: columns", () => {
|
|
269
|
+
afterEach(() => cleanup());
|
|
270
|
+
|
|
271
|
+
test("columns=3 sets grid to 3 columns", async () => {
|
|
272
|
+
const { container } = await render(Gallery, {
|
|
273
|
+
...default_props,
|
|
274
|
+
columns: 3,
|
|
275
|
+
fit_columns: false,
|
|
276
|
+
value: three_images
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// No testid or role available on the grid-container div; querySelector is the only option
|
|
280
|
+
const grid = container.querySelector(".grid-container") as HTMLElement;
|
|
281
|
+
expect(grid).toBeInTheDocument();
|
|
282
|
+
expect(grid.style.getPropertyValue("--grid-cols")).toBe("3");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("columns=2 with 5 images renders all 5 items in a 2-column grid", async () => {
|
|
286
|
+
const five_images = Array.from({ length: 5 }, (_, i) => img(`img${i + 1}`));
|
|
287
|
+
|
|
288
|
+
const { container, getByRole } = await render(Gallery, {
|
|
289
|
+
...default_props,
|
|
290
|
+
columns: 2,
|
|
291
|
+
fit_columns: false,
|
|
292
|
+
value: five_images
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// All 5 thumbnails are rendered
|
|
296
|
+
for (let i = 1; i <= 5; i++) {
|
|
297
|
+
expect(
|
|
298
|
+
getByRole("button", { name: `Thumbnail ${i} of 5` })
|
|
299
|
+
).toBeVisible();
|
|
140
300
|
}
|
|
141
301
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
302
|
+
// Grid is set to 2 columns (wrapping after 2)
|
|
303
|
+
const grid = container.querySelector(".grid-container") as HTMLElement;
|
|
304
|
+
expect(grid.style.getPropertyValue("--grid-cols")).toBe("2");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("columns as array sets responsive breakpoints", async () => {
|
|
308
|
+
const { container } = await render(Gallery, {
|
|
309
|
+
...default_props,
|
|
310
|
+
columns: [1, 2, 3],
|
|
311
|
+
fit_columns: false,
|
|
312
|
+
value: three_images
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const grid = container.querySelector(".grid-container") as HTMLElement;
|
|
316
|
+
expect(grid.style.getPropertyValue("--grid-cols")).toBe("1,2,3");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("Props: fit_columns", () => {
|
|
321
|
+
afterEach(() => cleanup());
|
|
322
|
+
|
|
323
|
+
test("fit_columns=true with 1 image and columns=3 reduces to 1 column", async () => {
|
|
324
|
+
const { container } = await render(Gallery, {
|
|
325
|
+
...default_props,
|
|
326
|
+
columns: 3,
|
|
327
|
+
fit_columns: true,
|
|
328
|
+
value: [img("only")]
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const grid = container.querySelector(".grid-container") as HTMLElement;
|
|
332
|
+
expect(grid.style.getPropertyValue("--grid-cols")).toBe("1");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("fit_columns=false with 1 image and columns=3 keeps 3 columns", async () => {
|
|
336
|
+
const { container } = await render(Gallery, {
|
|
337
|
+
...default_props,
|
|
338
|
+
columns: 3,
|
|
339
|
+
fit_columns: false,
|
|
340
|
+
value: [img("only")]
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const grid = container.querySelector(".grid-container") as HTMLElement;
|
|
344
|
+
expect(grid.style.getPropertyValue("--grid-cols")).toBe("3");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("fit_columns=true with items equal to columns keeps columns unchanged", async () => {
|
|
348
|
+
const { container } = await render(Gallery, {
|
|
349
|
+
...default_props,
|
|
350
|
+
columns: 3,
|
|
351
|
+
fit_columns: true,
|
|
352
|
+
value: three_images
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const grid = container.querySelector(".grid-container") as HTMLElement;
|
|
356
|
+
expect(grid.style.getPropertyValue("--grid-cols")).toBe("3");
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe("Props: buttons", () => {
|
|
361
|
+
afterEach(() => cleanup());
|
|
362
|
+
|
|
363
|
+
const preview_props = {
|
|
364
|
+
...default_props,
|
|
365
|
+
preview: true,
|
|
366
|
+
selected_index: 0,
|
|
367
|
+
value: [img("cat")]
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
test("buttons=['download'] shows download button in preview", async () => {
|
|
371
|
+
const { getByRole } = await render(Gallery, {
|
|
372
|
+
...preview_props,
|
|
373
|
+
buttons: ["download"]
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(getByRole("button", { name: "common.download" })).toBeVisible();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("buttons=['fullscreen'] shows fullscreen button in preview", async () => {
|
|
380
|
+
const { getByLabelText } = await render(Gallery, {
|
|
381
|
+
...preview_props,
|
|
382
|
+
buttons: ["fullscreen"]
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(getByLabelText("Fullscreen")).toBeVisible();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("buttons=[] shows only close button in preview", async () => {
|
|
389
|
+
const { queryByRole, queryByLabelText, getByRole } = await render(Gallery, {
|
|
390
|
+
...preview_props,
|
|
391
|
+
buttons: []
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(getByRole("button", { name: "Close" })).toBeVisible();
|
|
395
|
+
expect(
|
|
396
|
+
queryByRole("button", { name: "common.download" })
|
|
397
|
+
).not.toBeInTheDocument();
|
|
398
|
+
expect(queryByLabelText("Fullscreen")).not.toBeInTheDocument();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("custom button renders and dispatches custom_button_click", async () => {
|
|
402
|
+
const { listen, getByLabelText } = await render(Gallery, {
|
|
403
|
+
...preview_props,
|
|
404
|
+
buttons: [{ value: "Analyze", id: 7, icon: null }]
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const custom = listen("custom_button_click");
|
|
408
|
+
const btn = getByLabelText("Analyze");
|
|
409
|
+
await fireEvent.click(btn);
|
|
410
|
+
|
|
411
|
+
expect(custom).toHaveBeenCalledTimes(1);
|
|
412
|
+
expect(custom).toHaveBeenCalledWith({ id: 7 });
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("Props: interactive", () => {
|
|
417
|
+
afterEach(() => cleanup());
|
|
418
|
+
|
|
419
|
+
test("interactive=true shows delete buttons on gallery items", async () => {
|
|
420
|
+
const { getAllByRole } = await render(Gallery, {
|
|
421
|
+
...default_props,
|
|
422
|
+
interactive: true,
|
|
423
|
+
value: three_images
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const deleteButtons = getAllByRole("button", { name: "Delete image" });
|
|
427
|
+
expect(deleteButtons.length).toBe(3);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("interactive=false hides delete buttons", async () => {
|
|
431
|
+
const { queryByRole } = await render(Gallery, {
|
|
432
|
+
...default_props,
|
|
433
|
+
interactive: false,
|
|
434
|
+
value: three_images
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
expect(
|
|
438
|
+
queryByRole("button", { name: "Delete image" })
|
|
439
|
+
).not.toBeInTheDocument();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("interactive=true with no value shows upload area", async () => {
|
|
443
|
+
const { getByTestId } = await render(Gallery, {
|
|
444
|
+
...default_props,
|
|
445
|
+
interactive: true,
|
|
446
|
+
value: null
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// The file input itself is display:none (the visible area is a styled overlay),
|
|
450
|
+
// so we check it exists in the DOM rather than visibility.
|
|
451
|
+
expect(getByTestId("file-upload")).toBeInTheDocument();
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("interactive=false with no value does not show upload area", async () => {
|
|
455
|
+
const { queryByTestId } = await render(Gallery, {
|
|
456
|
+
...default_props,
|
|
457
|
+
interactive: false,
|
|
458
|
+
value: null
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
expect(queryByTestId("file-upload")).not.toBeInTheDocument();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe("Props: sources", () => {
|
|
466
|
+
afterEach(() => cleanup());
|
|
467
|
+
|
|
468
|
+
const sources_props = {
|
|
469
|
+
...default_props,
|
|
470
|
+
interactive: true,
|
|
471
|
+
selected_index: null as number | null,
|
|
472
|
+
value: three_images
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
test("sources=['upload'] shows only upload button", async () => {
|
|
476
|
+
const { getByLabelText, queryByLabelText } = await render(Gallery, {
|
|
477
|
+
...sources_props,
|
|
478
|
+
sources: ["upload"]
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(getByLabelText("upload_text.click_to_upload")).toBeVisible();
|
|
482
|
+
expect(queryByLabelText("common.webcam")).not.toBeInTheDocument();
|
|
483
|
+
expect(
|
|
484
|
+
queryByLabelText("upload_text.paste_clipboard")
|
|
485
|
+
).not.toBeInTheDocument();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("sources=['upload', 'webcam'] shows upload and webcam buttons", async () => {
|
|
489
|
+
const { getByLabelText } = await render(Gallery, {
|
|
490
|
+
...sources_props,
|
|
491
|
+
sources: ["upload", "webcam"]
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
expect(getByLabelText("upload_text.click_to_upload")).toBeVisible();
|
|
495
|
+
expect(getByLabelText("common.webcam")).toBeVisible();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("sources=['upload', 'clipboard'] shows upload and clipboard buttons", async () => {
|
|
499
|
+
const { getByLabelText } = await render(Gallery, {
|
|
500
|
+
...sources_props,
|
|
501
|
+
sources: ["upload", "clipboard"]
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
expect(getByLabelText("upload_text.click_to_upload")).toBeVisible();
|
|
505
|
+
expect(getByLabelText("upload_text.paste_clipboard")).toBeVisible();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("sources=['upload', 'webcam', 'clipboard'] shows all source buttons", async () => {
|
|
509
|
+
const { getByLabelText } = await render(Gallery, {
|
|
510
|
+
...sources_props,
|
|
511
|
+
sources: ["upload", "webcam", "clipboard"]
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
expect(getByLabelText("upload_text.click_to_upload")).toBeVisible();
|
|
515
|
+
expect(getByLabelText("common.webcam")).toBeVisible();
|
|
516
|
+
expect(getByLabelText("upload_text.paste_clipboard")).toBeVisible();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("source buttons are hidden when selected_index is set (preview active)", async () => {
|
|
520
|
+
const { queryByLabelText } = await render(Gallery, {
|
|
521
|
+
...sources_props,
|
|
522
|
+
sources: ["upload", "webcam", "clipboard"],
|
|
523
|
+
selected_index: 0,
|
|
524
|
+
preview: true
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
expect(queryByLabelText("common.webcam")).not.toBeInTheDocument();
|
|
528
|
+
expect(
|
|
529
|
+
queryByLabelText("upload_text.paste_clipboard")
|
|
530
|
+
).not.toBeInTheDocument();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("source buttons are hidden when interactive=false", async () => {
|
|
534
|
+
const { queryByLabelText } = await render(Gallery, {
|
|
535
|
+
...sources_props,
|
|
536
|
+
interactive: false,
|
|
537
|
+
sources: ["upload", "webcam", "clipboard"]
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
expect(queryByLabelText("common.webcam")).not.toBeInTheDocument();
|
|
541
|
+
expect(
|
|
542
|
+
queryByLabelText("upload_text.paste_clipboard")
|
|
543
|
+
).not.toBeInTheDocument();
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("Props: visual-only", () => {
|
|
548
|
+
test.todo(
|
|
549
|
+
"VISUAL: object_fit='contain' vs 'cover' changes image display in thumbnails — needs Playwright visual regression screenshot comparison"
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
test.todo(
|
|
553
|
+
"VISUAL: height prop sets gallery container height — needs Playwright visual regression screenshot comparison"
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
test.todo(
|
|
557
|
+
"VISUAL: rows prop controls grid row count — needs Playwright visual regression screenshot comparison"
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe("Events: change", () => {
|
|
562
|
+
afterEach(() => cleanup());
|
|
563
|
+
|
|
564
|
+
test("change: fired when value changes via set_data", async () => {
|
|
565
|
+
const { listen, set_data } = await render(Gallery, {
|
|
566
|
+
...default_props,
|
|
567
|
+
value: null
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const change = listen("change");
|
|
571
|
+
await set_data({ value: three_images });
|
|
572
|
+
|
|
573
|
+
expect(change).toHaveBeenCalled();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("change: not fired spuriously on mount", async () => {
|
|
577
|
+
const { listen } = await render(Gallery, {
|
|
578
|
+
...default_props,
|
|
579
|
+
value: three_images
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const change = listen("change", { retrospective: true });
|
|
583
|
+
expect(change).not.toHaveBeenCalled();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("change: fired each time value updates", async () => {
|
|
587
|
+
const { listen, set_data } = await render(Gallery, {
|
|
588
|
+
...default_props,
|
|
589
|
+
value: null
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const change = listen("change");
|
|
593
|
+
|
|
594
|
+
await set_data({ value: [img("a")] });
|
|
595
|
+
await set_data({ value: [img("a"), img("b")] });
|
|
596
|
+
|
|
597
|
+
expect(change).toHaveBeenCalledTimes(2);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("change: setting value to null after items triggers change", async () => {
|
|
601
|
+
const { listen, set_data } = await render(Gallery, {
|
|
602
|
+
...default_props,
|
|
603
|
+
value: three_images
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const change = listen("change");
|
|
607
|
+
await set_data({ value: null });
|
|
608
|
+
|
|
609
|
+
expect(change).toHaveBeenCalled();
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
describe("Events: select", () => {
|
|
614
|
+
afterEach(() => cleanup());
|
|
615
|
+
|
|
616
|
+
test("select: fired when thumbnail is clicked", async () => {
|
|
617
|
+
const { listen, getByRole } = await render(Gallery, {
|
|
618
|
+
...default_props,
|
|
619
|
+
value: three_images
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const select = listen("select");
|
|
623
|
+
|
|
624
|
+
await fireEvent.click(getByRole("button", { name: "Thumbnail 2 of 3" }));
|
|
625
|
+
|
|
626
|
+
expect(select).toHaveBeenCalledTimes(1);
|
|
627
|
+
expect(select).toHaveBeenCalledWith(expect.objectContaining({ index: 1 }));
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("select: fired when selected_index changes via set_data", async () => {
|
|
631
|
+
const { listen, set_data } = await render(Gallery, {
|
|
632
|
+
...default_props,
|
|
633
|
+
preview: true,
|
|
634
|
+
selected_index: 0,
|
|
635
|
+
value: three_images
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const select = listen("select");
|
|
639
|
+
|
|
640
|
+
await set_data({ selected_index: 2 });
|
|
641
|
+
|
|
642
|
+
expect(select).toHaveBeenCalled();
|
|
643
|
+
expect(select).toHaveBeenCalledWith(expect.objectContaining({ index: 2 }));
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
describe("Events: delete", () => {
|
|
648
|
+
afterEach(() => cleanup());
|
|
649
|
+
|
|
650
|
+
test("delete: fired when delete button is clicked", async () => {
|
|
651
|
+
const { listen, getAllByRole } = await render(Gallery, {
|
|
652
|
+
...default_props,
|
|
653
|
+
interactive: true,
|
|
654
|
+
value: [img("a"), img("b"), img("c")]
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const del = listen("delete");
|
|
658
|
+
|
|
659
|
+
const deleteButtons = getAllByRole("button", { name: "Delete image" });
|
|
660
|
+
await fireEvent.click(deleteButtons[1]);
|
|
661
|
+
|
|
662
|
+
expect(del).toHaveBeenCalledTimes(1);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("delete: removes item from gallery", async () => {
|
|
666
|
+
const { getAllByRole, queryByRole } = await render(Gallery, {
|
|
667
|
+
...default_props,
|
|
668
|
+
interactive: true,
|
|
669
|
+
value: [img("a"), img("b")]
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
expect(queryByRole("button", { name: "Thumbnail 2 of 2" })).toBeVisible();
|
|
673
|
+
|
|
674
|
+
const deleteButtons = getAllByRole("button", { name: "Delete image" });
|
|
675
|
+
await fireEvent.click(deleteButtons[0]);
|
|
676
|
+
|
|
677
|
+
await waitFor(() => {
|
|
678
|
+
expect(
|
|
679
|
+
queryByRole("button", { name: "Thumbnail 2 of 2" })
|
|
680
|
+
).not.toBeInTheDocument();
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
expect(queryByRole("button", { name: "Thumbnail 1 of 1" })).toBeVisible();
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
describe("Events: preview_open / preview_close", () => {
|
|
688
|
+
afterEach(() => cleanup());
|
|
689
|
+
|
|
690
|
+
test("preview_open: fired when clicking thumbnail to enter preview", async () => {
|
|
691
|
+
const { listen, getByRole } = await render(Gallery, {
|
|
692
|
+
...default_props,
|
|
693
|
+
allow_preview: true,
|
|
694
|
+
preview: false,
|
|
695
|
+
selected_index: null,
|
|
696
|
+
value: three_images
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
const preview_open = listen("preview_open");
|
|
700
|
+
|
|
701
|
+
await fireEvent.click(getByRole("button", { name: "Thumbnail 1 of 3" }));
|
|
702
|
+
|
|
703
|
+
expect(preview_open).toHaveBeenCalledTimes(1);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("preview_close: fired when clicking close button", async () => {
|
|
707
|
+
const { listen, getByRole } = await render(Gallery, {
|
|
708
|
+
...default_props,
|
|
709
|
+
preview: true,
|
|
710
|
+
selected_index: 0,
|
|
711
|
+
value: three_images
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
const preview_close = listen("preview_close");
|
|
715
|
+
|
|
716
|
+
await fireEvent.click(getByRole("button", { name: "Close" }));
|
|
717
|
+
|
|
718
|
+
expect(preview_close).toHaveBeenCalledTimes(1);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test("preview_close: fired when pressing Escape in preview", async () => {
|
|
722
|
+
const { listen, getByRole } = await render(Gallery, {
|
|
723
|
+
...default_props,
|
|
724
|
+
preview: true,
|
|
725
|
+
selected_index: 0,
|
|
726
|
+
value: three_images
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const preview_close = listen("preview_close");
|
|
730
|
+
const detail_button = getByRole("button", {
|
|
731
|
+
name: "detailed view of selected image"
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
await fireEvent.keyDown(detail_button, { code: "Escape" });
|
|
735
|
+
|
|
736
|
+
expect(preview_close).toHaveBeenCalledTimes(1);
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
describe("Keyboard navigation", () => {
|
|
741
|
+
afterEach(() => cleanup());
|
|
742
|
+
|
|
743
|
+
const keyboard_props = {
|
|
744
|
+
...default_props,
|
|
745
|
+
preview: true,
|
|
746
|
+
selected_index: 0,
|
|
747
|
+
value: three_images
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
test("ArrowRight advances to next image", async () => {
|
|
751
|
+
const { getByTestId, getByRole } = await render(Gallery, keyboard_props);
|
|
752
|
+
|
|
753
|
+
const detail_button = getByRole("button", {
|
|
754
|
+
name: "detailed view of selected image"
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
await fireEvent.keyDown(detail_button, { code: "ArrowRight" });
|
|
758
|
+
|
|
759
|
+
const detail = getByTestId("detailed-image") as HTMLImageElement;
|
|
760
|
+
expect(detail.src).toBe("https://example.com/img2.png");
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("ArrowLeft goes to previous image (wraps to last)", async () => {
|
|
764
|
+
const { getByTestId, getByRole } = await render(Gallery, keyboard_props);
|
|
765
|
+
|
|
766
|
+
const detail_button = getByRole("button", {
|
|
767
|
+
name: "detailed view of selected image"
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
await fireEvent.keyDown(detail_button, { code: "ArrowLeft" });
|
|
771
|
+
|
|
772
|
+
const detail = getByTestId("detailed-image") as HTMLImageElement;
|
|
773
|
+
expect(detail.src).toBe("https://example.com/img3.png");
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test("ArrowRight wraps around at end", async () => {
|
|
777
|
+
const { getByTestId, getByRole, set_data } = await render(
|
|
778
|
+
Gallery,
|
|
779
|
+
keyboard_props
|
|
145
780
|
);
|
|
781
|
+
|
|
782
|
+
await set_data({ selected_index: 2 });
|
|
783
|
+
|
|
784
|
+
const detail_button = getByRole("button", {
|
|
785
|
+
name: "detailed view of selected image"
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
await fireEvent.keyDown(detail_button, { code: "ArrowRight" });
|
|
789
|
+
|
|
790
|
+
const detail = getByTestId("detailed-image") as HTMLImageElement;
|
|
791
|
+
expect(detail.src).toBe("https://example.com/img1.png");
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("Escape closes preview", async () => {
|
|
795
|
+
const { queryByTestId, getByRole } = await render(Gallery, keyboard_props);
|
|
796
|
+
|
|
797
|
+
const detail_button = getByRole("button", {
|
|
798
|
+
name: "detailed view of selected image"
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
await fireEvent.keyDown(detail_button, { code: "Escape" });
|
|
802
|
+
|
|
803
|
+
expect(queryByTestId("detailed-image")).not.toBeInTheDocument();
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
describe("get_data / set_data", () => {
|
|
808
|
+
afterEach(() => cleanup());
|
|
809
|
+
|
|
810
|
+
test("get_data returns current value", async () => {
|
|
811
|
+
const { get_data } = await render(Gallery, {
|
|
812
|
+
...default_props,
|
|
813
|
+
value: three_images
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
const data = await get_data();
|
|
817
|
+
expect(data.value).toEqual(three_images);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("get_data returns null when no value", async () => {
|
|
821
|
+
const { get_data } = await render(Gallery, {
|
|
822
|
+
...default_props,
|
|
823
|
+
value: null
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
const data = await get_data();
|
|
827
|
+
expect(data.value).toBeNull();
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
test("set_data with new value updates DOM", async () => {
|
|
831
|
+
const { set_data, getByRole, queryByRole } = await render(Gallery, {
|
|
832
|
+
...default_props,
|
|
833
|
+
value: [img("a")]
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
expect(getByRole("button", { name: "Thumbnail 1 of 1" })).toBeVisible();
|
|
837
|
+
|
|
838
|
+
await set_data({ value: [img("a"), img("b"), img("c")] });
|
|
839
|
+
|
|
840
|
+
expect(getByRole("button", { name: "Thumbnail 1 of 3" })).toBeVisible();
|
|
841
|
+
expect(getByRole("button", { name: "Thumbnail 3 of 3" })).toBeVisible();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("set_data -> get_data round-trip", async () => {
|
|
845
|
+
const { set_data, get_data } = await render(Gallery, {
|
|
846
|
+
...default_props,
|
|
847
|
+
value: null
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
const new_value = [img("x"), img("y")];
|
|
851
|
+
await set_data({ value: new_value });
|
|
852
|
+
|
|
853
|
+
const data = await get_data();
|
|
854
|
+
expect(data.value).toEqual(new_value);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
test("set_data with null clears gallery", async () => {
|
|
858
|
+
const { set_data, get_data, queryByRole } = await render(Gallery, {
|
|
859
|
+
...default_props,
|
|
860
|
+
value: three_images
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
await set_data({ value: null });
|
|
864
|
+
|
|
865
|
+
const data = await get_data();
|
|
866
|
+
expect(data.value).toBeNull();
|
|
867
|
+
expect(
|
|
868
|
+
queryByRole("button", { name: /^Thumbnail/ })
|
|
869
|
+
).not.toBeInTheDocument();
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
describe("Edge cases", () => {
|
|
874
|
+
afterEach(() => cleanup());
|
|
875
|
+
|
|
876
|
+
test("caption in preview shows caption text", async () => {
|
|
877
|
+
const { getAllByText } = await render(Gallery, {
|
|
878
|
+
...default_props,
|
|
879
|
+
preview: true,
|
|
880
|
+
selected_index: 0,
|
|
881
|
+
value: [img("cat", "A beautiful cat")]
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// Caption appears in both the preview caption and the grid thumbnail label
|
|
885
|
+
const matches = getAllByText("A beautiful cat");
|
|
886
|
+
expect(matches.length).toBeGreaterThanOrEqual(1);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test("no caption hides caption element in preview", async () => {
|
|
890
|
+
const { queryByText } = await render(Gallery, {
|
|
891
|
+
...default_props,
|
|
892
|
+
preview: true,
|
|
893
|
+
selected_index: 0,
|
|
894
|
+
value: [img("cat", null)]
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// The caption element should not be present
|
|
898
|
+
// (there's no static text content to query for a missing caption,
|
|
899
|
+
// so we verify no <caption> tag is rendered)
|
|
900
|
+
const captions = document.querySelectorAll("caption.caption");
|
|
901
|
+
expect(captions.length).toBe(0);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
test("selected_index clamped when value shrinks", async () => {
|
|
905
|
+
// NOTE: Gallery.svelte has a bug in scroll_to_img where el[index]
|
|
906
|
+
// can become null after value shrinks, causing an unhandled rejection.
|
|
907
|
+
// We test clamping via get_data to avoid triggering the scroll effect.
|
|
908
|
+
const { set_data, get_data } = await render(Gallery, {
|
|
909
|
+
...default_props,
|
|
910
|
+
preview: true,
|
|
911
|
+
selected_index: 2,
|
|
912
|
+
allow_preview: false,
|
|
913
|
+
value: three_images
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
// Shrink to 2 items — selected_index should clamp from 2 to 1
|
|
917
|
+
await set_data({ value: [img("a"), img("b")] });
|
|
918
|
+
|
|
919
|
+
const data = await get_data();
|
|
920
|
+
expect(data.selected_index).toBe(1);
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
test("clicking close button hides preview and shows grid only", async () => {
|
|
924
|
+
const { getByRole, queryByTestId } = await render(Gallery, {
|
|
925
|
+
...default_props,
|
|
926
|
+
preview: true,
|
|
927
|
+
selected_index: 0,
|
|
928
|
+
value: three_images
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
expect(queryByTestId("detailed-image")).toBeVisible();
|
|
932
|
+
|
|
933
|
+
await fireEvent.click(getByRole("button", { name: "Close" }));
|
|
934
|
+
|
|
935
|
+
expect(queryByTestId("detailed-image")).not.toBeInTheDocument();
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("clicking thumbnail in preview switches selection", async () => {
|
|
939
|
+
const { getByTestId, getAllByRole } = await render(Gallery, {
|
|
940
|
+
...default_props,
|
|
941
|
+
preview: true,
|
|
942
|
+
selected_index: 0,
|
|
943
|
+
value: three_images
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// In preview mode, both the preview strip and the grid render buttons
|
|
947
|
+
// with the same aria-label. The preview strip thumbnails appear first
|
|
948
|
+
// in DOM order (inside data-testid="container_el").
|
|
949
|
+
const thumb3_buttons = getAllByRole("button", {
|
|
950
|
+
name: "Thumbnail 3 of 3"
|
|
951
|
+
});
|
|
952
|
+
await fireEvent.click(thumb3_buttons[0]);
|
|
953
|
+
|
|
954
|
+
const detail = getByTestId("detailed-image") as HTMLImageElement;
|
|
955
|
+
expect(detail.src).toBe("https://example.com/img3.png");
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
describe("Regression: #13170 — selected_index points to newly appended image", () => {
|
|
960
|
+
afterEach(() => cleanup());
|
|
961
|
+
|
|
962
|
+
test("selected_index pointing to a newly appended last item shows the correct image", async () => {
|
|
963
|
+
const initial_value = [img("img1"), img("img2")];
|
|
964
|
+
|
|
965
|
+
const { set_data, getByTestId } = await render(Gallery, {
|
|
966
|
+
...default_props,
|
|
967
|
+
preview: true,
|
|
968
|
+
selected_index: 0,
|
|
969
|
+
value: initial_value
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
const updated_value = [...initial_value, img("img3")];
|
|
973
|
+
await set_data({ value: updated_value, selected_index: 2 });
|
|
974
|
+
|
|
975
|
+
const preview_img = getByTestId("detailed-image") as HTMLImageElement;
|
|
976
|
+
expect(preview_img.src).toBe("https://example.com/img3.png");
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
test("selected_index pointing to newly appended item dispatches select with correct data", async () => {
|
|
980
|
+
const initial_value = [img("a"), img("b")];
|
|
981
|
+
|
|
982
|
+
const { set_data, listen } = await render(Gallery, {
|
|
983
|
+
...default_props,
|
|
984
|
+
preview: true,
|
|
985
|
+
selected_index: 0,
|
|
986
|
+
value: initial_value
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
const select = listen("select");
|
|
990
|
+
|
|
991
|
+
await set_data({
|
|
992
|
+
value: [...initial_value, img("c")],
|
|
993
|
+
selected_index: 2
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
expect(select).toHaveBeenCalled();
|
|
997
|
+
const last_call = select.mock.calls[select.mock.calls.length - 1][0];
|
|
998
|
+
expect(last_call.index).toBe(2);
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
test("rapidly appending images with selected_index tracking last always shows the latest", async () => {
|
|
1002
|
+
const { set_data, getByTestId } = await render(Gallery, {
|
|
1003
|
+
...default_props,
|
|
1004
|
+
preview: true,
|
|
1005
|
+
selected_index: 0,
|
|
1006
|
+
value: [img("img1")]
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
for (let i = 2; i <= 5; i++) {
|
|
1010
|
+
const images = Array.from({ length: i }, (_, j) => img(`img${j + 1}`));
|
|
1011
|
+
await set_data({ value: images, selected_index: i - 1 });
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const preview_img = getByTestId("detailed-image") as HTMLImageElement;
|
|
1015
|
+
expect(preview_img.src).toBe("https://example.com/img5.png");
|
|
146
1016
|
});
|
|
147
1017
|
});
|
package/Index.svelte
CHANGED
|
@@ -37,12 +37,10 @@
|
|
|
37
37
|
|
|
38
38
|
let fullscreen = $state(false);
|
|
39
39
|
|
|
40
|
-
function handle_delete(
|
|
41
|
-
event: CustomEvent<{ file: FileData; index: number }>
|
|
42
|
-
): void {
|
|
40
|
+
function handle_delete(data: { file: FileData; index: number }): void {
|
|
43
41
|
if (!gradio.props.value) return;
|
|
44
|
-
const { index } =
|
|
45
|
-
gradio.dispatch("delete",
|
|
42
|
+
const { index } = data;
|
|
43
|
+
gradio.dispatch("delete", data);
|
|
46
44
|
gradio.props.value = gradio.props.value.filter((_, i) => i !== index);
|
|
47
45
|
gradio.dispatch("change", gradio.props.value);
|
|
48
46
|
}
|
|
@@ -309,7 +307,7 @@
|
|
|
309
307
|
(btn) => typeof btn === "string" && btn === "fullscreen"
|
|
310
308
|
)}
|
|
311
309
|
buttons={gradio.props.buttons}
|
|
312
|
-
|
|
310
|
+
oncustom_button_click={(id) => {
|
|
313
311
|
gradio.dispatch("custom_button_click", { id });
|
|
314
312
|
}}
|
|
315
313
|
{fullscreen}
|
package/dist/Index.svelte
CHANGED
|
@@ -37,12 +37,10 @@
|
|
|
37
37
|
|
|
38
38
|
let fullscreen = $state(false);
|
|
39
39
|
|
|
40
|
-
function handle_delete(
|
|
41
|
-
event: CustomEvent<{ file: FileData; index: number }>
|
|
42
|
-
): void {
|
|
40
|
+
function handle_delete(data: { file: FileData; index: number }): void {
|
|
43
41
|
if (!gradio.props.value) return;
|
|
44
|
-
const { index } =
|
|
45
|
-
gradio.dispatch("delete",
|
|
42
|
+
const { index } = data;
|
|
43
|
+
gradio.dispatch("delete", data);
|
|
46
44
|
gradio.props.value = gradio.props.value.filter((_, i) => i !== index);
|
|
47
45
|
gradio.dispatch("change", gradio.props.value);
|
|
48
46
|
}
|
|
@@ -309,7 +307,7 @@
|
|
|
309
307
|
(btn) => typeof btn === "string" && btn === "fullscreen"
|
|
310
308
|
)}
|
|
311
309
|
buttons={gradio.props.buttons}
|
|
312
|
-
|
|
310
|
+
oncustom_button_click={(id) => {
|
|
313
311
|
gradio.dispatch("custom_button_click", { id });
|
|
314
312
|
}}
|
|
315
313
|
{fullscreen}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradio/gallery",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.7",
|
|
4
4
|
"description": "Gradio UI packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
"private": false,
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"dequal": "^2.0.3",
|
|
11
|
-
"@gradio/atoms": "^0.
|
|
12
|
-
"@gradio/
|
|
13
|
-
"@gradio/client": "^2.1.0",
|
|
14
|
-
"@gradio/image": "^0.26.1",
|
|
15
|
-
"@gradio/statustracker": "^0.13.1",
|
|
11
|
+
"@gradio/atoms": "^0.24.0",
|
|
12
|
+
"@gradio/client": "^2.2.0",
|
|
16
13
|
"@gradio/icons": "^0.15.1",
|
|
14
|
+
"@gradio/file": "^0.14.8",
|
|
17
15
|
"@gradio/utils": "^0.12.2",
|
|
18
|
-
"@gradio/
|
|
19
|
-
"@gradio/upload": "^0.17.
|
|
16
|
+
"@gradio/image": "^0.26.3",
|
|
17
|
+
"@gradio/upload": "^0.17.9",
|
|
18
|
+
"@gradio/video": "^0.20.8",
|
|
19
|
+
"@gradio/statustracker": "^0.14.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@gradio/preview": "^0.16.2"
|