@snowcone-app/ui 0.9.1 → 0.9.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.
- package/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/dist/index.cjs +142 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +142 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/composed/ArtworkCustomizer.tsx +2 -2
- package/src/composed/CurrentSelectionDisplay.tsx +1 -1
- package/src/composed/HeroProductImage.tsx +59 -0
- package/src/composed/ProductGallery.tsx +1 -1
- package/src/hooks/useProductGallery.ts +1 -1
- package/src/patterns/MockupPriorityProvider.tsx +49 -7
- package/src/patterns/RealtimeProvider.tsx +123 -5
- package/src/patterns/__tests__/MockupPriorityProvider.dragStop.test.tsx +112 -0
- package/src/patterns/__tests__/RealtimeProvider.cropDelivery.test.tsx +103 -3
|
@@ -124,6 +124,23 @@ function sentConfigs(): Array<Record<string, unknown>> {
|
|
|
124
124
|
|
|
125
125
|
let blobCounter = 0;
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The box's rule (rendercenter realtime.ts renderFingerprint): a config
|
|
129
|
+
* re-renders only when its render fingerprint changes — overlayCrop is NOT in
|
|
130
|
+
* it and mockupIds are SORTED. Returns the mockup sets the box re-renders for
|
|
131
|
+
* `configs`, given the set in force before them.
|
|
132
|
+
*/
|
|
133
|
+
function boxRerenders(before: string[], configs: Array<Record<string, unknown>>): string[][] {
|
|
134
|
+
const fp = (ids: unknown) => [...(ids as string[])].sort().join(",");
|
|
135
|
+
let prev = fp(before);
|
|
136
|
+
const out: string[][] = [];
|
|
137
|
+
for (const c of configs) {
|
|
138
|
+
if (fp(c.mockupIds) !== prev) out.push(c.mockupIds as string[]);
|
|
139
|
+
prev = fp(c.mockupIds);
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
127
144
|
beforeEach(() => {
|
|
128
145
|
vi.clearAllMocks();
|
|
129
146
|
h.state.results = [];
|
|
@@ -220,12 +237,20 @@ describe("RealtimeProvider cropped delivery", () => {
|
|
|
220
237
|
await act(async () => rerender(<Harness version={4} />));
|
|
221
238
|
|
|
222
239
|
await waitFor(() => {
|
|
223
|
-
// Unflipped:
|
|
240
|
+
// Unflipped: configs WITHOUT overlayCrop, ending on the whole session
|
|
241
|
+
// set (m1 + m2)...
|
|
224
242
|
expect(sentConfigs().length).toBeGreaterThan(0);
|
|
225
243
|
expect(sentConfigs().every((c) => !c.overlayCrop)).toBe(true);
|
|
226
|
-
|
|
227
|
-
expect(h.updateMockupIds).toHaveBeenCalledWith(["m1"]);
|
|
244
|
+
expect(sentConfigs().at(-1)?.mockupIds).toEqual(["m1", "m2"]);
|
|
228
245
|
});
|
|
246
|
+
// ...and the box really re-renders the lost edit. Dropping the crop with
|
|
247
|
+
// the same set is "no change" to the box (BC43BE swap, prod 2026-11-11:
|
|
248
|
+
// the hero stayed on v1 while the canvas reached v3).
|
|
249
|
+
const rerenders = boxRerenders(["m1", "m2"], sentConfigs());
|
|
250
|
+
expect(rerenders.some((ids) => ids.includes("m1"))).toBe(true);
|
|
251
|
+
expect(rerenders.at(-1)).toEqual(["m1", "m2"]);
|
|
252
|
+
// Never a single-mockup request: the server renders the last config's set.
|
|
253
|
+
expect(h.updateMockupIds).not.toHaveBeenCalled();
|
|
229
254
|
|
|
230
255
|
// Kill-switch: even after everything re-seeds, no flip this session
|
|
231
256
|
h.sendConfig.mockClear();
|
|
@@ -238,6 +263,81 @@ describe("RealtimeProvider cropped delivery", () => {
|
|
|
238
263
|
errSpy.mockRestore();
|
|
239
264
|
});
|
|
240
265
|
|
|
266
|
+
it("several failed composites in one edit never narrow the session to the last one (BC43BE, 2026-11-11)", async () => {
|
|
267
|
+
// Prod trace: five cropped frames failed together and the fallback sent
|
|
268
|
+
// one config per mockup — [V7X7MN] … [YDNHY6] — so the server rendered
|
|
269
|
+
// only YDNHY6 on every later edit and the hero never updated.
|
|
270
|
+
const { rerender } = render(<Harness version={1} />);
|
|
271
|
+
await waitFor(() => expect(h.sendConfig).toHaveBeenCalled());
|
|
272
|
+
await seedAndFlip(rerender);
|
|
273
|
+
h.sendConfig.mockClear();
|
|
274
|
+
|
|
275
|
+
h.compositeCroppedFrame.mockRejectedValue(new Error("E_OVERLAY_ENCODE_FAILED"));
|
|
276
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
277
|
+
h.state.results = [
|
|
278
|
+
croppedResult("m1", "https://r/m1-crop-2"),
|
|
279
|
+
croppedResult("m2", "https://r/m2-crop-2"),
|
|
280
|
+
];
|
|
281
|
+
await act(async () => rerender(<Harness version={4} />));
|
|
282
|
+
|
|
283
|
+
await waitFor(() => expect(sentConfigs().length).toBeGreaterThan(0));
|
|
284
|
+
await act(async () => {
|
|
285
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
286
|
+
});
|
|
287
|
+
// The box re-renders both failed pictures, and the config left in force
|
|
288
|
+
// (the last one: the server renders the last config's set on every later
|
|
289
|
+
// edit) covers every picture.
|
|
290
|
+
const configs = sentConfigs();
|
|
291
|
+
expect(configs.every((c) => c.overlayCrop === undefined)).toBe(true);
|
|
292
|
+
expect(configs.at(-1)?.mockupIds).toEqual(["m1", "m2"]);
|
|
293
|
+
const rerenders = boxRerenders(["m1", "m2"], configs);
|
|
294
|
+
expect(rerenders.at(-1)).toEqual(["m1", "m2"]);
|
|
295
|
+
expect(h.updateMockupIds).not.toHaveBeenCalled();
|
|
296
|
+
errSpy.mockRestore();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("a composite whose frame was superseded mid-drag is dropped, not treated as a failure", async () => {
|
|
300
|
+
// The SDK revokes a mockup's previous blob: frame when the next one lands,
|
|
301
|
+
// so the old composite fails to load it. The newer frame must win and
|
|
302
|
+
// cropped delivery must stay on.
|
|
303
|
+
const { rerender } = render(<Harness version={1} />);
|
|
304
|
+
await waitFor(() => expect(h.sendConfig).toHaveBeenCalled());
|
|
305
|
+
await seedAndFlip(rerender);
|
|
306
|
+
h.sendConfig.mockClear();
|
|
307
|
+
|
|
308
|
+
let rejectOld!: (e: Error) => void;
|
|
309
|
+
h.compositeCroppedFrame.mockImplementationOnce(
|
|
310
|
+
() => new Promise((_, reject) => { rejectOld = reject; })
|
|
311
|
+
);
|
|
312
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
313
|
+
const seen: string[] = [];
|
|
314
|
+
capturedCtx.subscribeMockupResultById("m1", (r) => seen.push(r.imageUrl));
|
|
315
|
+
|
|
316
|
+
h.state.results = [croppedResult("m1", "blob:v3-m1"), fullResult("m2", "https://r/m2-full")];
|
|
317
|
+
await act(async () => rerender(<Harness version={4} />));
|
|
318
|
+
await waitFor(() => expect(h.compositeCroppedFrame).toHaveBeenCalledTimes(1));
|
|
319
|
+
|
|
320
|
+
// v4 lands while v3 is still compositing; the SDK revoked v3's URL.
|
|
321
|
+
h.state.results = [croppedResult("m1", "blob:v4-m1"), fullResult("m2", "https://r/m2-full")];
|
|
322
|
+
await act(async () => rerender(<Harness version={5} />));
|
|
323
|
+
await act(async () => { rejectOld(new Error("E_OVERLAY_FRAME_LOAD")); });
|
|
324
|
+
|
|
325
|
+
await waitFor(() => {
|
|
326
|
+
expect(h.compositeCroppedFrame).toHaveBeenLastCalledWith(
|
|
327
|
+
expect.objectContaining({ frameUrl: "blob:v4-m1" })
|
|
328
|
+
);
|
|
329
|
+
expect(seen.some((u) => u.startsWith("blob:composite-"))).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
await act(async () => { await new Promise((r) => setTimeout(r, 20)); });
|
|
332
|
+
expect(h.sendConfig).not.toHaveBeenCalled(); // no unflip, no re-request
|
|
333
|
+
expect(h.updateMockupIds).not.toHaveBeenCalled();
|
|
334
|
+
expect(errSpy).not.toHaveBeenCalledWith(
|
|
335
|
+
expect.stringContaining("E_OVERLAY_COMPOSITE_FAILED"),
|
|
336
|
+
expect.anything()
|
|
337
|
+
);
|
|
338
|
+
errSpy.mockRestore();
|
|
339
|
+
});
|
|
340
|
+
|
|
241
341
|
it("drops to full-frame before rendering a never-seeded mockup via updateMockupIds", async () => {
|
|
242
342
|
const { rerender } = render(<Harness version={1} />);
|
|
243
343
|
await waitFor(() => expect(h.sendConfig).toHaveBeenCalled());
|