@gradio/imageeditor 0.20.1 → 0.21.0
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 +46 -0
- package/ImageEditor.test.ts +715 -0
- package/ImageEditorTestWrapper.svelte +81 -0
- package/Index.svelte +0 -2
- package/InteractiveImageEditor.svelte +64 -15
- package/dist/ImageEditorTestWrapper.svelte +81 -0
- package/dist/ImageEditorTestWrapper.svelte.d.ts +11 -0
- package/dist/Index.svelte +0 -2
- package/dist/InteractiveImageEditor.svelte +64 -15
- package/dist/shared/Controls.svelte +12 -9
- package/dist/shared/Controls.svelte.d.ts +2 -0
- package/dist/shared/ImageEditor.svelte +25 -9
- package/dist/shared/ImageEditor.svelte.d.ts +2 -1
- package/dist/shared/Layers.svelte +18 -5
- package/dist/shared/Layers.svelte.d.ts +2 -0
- package/dist/shared/Resize.svelte +1 -1
- package/dist/shared/SecondaryToolbar.svelte +4 -0
- package/dist/shared/SecondaryToolbar.svelte.d.ts +2 -0
- package/dist/shared/Toolbar.svelte +15 -12
- package/dist/shared/Toolbar.svelte.d.ts +2 -0
- package/dist/shared/brush/brush-textures.js +8 -24
- package/dist/shared/resize/resize.js +10 -1
- package/package.json +8 -8
- package/shared/Controls.svelte +12 -9
- package/shared/ImageEditor.svelte +25 -9
- package/shared/Layers.svelte +18 -5
- package/shared/Resize.svelte +1 -1
- package/shared/SecondaryToolbar.svelte +4 -0
- package/shared/Toolbar.svelte +15 -12
- package/shared/brush/brush-textures.test.ts +77 -0
- package/shared/brush/brush-textures.ts +8 -27
- package/shared/resize/resize.ts +9 -1
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
cleanup,
|
|
4
|
+
fireEvent,
|
|
5
|
+
mock_client,
|
|
6
|
+
render,
|
|
7
|
+
TEST_PNG,
|
|
8
|
+
waitFor
|
|
9
|
+
} from "@self/tootils/render";
|
|
10
|
+
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
|
|
11
|
+
|
|
12
|
+
import es from "../core/src/lang/es.json";
|
|
13
|
+
import ImageEditor from "./Index.svelte";
|
|
14
|
+
import ImageEditorTestWrapper from "./ImageEditorTestWrapper.svelte";
|
|
15
|
+
|
|
16
|
+
const default_props = {
|
|
17
|
+
sources: ["upload"] as const,
|
|
18
|
+
interactive: true,
|
|
19
|
+
label: "Image Editor",
|
|
20
|
+
show_label: true,
|
|
21
|
+
value: null,
|
|
22
|
+
canvas_size: [800, 600] as [number, number],
|
|
23
|
+
transforms: [] as const,
|
|
24
|
+
layers: { allow_additional_layers: true, layers: ["Layer 1"] },
|
|
25
|
+
brush: {
|
|
26
|
+
default_size: "auto" as const,
|
|
27
|
+
colors: ["#000000"],
|
|
28
|
+
default_color: "#000000",
|
|
29
|
+
color_mode: "defaults" as const
|
|
30
|
+
},
|
|
31
|
+
eraser: { default_size: "auto" as const },
|
|
32
|
+
webcam_options: { mirror: true, constraints: {} },
|
|
33
|
+
buttons: []
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const spanish_i18n = (key: string): string => {
|
|
37
|
+
const [namespace, message] = key.split(".");
|
|
38
|
+
return (
|
|
39
|
+
(es as Record<string, Record<string, string>>)[namespace]?.[message] ?? key
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
run_shared_prop_tests({
|
|
44
|
+
component: ImageEditor,
|
|
45
|
+
name: "ImageEditor",
|
|
46
|
+
base_props: { ...default_props, interactive: false },
|
|
47
|
+
has_label: false,
|
|
48
|
+
has_validation_error: true
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("get_data / set_data", () => {
|
|
52
|
+
afterEach(() => cleanup());
|
|
53
|
+
|
|
54
|
+
test("an untouched editor uploads the original image bytes", async () => {
|
|
55
|
+
const background = { ...TEST_PNG, orig_name: "background.png" };
|
|
56
|
+
const layer = { ...TEST_PNG, orig_name: "layer.png" };
|
|
57
|
+
const composite = { ...TEST_PNG, orig_name: "composite.png" };
|
|
58
|
+
const value = { background, layers: [layer], composite };
|
|
59
|
+
const { getByRole, get_data, listen } = await render(ImageEditor, {
|
|
60
|
+
...default_props,
|
|
61
|
+
client: mock_client(),
|
|
62
|
+
value
|
|
63
|
+
});
|
|
64
|
+
const uploaded = listen("upload", { retrospective: true });
|
|
65
|
+
await waitFor(
|
|
66
|
+
() => {
|
|
67
|
+
expect(getByRole("button", { name: "image_editor.pan" })).toBeVisible();
|
|
68
|
+
expect(uploaded).toHaveBeenCalled();
|
|
69
|
+
},
|
|
70
|
+
{ timeout: 5000 }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result = (await get_data()).value;
|
|
74
|
+
const original_bytes = await (await fetch(TEST_PNG.url!)).arrayBuffer();
|
|
75
|
+
|
|
76
|
+
expect(result.background.orig_name).toBe("background.png");
|
|
77
|
+
expect(result.layers[0].orig_name).toBe("layer.png");
|
|
78
|
+
expect(result.composite.orig_name).toBe("composite.png");
|
|
79
|
+
expect(await result.background.blob.arrayBuffer()).toEqual(original_bytes);
|
|
80
|
+
expect(await result.layers[0].blob.arrayBuffer()).toEqual(original_bytes);
|
|
81
|
+
expect(await result.composite.blob.arrayBuffer()).toEqual(original_bytes);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("Internationalization", () => {
|
|
86
|
+
afterEach(() => cleanup());
|
|
87
|
+
|
|
88
|
+
test("canvas controls use translated accessible names", async () => {
|
|
89
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
90
|
+
kind: "controls",
|
|
91
|
+
i18n: spanish_i18n
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(getByRole("button", { name: "Descargar" })).toBeVisible();
|
|
95
|
+
expect(getByRole("button", { name: "Desplazar" })).toBeVisible();
|
|
96
|
+
expect(getByRole("button", { name: "Alejar" })).toBeVisible();
|
|
97
|
+
expect(getByRole("button", { name: "Deshacer" })).toBeVisible();
|
|
98
|
+
expect(getByRole("button", { name: "Guardar cambios" })).toBeVisible();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("editing tools use translated accessible names", async () => {
|
|
102
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
103
|
+
kind: "toolbar",
|
|
104
|
+
i18n: spanish_i18n
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(getByRole("button", { name: "Imagen" })).toBeVisible();
|
|
108
|
+
expect(getByRole("button", { name: "Pincel" })).toBeVisible();
|
|
109
|
+
expect(getByRole("button", { name: "Borrar" })).toBeVisible();
|
|
110
|
+
expect(getByRole("button", { name: "Cargar" })).toBeVisible();
|
|
111
|
+
expect(getByRole("button", { name: "Pegar" })).toBeVisible();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("layer controls use translated accessible names", async () => {
|
|
115
|
+
const { getByRole, getByText } = await render(ImageEditorTestWrapper, {
|
|
116
|
+
kind: "layers",
|
|
117
|
+
i18n: spanish_i18n
|
|
118
|
+
});
|
|
119
|
+
const show_layers = getByRole("button", {
|
|
120
|
+
name: "Mostrar capas"
|
|
121
|
+
});
|
|
122
|
+
expect(getByText("Capa 1")).toBeVisible();
|
|
123
|
+
|
|
124
|
+
await fireEvent.click(show_layers);
|
|
125
|
+
|
|
126
|
+
expect(getByText("Capas")).toBeVisible();
|
|
127
|
+
expect(getByRole("button", { name: "Añadir capa" })).toBeVisible();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("Static mode", () => {
|
|
132
|
+
afterEach(() => cleanup());
|
|
133
|
+
|
|
134
|
+
const static_props = { ...default_props, interactive: false };
|
|
135
|
+
|
|
136
|
+
test("renders the composite image", async () => {
|
|
137
|
+
const { getByRole } = await render(ImageEditor, {
|
|
138
|
+
...static_props,
|
|
139
|
+
value: { background: null, layers: [], composite: TEST_PNG }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// The static branch renders the composite through StaticImage without any
|
|
143
|
+
// alt text, so the <img> is exposed as decorative rather than role="img".
|
|
144
|
+
await waitFor(() => expect(getByRole("presentation")).toBeVisible());
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("renders no editing toolbar", async () => {
|
|
148
|
+
const { queryByRole } = await render(ImageEditor, {
|
|
149
|
+
...static_props,
|
|
150
|
+
value: { background: null, layers: [], composite: TEST_PNG }
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(
|
|
154
|
+
queryByRole("button", { name: "image_editor.brush" })
|
|
155
|
+
).not.toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("renders the label", async () => {
|
|
159
|
+
const { getByText } = await render(ImageEditor, {
|
|
160
|
+
...static_props,
|
|
161
|
+
label: "Sketch pad",
|
|
162
|
+
show_label: true,
|
|
163
|
+
value: { background: null, layers: [], composite: TEST_PNG }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await waitFor(() => expect(getByText("Sketch pad")).toBeVisible());
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("renders an empty component when there is no value", async () => {
|
|
170
|
+
const { queryByRole } = await render(ImageEditor, {
|
|
171
|
+
...static_props,
|
|
172
|
+
value: null
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(queryByRole("presentation")).not.toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("clear_status is dispatched when the error status is dismissed", async () => {
|
|
179
|
+
const { listen, getByLabelText } = await render(ImageEditor, {
|
|
180
|
+
...static_props,
|
|
181
|
+
loading_status: {
|
|
182
|
+
status: "error",
|
|
183
|
+
queue_position: null,
|
|
184
|
+
queue_size: null,
|
|
185
|
+
eta: null,
|
|
186
|
+
message: "it broke",
|
|
187
|
+
show_progress: "full",
|
|
188
|
+
scroll_to_output: false,
|
|
189
|
+
visible: true,
|
|
190
|
+
fn_index: 0
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const clear_status = listen("clear_status");
|
|
194
|
+
|
|
195
|
+
await fireEvent.click(getByLabelText("common.clear"));
|
|
196
|
+
|
|
197
|
+
expect(clear_status).toHaveBeenCalledTimes(1);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("Interactive mode", () => {
|
|
202
|
+
afterEach(() => cleanup());
|
|
203
|
+
|
|
204
|
+
async function render_editor(
|
|
205
|
+
overrides: Record<string, any> = {}
|
|
206
|
+
): Promise<Awaited<ReturnType<typeof render>>> {
|
|
207
|
+
const view = await render(ImageEditor, {
|
|
208
|
+
...default_props,
|
|
209
|
+
client: mock_client(),
|
|
210
|
+
...overrides
|
|
211
|
+
});
|
|
212
|
+
await waitFor(
|
|
213
|
+
() => {
|
|
214
|
+
expect(
|
|
215
|
+
view.getByRole("button", { name: "image_editor.pan" })
|
|
216
|
+
).toBeVisible();
|
|
217
|
+
},
|
|
218
|
+
{ timeout: 5000 }
|
|
219
|
+
);
|
|
220
|
+
return view;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test("renders the canvas controls", async () => {
|
|
224
|
+
const { getByRole } = await render_editor();
|
|
225
|
+
|
|
226
|
+
expect(getByRole("button", { name: "image_editor.pan" })).toBeVisible();
|
|
227
|
+
expect(
|
|
228
|
+
getByRole("button", { name: "image_editor.zoom_out" })
|
|
229
|
+
).toBeVisible();
|
|
230
|
+
expect(getByRole("button", { name: "common.undo" })).toBeVisible();
|
|
231
|
+
expect(
|
|
232
|
+
getByRole("button", { name: "image_editor.clear_canvas" })
|
|
233
|
+
).toBeVisible();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("renders the drawing tools", async () => {
|
|
237
|
+
const { getByRole } = await render_editor();
|
|
238
|
+
|
|
239
|
+
expect(getByRole("button", { name: "image_editor.brush" })).toBeVisible();
|
|
240
|
+
expect(getByRole("button", { name: "image_editor.erase" })).toBeVisible();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("undo and redo start disabled on a fresh canvas", async () => {
|
|
244
|
+
const { getByRole } = await render_editor();
|
|
245
|
+
|
|
246
|
+
expect(getByRole("button", { name: "common.undo" })).toBeDisabled();
|
|
247
|
+
expect(getByRole("button", { name: "image_editor.redo" })).toBeDisabled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("renders the label", async () => {
|
|
251
|
+
const { getByText } = await render_editor({
|
|
252
|
+
label: "Sketch pad",
|
|
253
|
+
show_label: true
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(getByText("Sketch pad")).toBeVisible();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("the download button is shown by default", async () => {
|
|
260
|
+
const { getByRole } = await render_editor({ buttons: null });
|
|
261
|
+
|
|
262
|
+
expect(getByRole("button", { name: "common.download" })).toBeVisible();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("an empty buttons list hides the download button", async () => {
|
|
266
|
+
const { queryByRole } = await render_editor({ buttons: [] });
|
|
267
|
+
|
|
268
|
+
expect(
|
|
269
|
+
queryByRole("button", { name: "common.download" })
|
|
270
|
+
).not.toBeInTheDocument();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("buttons: ['download'] shows the download button", async () => {
|
|
274
|
+
const { getByRole } = await render_editor({ buttons: ["download"] });
|
|
275
|
+
|
|
276
|
+
expect(getByRole("button", { name: "common.download" })).toBeVisible();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("the save button only appears when an apply listener is attached", async () => {
|
|
280
|
+
const { queryByRole } = await render_editor({ attached_events: [] });
|
|
281
|
+
|
|
282
|
+
expect(
|
|
283
|
+
queryByRole("button", { name: "image_editor.save_changes" })
|
|
284
|
+
).not.toBeInTheDocument();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("attaching an apply listener shows the save button", async () => {
|
|
288
|
+
const { getByRole } = await render_editor({ attached_events: ["apply"] });
|
|
289
|
+
|
|
290
|
+
expect(
|
|
291
|
+
getByRole("button", { name: "image_editor.save_changes" })
|
|
292
|
+
).toBeVisible();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("Toolbar: sources", () => {
|
|
297
|
+
afterEach(() => cleanup());
|
|
298
|
+
|
|
299
|
+
test("all three sources render their own button", async () => {
|
|
300
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
301
|
+
kind: "toolbar",
|
|
302
|
+
sources: ["upload", "clipboard", "webcam"]
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(getByRole("button", { name: "image_editor.upload" })).toBeVisible();
|
|
306
|
+
expect(getByRole("button", { name: "image_editor.paste" })).toBeVisible();
|
|
307
|
+
expect(getByRole("button", { name: "image_editor.webcam" })).toBeVisible();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("only the listed sources are offered", async () => {
|
|
311
|
+
const { getByRole, queryByRole } = await render(ImageEditorTestWrapper, {
|
|
312
|
+
kind: "toolbar",
|
|
313
|
+
sources: ["upload"]
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(getByRole("button", { name: "image_editor.upload" })).toBeVisible();
|
|
317
|
+
expect(
|
|
318
|
+
queryByRole("button", { name: "image_editor.webcam" })
|
|
319
|
+
).not.toBeInTheDocument();
|
|
320
|
+
expect(
|
|
321
|
+
queryByRole("button", { name: "image_editor.paste" })
|
|
322
|
+
).not.toBeInTheDocument();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("no sources and no transforms hides the image tool entirely", async () => {
|
|
326
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
327
|
+
kind: "toolbar",
|
|
328
|
+
sources: [],
|
|
329
|
+
transforms: []
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(
|
|
333
|
+
queryByRole("button", { name: "image.image" })
|
|
334
|
+
).not.toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("a transform keeps the image tool available once there is a background", async () => {
|
|
338
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
339
|
+
kind: "toolbar",
|
|
340
|
+
sources: [],
|
|
341
|
+
transforms: ["crop"],
|
|
342
|
+
background: true
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(getByRole("button", { name: "image.image" })).toBeVisible();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("Toolbar: transforms", () => {
|
|
350
|
+
afterEach(() => cleanup());
|
|
351
|
+
|
|
352
|
+
test("crop and resize are offered for an image with a background", async () => {
|
|
353
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
354
|
+
kind: "toolbar",
|
|
355
|
+
transforms: ["crop", "resize"],
|
|
356
|
+
background: true
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(getByRole("button", { name: "image_editor.crop" })).toBeVisible();
|
|
360
|
+
expect(getByRole("button", { name: "image_editor.resize" })).toBeVisible();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("only the listed transforms are offered", async () => {
|
|
364
|
+
const { getByRole, queryByRole } = await render(ImageEditorTestWrapper, {
|
|
365
|
+
kind: "toolbar",
|
|
366
|
+
transforms: ["crop"],
|
|
367
|
+
background: true
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(getByRole("button", { name: "image_editor.crop" })).toBeVisible();
|
|
371
|
+
expect(
|
|
372
|
+
queryByRole("button", { name: "image_editor.resize" })
|
|
373
|
+
).not.toBeInTheDocument();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("transforms are not offered until there is a background to transform", async () => {
|
|
377
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
378
|
+
kind: "toolbar",
|
|
379
|
+
transforms: ["crop", "resize"],
|
|
380
|
+
background: false
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(
|
|
384
|
+
queryByRole("button", { name: "image_editor.crop" })
|
|
385
|
+
).not.toBeInTheDocument();
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("Toolbar: brush and eraser", () => {
|
|
390
|
+
afterEach(() => cleanup());
|
|
391
|
+
|
|
392
|
+
test("brush: false removes the brush tool", async () => {
|
|
393
|
+
const { queryByRole, getByRole } = await render(ImageEditorTestWrapper, {
|
|
394
|
+
kind: "toolbar",
|
|
395
|
+
brush_options: false
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(
|
|
399
|
+
queryByRole("button", { name: "image_editor.brush" })
|
|
400
|
+
).not.toBeInTheDocument();
|
|
401
|
+
expect(getByRole("button", { name: "image_editor.erase" })).toBeVisible();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("eraser: false removes the eraser tool", async () => {
|
|
405
|
+
const { queryByRole, getByRole } = await render(ImageEditorTestWrapper, {
|
|
406
|
+
kind: "toolbar",
|
|
407
|
+
eraser_options: false
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(
|
|
411
|
+
queryByRole("button", { name: "image_editor.erase" })
|
|
412
|
+
).not.toBeInTheDocument();
|
|
413
|
+
expect(getByRole("button", { name: "image_editor.brush" })).toBeVisible();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("clicking the brush asks the editor to switch to the draw tool", async () => {
|
|
417
|
+
const ontool_change = vi.fn();
|
|
418
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
419
|
+
kind: "toolbar",
|
|
420
|
+
ontool_change
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await fireEvent.click(getByRole("button", { name: "image_editor.brush" }));
|
|
424
|
+
|
|
425
|
+
expect(ontool_change).toHaveBeenCalledWith({ tool: "draw" });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("clicking the eraser asks the editor to switch to the erase tool", async () => {
|
|
429
|
+
const ontool_change = vi.fn();
|
|
430
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
431
|
+
kind: "toolbar",
|
|
432
|
+
ontool_change
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
await fireEvent.click(getByRole("button", { name: "image_editor.erase" }));
|
|
436
|
+
|
|
437
|
+
expect(ontool_change).toHaveBeenCalledWith({ tool: "erase" });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("the draw tool offers colour and size controls", async () => {
|
|
441
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
442
|
+
kind: "toolbar",
|
|
443
|
+
tool: "draw"
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(getByRole("button", { name: "image_editor.color" })).toBeVisible();
|
|
447
|
+
expect(
|
|
448
|
+
getByRole("button", { name: "image_editor.brush_size" })
|
|
449
|
+
).toBeVisible();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("the erase tool offers a size control", async () => {
|
|
453
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
454
|
+
kind: "toolbar",
|
|
455
|
+
tool: "erase"
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(
|
|
459
|
+
getByRole("button", { name: "image_editor.eraser_size" })
|
|
460
|
+
).toBeVisible();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test("choosing the brush size sub-tool is reported to the editor", async () => {
|
|
464
|
+
const onsubtool_change = vi.fn();
|
|
465
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
466
|
+
kind: "toolbar",
|
|
467
|
+
tool: "draw",
|
|
468
|
+
onsubtool_change
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await fireEvent.click(
|
|
472
|
+
getByRole("button", { name: "image_editor.brush_size" })
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
expect(onsubtool_change).toHaveBeenCalledWith({
|
|
476
|
+
tool: "draw",
|
|
477
|
+
subtool: "size"
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe("Controls", () => {
|
|
483
|
+
afterEach(() => cleanup());
|
|
484
|
+
|
|
485
|
+
test("undo is disabled until there is something to undo", async () => {
|
|
486
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
487
|
+
kind: "controls",
|
|
488
|
+
can_undo: false
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
expect(getByRole("button", { name: "common.undo" })).toBeDisabled();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("undo is enabled when there is something to undo", async () => {
|
|
495
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
496
|
+
kind: "controls",
|
|
497
|
+
can_undo: true
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
expect(getByRole("button", { name: "common.undo" })).toBeEnabled();
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("clicking undo calls back", async () => {
|
|
504
|
+
const onundo = vi.fn();
|
|
505
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
506
|
+
kind: "controls",
|
|
507
|
+
can_undo: true,
|
|
508
|
+
onundo
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await fireEvent.click(getByRole("button", { name: "common.undo" }));
|
|
512
|
+
|
|
513
|
+
expect(onundo).toHaveBeenCalledTimes(1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("clicking redo calls back", async () => {
|
|
517
|
+
const onredo = vi.fn();
|
|
518
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
519
|
+
kind: "controls",
|
|
520
|
+
can_redo: true,
|
|
521
|
+
onredo
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
await fireEvent.click(getByRole("button", { name: "image_editor.redo" }));
|
|
525
|
+
|
|
526
|
+
expect(onredo).toHaveBeenCalledTimes(1);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("clicking clear calls back", async () => {
|
|
530
|
+
const onremove_image = vi.fn();
|
|
531
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
532
|
+
kind: "controls",
|
|
533
|
+
onremove_image
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
await fireEvent.click(
|
|
537
|
+
getByRole("button", { name: "image_editor.clear_canvas" })
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
expect(onremove_image).toHaveBeenCalledTimes(1);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test("enable_download: false removes the download button", async () => {
|
|
544
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
545
|
+
kind: "controls",
|
|
546
|
+
enable_download: false
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
expect(
|
|
550
|
+
queryByRole("button", { name: "common.download" })
|
|
551
|
+
).not.toBeInTheDocument();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("changeable: false removes the save button", async () => {
|
|
555
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
556
|
+
kind: "controls",
|
|
557
|
+
changeable: false
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expect(
|
|
561
|
+
queryByRole("button", { name: "image_editor.save_changes" })
|
|
562
|
+
).not.toBeInTheDocument();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("save is disabled until there are changes to save", async () => {
|
|
566
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
567
|
+
kind: "controls",
|
|
568
|
+
changeable: true,
|
|
569
|
+
can_save: false
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
expect(
|
|
573
|
+
getByRole("button", { name: "image_editor.save_changes" })
|
|
574
|
+
).toBeDisabled();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test("clicking save calls back", async () => {
|
|
578
|
+
const onsave = vi.fn();
|
|
579
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
580
|
+
kind: "controls",
|
|
581
|
+
changeable: true,
|
|
582
|
+
can_save: true,
|
|
583
|
+
onsave
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
await fireEvent.click(
|
|
587
|
+
getByRole("button", { name: "image_editor.save_changes" })
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
expect(onsave).toHaveBeenCalledTimes(1);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("clicking zoom out calls back", async () => {
|
|
594
|
+
const onzoom_out = vi.fn();
|
|
595
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
596
|
+
kind: "controls",
|
|
597
|
+
onzoom_out
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
await fireEvent.click(
|
|
601
|
+
getByRole("button", { name: "image_editor.zoom_out" })
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
expect(onzoom_out).toHaveBeenCalledTimes(1);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("clicking pan calls back", async () => {
|
|
608
|
+
const onpan = vi.fn();
|
|
609
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
610
|
+
kind: "controls",
|
|
611
|
+
onpan
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
await fireEvent.click(getByRole("button", { name: "image_editor.pan" }));
|
|
615
|
+
|
|
616
|
+
expect(onpan).toHaveBeenCalledTimes(1);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
describe("Layers", () => {
|
|
621
|
+
afterEach(() => cleanup());
|
|
622
|
+
|
|
623
|
+
const named_layers = {
|
|
624
|
+
active_layer: "layer-1",
|
|
625
|
+
layers: [
|
|
626
|
+
{ name: "Background", id: "layer-1", user_created: false, visible: true },
|
|
627
|
+
{ name: "Sketch", id: "layer-2", user_created: true, visible: true }
|
|
628
|
+
]
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
test("the active layer is named in the toolbar", async () => {
|
|
632
|
+
const { getByText } = await render(ImageEditorTestWrapper, {
|
|
633
|
+
kind: "layers",
|
|
634
|
+
layer_store: named_layers
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
expect(getByText("Background")).toBeVisible();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("opening the layer panel lists every layer", async () => {
|
|
641
|
+
const { getByRole, getAllByText } = await render(ImageEditorTestWrapper, {
|
|
642
|
+
kind: "layers",
|
|
643
|
+
layer_store: named_layers
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
await fireEvent.click(
|
|
647
|
+
getByRole("button", { name: "image_editor.show_layers" })
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
expect(getAllByText("Sketch").length).toBeGreaterThan(0);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("auto-generated layer names are translated", async () => {
|
|
654
|
+
const { getByText } = await render(ImageEditorTestWrapper, {
|
|
655
|
+
kind: "layers",
|
|
656
|
+
i18n: spanish_i18n
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
expect(getByText("Capa 1")).toBeVisible();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("a user-supplied layer name is shown verbatim", async () => {
|
|
663
|
+
const { getByText } = await render(ImageEditorTestWrapper, {
|
|
664
|
+
kind: "layers",
|
|
665
|
+
i18n: spanish_i18n,
|
|
666
|
+
layer_store: named_layers
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(getByText("Background")).toBeVisible();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("a new layer can be added when additional layers are allowed", async () => {
|
|
673
|
+
const onnew_layer = vi.fn();
|
|
674
|
+
const { getByRole } = await render(ImageEditorTestWrapper, {
|
|
675
|
+
kind: "layers",
|
|
676
|
+
enable_additional_layers: true,
|
|
677
|
+
onnew_layer
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
await fireEvent.click(
|
|
681
|
+
getByRole("button", { name: "image_editor.show_layers" })
|
|
682
|
+
);
|
|
683
|
+
await fireEvent.click(
|
|
684
|
+
getByRole("button", { name: "image_editor.add_layer" })
|
|
685
|
+
);
|
|
686
|
+
|
|
687
|
+
expect(onnew_layer).toHaveBeenCalledTimes(1);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("the add layer button is hidden when additional layers are disallowed", async () => {
|
|
691
|
+
const { getByRole, queryByRole } = await render(ImageEditorTestWrapper, {
|
|
692
|
+
kind: "layers",
|
|
693
|
+
enable_additional_layers: false
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
await fireEvent.click(
|
|
697
|
+
getByRole("button", { name: "image_editor.show_layers" })
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
expect(
|
|
701
|
+
queryByRole("button", { name: "image_editor.add_layer" })
|
|
702
|
+
).not.toBeInTheDocument();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("enable_layers: false renders no layer controls at all", async () => {
|
|
706
|
+
const { queryByRole } = await render(ImageEditorTestWrapper, {
|
|
707
|
+
kind: "layers",
|
|
708
|
+
enable_layers: false
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
expect(
|
|
712
|
+
queryByRole("button", { name: "image_editor.show_layers" })
|
|
713
|
+
).not.toBeInTheDocument();
|
|
714
|
+
});
|
|
715
|
+
});
|