@jskit-ai/uploads-image-web 0.1.1 → 0.1.3

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/uploads-image-web",
4
- version: "0.1.1",
4
+ version: "0.1.3",
5
5
  kind: "runtime",
6
6
  description: "Reusable client-side image upload runtime with pre-upload image editing.",
7
7
  dependsOn: [
@@ -43,6 +43,10 @@ export default Object.freeze({
43
43
  subpath: "./client/composables/createImageUploadRuntime",
44
44
  summary: "Exports the reusable image upload runtime factory."
45
45
  },
46
+ {
47
+ subpath: "./client/composables/createManagedImageAssetRuntime",
48
+ summary: "Exports the managed image asset runtime factory for upload and optional delete flows."
49
+ },
46
50
  {
47
51
  subpath: "./client/styles",
48
52
  summary: "Exports Uppy CSS side effects for image upload UIs."
@@ -61,7 +65,7 @@ export default Object.freeze({
61
65
  mutations: {
62
66
  dependencies: {
63
67
  runtime: {
64
- "@jskit-ai/uploads-runtime": "0.1.1",
68
+ "@jskit-ai/uploads-runtime": "0.1.3",
65
69
  "@uppy/compressor": "^3.1.0",
66
70
  "@uppy/core": "^5.2.0",
67
71
  "@uppy/dashboard": "^5.1.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/uploads-image-web",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -8,11 +8,12 @@
8
8
  "exports": {
9
9
  "./client": "./src/client/index.js",
10
10
  "./client/composables/createImageUploadRuntime": "./src/client/composables/createImageUploadRuntime.js",
11
+ "./client/composables/createManagedImageAssetRuntime": "./src/client/composables/createManagedImageAssetRuntime.js",
11
12
  "./client/styles": "./src/client/styles/index.js",
12
13
  "./shared": "./src/shared/index.js"
13
14
  },
14
15
  "dependencies": {
15
- "@jskit-ai/uploads-runtime": "0.1.1",
16
+ "@jskit-ai/uploads-runtime": "0.1.3",
16
17
  "@uppy/compressor": "^3.1.0",
17
18
  "@uppy/core": "^5.2.0",
18
19
  "@uppy/dashboard": "^5.1.1",
@@ -0,0 +1,451 @@
1
+ import { computed, ref, unref } from "vue";
2
+ import { createImageUploadRuntime } from "./createImageUploadRuntime.js";
3
+
4
+ const DEFAULT_MANAGED_IMAGE_ASSET_MESSAGES = Object.freeze({
5
+ endpointUnavailable: "Image endpoint is unavailable.",
6
+ uploadSuccess: "Image uploaded.",
7
+ uploadInvalidResponse: "Image uploaded, but the response payload was invalid.",
8
+ uploadError: "Unable to upload image.",
9
+ uploadRestriction: "Selected image does not meet upload restrictions.",
10
+ editorUnavailable: "Image editor is unavailable in this environment.",
11
+ deleteSuccess: "Image removed.",
12
+ deleteError: "Unable to remove image.",
13
+ deleteUnavailable: "Image removal is unavailable.",
14
+ changeError: "Image updated, but the page could not refresh."
15
+ });
16
+
17
+ function normalizeText(value) {
18
+ return String(value || "").trim();
19
+ }
20
+
21
+ function normalizeObject(value) {
22
+ return value && typeof value === "object" ? value : {};
23
+ }
24
+
25
+ function normalizeWritableRef(value, fallbackValue = "") {
26
+ if (value && typeof value === "object" && "value" in value) {
27
+ return value;
28
+ }
29
+
30
+ return ref(normalizeText(fallbackValue));
31
+ }
32
+
33
+ function closeDashboard(uppy) {
34
+ const dashboard = uppy?.getPlugin?.("Dashboard");
35
+ if (!dashboard || typeof dashboard.closeModal !== "function") {
36
+ return;
37
+ }
38
+
39
+ dashboard.closeModal();
40
+ }
41
+
42
+ async function readJsonResponse(response) {
43
+ const contentType = normalizeText(response?.headers?.get?.("content-type")).toLowerCase();
44
+ if (!contentType.includes("application/json")) {
45
+ return null;
46
+ }
47
+
48
+ try {
49
+ return await response.json();
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function resolveMessage(value, fallback) {
56
+ const normalizedValue = normalizeText(value);
57
+ return normalizedValue || fallback;
58
+ }
59
+
60
+ function resolveUploadErrorFallback({ error, response, fallbackMessage }) {
61
+ const body = response?.body && typeof response.body === "object" ? response.body : {};
62
+ return resolveMessage(body?.error || error?.message, fallbackMessage);
63
+ }
64
+
65
+ function resolveDeleteErrorFallback({ error, body, fallbackMessage }) {
66
+ const normalizedBody = body && typeof body === "object" ? body : {};
67
+ return resolveMessage(normalizedBody?.error || error?.message, fallbackMessage);
68
+ }
69
+
70
+ function normalizeDeleteResult(result) {
71
+ if (result && typeof result === "object" && ("body" in result || "response" in result)) {
72
+ return {
73
+ body: result.body ?? null,
74
+ response: result.response ?? null
75
+ };
76
+ }
77
+
78
+ return {
79
+ body: result ?? null,
80
+ response: null
81
+ };
82
+ }
83
+
84
+ function createManagedImageAssetRuntime({
85
+ uploadEndpoint = "",
86
+ deleteEndpoint = "",
87
+ fieldName = "file",
88
+ hasAsset = false,
89
+ assetVersion = "",
90
+ selectedFileName = null,
91
+ resolveAssetUrl = null,
92
+ resolveRequestHeaders = null,
93
+ onUploadSuccess = null,
94
+ onDeleteSuccess = null,
95
+ resolveUploadErrorMessage = null,
96
+ resolveDeleteErrorMessage = null,
97
+ deleteRequest = null,
98
+ reportFeedback = null,
99
+ messages = {},
100
+ uploadOptions = {},
101
+ deleteMethod = "DELETE",
102
+ source = "uploads-image-web.managed-image-asset",
103
+ dependencies = {}
104
+ } = {}) {
105
+ const createImageUploadRuntimeFactory = dependencies.createImageUploadRuntimeFactory || createImageUploadRuntime;
106
+ const fetchImpl = dependencies.fetchImpl || globalThis.fetch?.bind(globalThis);
107
+ const normalizedMessages = {
108
+ ...DEFAULT_MANAGED_IMAGE_ASSET_MESSAGES,
109
+ ...normalizeObject(messages)
110
+ };
111
+ const selectedFileNameRef = normalizeWritableRef(selectedFileName);
112
+ const isDeleting = ref(false);
113
+ const normalizedFieldName = normalizeText(fieldName || "file") || "file";
114
+ const normalizedDeleteMethod = normalizeText(deleteMethod || "DELETE").toUpperCase() || "DELETE";
115
+ const normalizedUploadOptions = normalizeObject(uploadOptions);
116
+
117
+ const normalizedUploadEndpoint = computed(() => normalizeText(unref(uploadEndpoint)));
118
+ const normalizedDeleteEndpoint = computed(() => {
119
+ const explicitDeleteEndpoint = normalizeText(unref(deleteEndpoint));
120
+ return explicitDeleteEndpoint || normalizedUploadEndpoint.value;
121
+ });
122
+ const normalizedHasAsset = computed(() => Boolean(unref(hasAsset)));
123
+ const normalizedAssetVersion = computed(() => normalizeText(unref(assetVersion)));
124
+ const assetUrl = computed(() => {
125
+ if (!normalizedHasAsset.value) {
126
+ return "";
127
+ }
128
+
129
+ if (typeof resolveAssetUrl !== "function") {
130
+ return normalizedUploadEndpoint.value;
131
+ }
132
+
133
+ return normalizeText(
134
+ resolveAssetUrl({
135
+ uploadEndpoint: normalizedUploadEndpoint.value,
136
+ deleteEndpoint: normalizedDeleteEndpoint.value,
137
+ hasAsset: normalizedHasAsset.value,
138
+ assetVersion: normalizedAssetVersion.value,
139
+ fieldName: normalizedFieldName
140
+ })
141
+ );
142
+ });
143
+
144
+ let imageUploadRuntime = null;
145
+ let runtimeUploadEndpoint = "";
146
+
147
+ function report({
148
+ message = "",
149
+ severity = "success",
150
+ channel = "snackbar",
151
+ dedupeKey = ""
152
+ } = {}) {
153
+ const normalizedMessage = normalizeText(message);
154
+ if (!normalizedMessage || typeof reportFeedback !== "function") {
155
+ return;
156
+ }
157
+
158
+ reportFeedback({
159
+ message: normalizedMessage,
160
+ severity,
161
+ channel,
162
+ dedupeKey: normalizeText(dedupeKey) || `${source}:${severity}:${normalizedMessage}`
163
+ });
164
+ }
165
+
166
+ async function resolveHeaders(action, endpoint) {
167
+ if (typeof resolveRequestHeaders !== "function") {
168
+ return {};
169
+ }
170
+
171
+ return normalizeObject(
172
+ await resolveRequestHeaders({
173
+ action,
174
+ uploadEndpoint: normalizedUploadEndpoint.value,
175
+ deleteEndpoint: normalizedDeleteEndpoint.value,
176
+ endpoint: normalizeText(endpoint),
177
+ fieldName: normalizedFieldName
178
+ })
179
+ );
180
+ }
181
+
182
+ function updateSelectedFileName(fileName = "") {
183
+ selectedFileNameRef.value = normalizeText(fileName);
184
+ }
185
+
186
+ async function runChangeHandler(handler, payload) {
187
+ if (typeof handler !== "function") {
188
+ return;
189
+ }
190
+
191
+ try {
192
+ await Promise.resolve(handler(payload));
193
+ } catch (error) {
194
+ report({
195
+ message: resolveMessage(error?.message, normalizedMessages.changeError),
196
+ severity: "error",
197
+ channel: "banner",
198
+ dedupeKey: `${source}:change-error`
199
+ });
200
+ }
201
+ }
202
+
203
+ function resolveCustomUploadErrorMessage(error, response) {
204
+ const fallbackMessage = resolveUploadErrorFallback({
205
+ error,
206
+ response,
207
+ fallbackMessage: normalizedMessages.uploadError
208
+ });
209
+ if (typeof resolveUploadErrorMessage !== "function") {
210
+ return fallbackMessage;
211
+ }
212
+
213
+ return resolveMessage(
214
+ resolveUploadErrorMessage({
215
+ error,
216
+ response,
217
+ fieldName: normalizedFieldName,
218
+ defaultMessage: fallbackMessage
219
+ }),
220
+ fallbackMessage
221
+ );
222
+ }
223
+
224
+ function resolveCustomDeleteErrorMessage(error, response, body) {
225
+ const fallbackMessage = resolveDeleteErrorFallback({
226
+ error,
227
+ body,
228
+ fallbackMessage: normalizedMessages.deleteError
229
+ });
230
+ if (typeof resolveDeleteErrorMessage !== "function") {
231
+ return fallbackMessage;
232
+ }
233
+
234
+ return resolveMessage(
235
+ resolveDeleteErrorMessage({
236
+ error,
237
+ response,
238
+ body,
239
+ fieldName: normalizedFieldName,
240
+ defaultMessage: fallbackMessage
241
+ }),
242
+ fallbackMessage
243
+ );
244
+ }
245
+
246
+ function destroy() {
247
+ if (!imageUploadRuntime) {
248
+ return;
249
+ }
250
+
251
+ imageUploadRuntime.destroy();
252
+ imageUploadRuntime = null;
253
+ runtimeUploadEndpoint = "";
254
+ }
255
+
256
+ function ensureUploadRuntime() {
257
+ const endpoint = normalizedUploadEndpoint.value;
258
+ if (!endpoint) {
259
+ destroy();
260
+ return null;
261
+ }
262
+
263
+ if (imageUploadRuntime && runtimeUploadEndpoint === endpoint) {
264
+ return imageUploadRuntime;
265
+ }
266
+
267
+ destroy();
268
+
269
+ imageUploadRuntime = createImageUploadRuntimeFactory({
270
+ ...normalizedUploadOptions,
271
+ endpoint,
272
+ fieldName: normalizedFieldName,
273
+ resolveRequestHeaders: async () => resolveHeaders("upload", endpoint),
274
+ onSelectedFileNameChanged: updateSelectedFileName,
275
+ onUploadSuccess: async (payload) => {
276
+ closeDashboard(payload?.uppy);
277
+ updateSelectedFileName("");
278
+ report({
279
+ message: normalizedMessages.uploadSuccess,
280
+ severity: "success",
281
+ channel: "snackbar",
282
+ dedupeKey: `${source}:upload-success`
283
+ });
284
+ await runChangeHandler(onUploadSuccess, payload);
285
+ },
286
+ onInvalidResponse: () => {
287
+ report({
288
+ message: normalizedMessages.uploadInvalidResponse,
289
+ severity: "error",
290
+ channel: "banner",
291
+ dedupeKey: `${source}:upload-invalid-response`
292
+ });
293
+ },
294
+ onUploadError: ({ error, response }) => {
295
+ report({
296
+ message: resolveCustomUploadErrorMessage(error, response),
297
+ severity: "error",
298
+ channel: "banner",
299
+ dedupeKey: `${source}:upload-error`
300
+ });
301
+ },
302
+ onRestrictionFailed: ({ error }) => {
303
+ report({
304
+ message: resolveMessage(error?.message, normalizedMessages.uploadRestriction),
305
+ severity: "error",
306
+ channel: "banner",
307
+ dedupeKey: `${source}:upload-restriction`
308
+ });
309
+ },
310
+ onUnavailable: () => {
311
+ report({
312
+ message: normalizedMessages.editorUnavailable,
313
+ severity: "error",
314
+ channel: "banner",
315
+ dedupeKey: `${source}:editor-unavailable`
316
+ });
317
+ }
318
+ });
319
+ runtimeUploadEndpoint = endpoint;
320
+
321
+ return imageUploadRuntime;
322
+ }
323
+
324
+ function setup() {
325
+ const runtime = ensureUploadRuntime();
326
+ runtime?.setup?.();
327
+ }
328
+
329
+ function openEditor() {
330
+ const runtime = ensureUploadRuntime();
331
+ if (!runtime) {
332
+ report({
333
+ message: normalizedMessages.endpointUnavailable,
334
+ severity: "error",
335
+ channel: "banner",
336
+ dedupeKey: `${source}:endpoint-unavailable`
337
+ });
338
+ return;
339
+ }
340
+
341
+ runtime.openEditor();
342
+ }
343
+
344
+ async function deleteAsset() {
345
+ if (isDeleting.value) {
346
+ return;
347
+ }
348
+
349
+ const endpoint = normalizedDeleteEndpoint.value;
350
+ if (!endpoint) {
351
+ report({
352
+ message: normalizedMessages.deleteUnavailable,
353
+ severity: "error",
354
+ channel: "banner",
355
+ dedupeKey: `${source}:delete-unavailable`
356
+ });
357
+ return;
358
+ }
359
+
360
+ if (typeof deleteRequest !== "function" && typeof fetchImpl !== "function") {
361
+ report({
362
+ message: normalizedMessages.deleteUnavailable,
363
+ severity: "error",
364
+ channel: "banner",
365
+ dedupeKey: `${source}:delete-unavailable`
366
+ });
367
+ return;
368
+ }
369
+
370
+ let response = null;
371
+ let body = null;
372
+
373
+ isDeleting.value = true;
374
+ try {
375
+ const headers = await resolveHeaders("delete", endpoint);
376
+
377
+ if (typeof deleteRequest === "function") {
378
+ const deleteResult = normalizeDeleteResult(
379
+ await deleteRequest({
380
+ endpoint,
381
+ method: normalizedDeleteMethod,
382
+ headers,
383
+ fetch: fetchImpl,
384
+ uploadEndpoint: normalizedUploadEndpoint.value,
385
+ deleteEndpoint: endpoint,
386
+ fieldName: normalizedFieldName
387
+ })
388
+ );
389
+ response = deleteResult.response;
390
+ body = deleteResult.body;
391
+ if (response && response.ok === false) {
392
+ const requestError = new Error("Image delete request failed.");
393
+ requestError.response = response;
394
+ requestError.body = body;
395
+ throw requestError;
396
+ }
397
+ } else {
398
+ response = await fetchImpl(endpoint, {
399
+ method: normalizedDeleteMethod,
400
+ credentials: "include",
401
+ headers
402
+ });
403
+ body = await readJsonResponse(response);
404
+
405
+ if (!response.ok) {
406
+ throw new Error("Image delete request failed.");
407
+ }
408
+ }
409
+
410
+ updateSelectedFileName("");
411
+ report({
412
+ message: normalizedMessages.deleteSuccess,
413
+ severity: "success",
414
+ channel: "snackbar",
415
+ dedupeKey: `${source}:delete-success`
416
+ });
417
+ await runChangeHandler(onDeleteSuccess, {
418
+ data: body,
419
+ response
420
+ });
421
+ } catch (error) {
422
+ const errorResponse = response || error?.response || null;
423
+ const errorBody = body ?? error?.body ?? null;
424
+
425
+ report({
426
+ message: resolveCustomDeleteErrorMessage(error, errorResponse, errorBody),
427
+ severity: "error",
428
+ channel: "banner",
429
+ dedupeKey: `${source}:delete-error`
430
+ });
431
+ } finally {
432
+ isDeleting.value = false;
433
+ }
434
+ }
435
+
436
+ return Object.freeze({
437
+ assetUrl,
438
+ deleteAsset,
439
+ destroy,
440
+ hasAsset: normalizedHasAsset,
441
+ isDeleting,
442
+ openEditor,
443
+ selectedFileName: selectedFileNameRef,
444
+ setup
445
+ });
446
+ }
447
+
448
+ export {
449
+ createManagedImageAssetRuntime,
450
+ DEFAULT_MANAGED_IMAGE_ASSET_MESSAGES
451
+ };
@@ -1,4 +1,5 @@
1
1
  export { createImageUploadRuntime } from "./composables/createImageUploadRuntime.js";
2
+ export { createManagedImageAssetRuntime } from "./composables/createManagedImageAssetRuntime.js";
2
3
 
3
4
  const clientProviders = Object.freeze([]);
4
5
 
@@ -0,0 +1,196 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { ref } from "vue";
4
+ import { createManagedImageAssetRuntime } from "../src/client/composables/createManagedImageAssetRuntime.js";
5
+
6
+ function createFakeUploadRuntimeFactory(createdRuntimes) {
7
+ return (options = {}) => {
8
+ const runtime = {
9
+ options,
10
+ destroyCount: 0,
11
+ openCount: 0,
12
+ setupCount: 0,
13
+ destroy() {
14
+ this.destroyCount += 1;
15
+ },
16
+ openEditor() {
17
+ this.openCount += 1;
18
+ },
19
+ setup() {
20
+ this.setupCount += 1;
21
+ }
22
+ };
23
+
24
+ createdRuntimes.push(runtime);
25
+ return runtime;
26
+ };
27
+ }
28
+
29
+ function createJsonResponse(body, { ok = true } = {}) {
30
+ return {
31
+ ok,
32
+ headers: {
33
+ get(name) {
34
+ return String(name || "").toLowerCase() === "content-type" ? "application/json" : "";
35
+ }
36
+ },
37
+ async json() {
38
+ return body;
39
+ }
40
+ };
41
+ }
42
+
43
+ test("createManagedImageAssetRuntime reuses the shared upload lifecycle and delete flow", async () => {
44
+ const createdRuntimes = [];
45
+ const feedbackEvents = [];
46
+ const changedEvents = [];
47
+ const fetchCalls = [];
48
+ const selectedFileName = ref("");
49
+ const uploadEndpoint = ref("/api/pets/1/photo");
50
+ const assetVersion = ref("9");
51
+ const hasAsset = ref(true);
52
+
53
+ const runtime = createManagedImageAssetRuntime({
54
+ uploadEndpoint,
55
+ fieldName: "photo",
56
+ hasAsset,
57
+ assetVersion,
58
+ selectedFileName,
59
+ resolveAssetUrl: ({ uploadEndpoint: endpoint, assetVersion: version }) =>
60
+ version ? `${endpoint}?v=${version}` : endpoint,
61
+ resolveRequestHeaders: async ({ action }) => ({
62
+ "csrf-token": `csrf-${action}`
63
+ }),
64
+ onUploadSuccess: async ({ data }) => {
65
+ changedEvents.push(["upload", data]);
66
+ },
67
+ onDeleteSuccess: async ({ data }) => {
68
+ changedEvents.push(["delete", data]);
69
+ },
70
+ reportFeedback: (payload) => {
71
+ feedbackEvents.push(payload);
72
+ },
73
+ messages: {
74
+ uploadSuccess: "Photo uploaded.",
75
+ deleteSuccess: "Photo removed."
76
+ },
77
+ dependencies: {
78
+ createImageUploadRuntimeFactory: createFakeUploadRuntimeFactory(createdRuntimes),
79
+ fetchImpl: async (url, options = {}) => {
80
+ fetchCalls.push([url, options]);
81
+ return createJsonResponse({ ok: true });
82
+ }
83
+ }
84
+ });
85
+
86
+ assert.equal(runtime.assetUrl.value, "/api/pets/1/photo?v=9");
87
+
88
+ runtime.setup();
89
+ runtime.openEditor();
90
+ assert.equal(createdRuntimes.length, 1);
91
+ assert.equal(createdRuntimes[0].setupCount, 1);
92
+ assert.equal(createdRuntimes[0].openCount, 1);
93
+ assert.equal(createdRuntimes[0].options.fieldName, "photo");
94
+
95
+ createdRuntimes[0].options.onSelectedFileNameChanged("pet.png");
96
+ assert.equal(selectedFileName.value, "pet.png");
97
+
98
+ const dashboard = {
99
+ closeCount: 0,
100
+ closeModal() {
101
+ this.closeCount += 1;
102
+ }
103
+ };
104
+ await createdRuntimes[0].options.onUploadSuccess({
105
+ data: { id: "pet-1" },
106
+ uppy: {
107
+ getPlugin(name) {
108
+ return name === "Dashboard" ? dashboard : null;
109
+ }
110
+ }
111
+ });
112
+
113
+ assert.equal(selectedFileName.value, "");
114
+ assert.equal(dashboard.closeCount, 1);
115
+ assert.deepEqual(changedEvents, [["upload", { id: "pet-1" }]]);
116
+ assert.equal(feedbackEvents.at(-1)?.message, "Photo uploaded.");
117
+
118
+ await runtime.deleteAsset();
119
+
120
+ assert.equal(runtime.isDeleting.value, false);
121
+ assert.deepEqual(fetchCalls, [
122
+ [
123
+ "/api/pets/1/photo",
124
+ {
125
+ method: "DELETE",
126
+ credentials: "include",
127
+ headers: {
128
+ "csrf-token": "csrf-delete"
129
+ }
130
+ }
131
+ ]
132
+ ]);
133
+ assert.deepEqual(changedEvents, [
134
+ ["upload", { id: "pet-1" }],
135
+ ["delete", { ok: true }]
136
+ ]);
137
+ assert.equal(feedbackEvents.at(-1)?.message, "Photo removed.");
138
+
139
+ uploadEndpoint.value = "/api/pets/2/photo";
140
+ runtime.openEditor();
141
+
142
+ assert.equal(createdRuntimes.length, 2);
143
+ assert.equal(createdRuntimes[0].destroyCount, 1);
144
+ assert.equal(createdRuntimes[1].openCount, 1);
145
+ assert.equal(createdRuntimes[1].options.endpoint, "/api/pets/2/photo");
146
+ });
147
+
148
+ test("createManagedImageAssetRuntime reports configured upload and delete errors", async () => {
149
+ const createdRuntimes = [];
150
+ const feedbackEvents = [];
151
+
152
+ const runtime = createManagedImageAssetRuntime({
153
+ uploadEndpoint: "/api/pets/1/photo",
154
+ fieldName: "photo",
155
+ reportFeedback: (payload) => {
156
+ feedbackEvents.push(payload);
157
+ },
158
+ resolveUploadErrorMessage: ({ response, defaultMessage }) =>
159
+ response?.body?.fieldErrors?.photo || defaultMessage,
160
+ resolveDeleteErrorMessage: ({ body, defaultMessage }) =>
161
+ body?.fieldErrors?.photo || defaultMessage,
162
+ dependencies: {
163
+ createImageUploadRuntimeFactory: createFakeUploadRuntimeFactory(createdRuntimes),
164
+ fetchImpl: async () =>
165
+ createJsonResponse(
166
+ {
167
+ fieldErrors: {
168
+ photo: "Cannot delete this photo."
169
+ }
170
+ },
171
+ { ok: false }
172
+ )
173
+ }
174
+ });
175
+
176
+ runtime.openEditor();
177
+ createdRuntimes[0].options.onUploadError({
178
+ error: {
179
+ message: "Request failed."
180
+ },
181
+ response: {
182
+ body: {
183
+ fieldErrors: {
184
+ photo: "Selected photo is invalid."
185
+ }
186
+ }
187
+ }
188
+ });
189
+
190
+ await runtime.deleteAsset();
191
+
192
+ assert.deepEqual(
193
+ feedbackEvents.map((entry) => entry.message),
194
+ ["Selected photo is invalid.", "Cannot delete this photo."]
195
+ );
196
+ });
@@ -13,7 +13,12 @@ test("uploads-image-web exports are explicit and aligned with usage", () => {
13
13
  repoRoot: REPO_ROOT,
14
14
  packageDir: PACKAGE_DIR,
15
15
  packageId: "@jskit-ai/uploads-image-web",
16
- requiredExports: ["./client", "./shared"]
16
+ requiredExports: [
17
+ "./client",
18
+ "./client/composables/createImageUploadRuntime",
19
+ "./client/composables/createManagedImageAssetRuntime",
20
+ "./shared"
21
+ ]
17
22
  });
18
23
 
19
24
  assert.deepEqual(result.wildcardExports, []);