fix(comparisonStore): preserve stored selection on cold load

The seed-defaults effect fired whenever fontA/fontB were still
undefined, including the window between constructor reading storage
and the per-id batch resolving. On a slow batch or fast catalog the
effect clobbered storage with catalog[0]/catalog[N-1], losing the
user's pick on reload.

Now bails when storage already holds IDs, and reads storage via
untrack so per-font selection writes don't re-trigger the effect.

Adds a deterministic regression test that controls catalog/batch
ordering via mockImplementation timing.
This commit is contained in:
Ilia Mashkov
2026-05-28 14:58:18 +03:00
parent 7a9422b574
commit b9e21a66d3
2 changed files with 66 additions and 12 deletions
@@ -165,6 +165,46 @@ describe('ComparisonStore', () => {
expect(mockStorage._value.fontBId).toBe(mockFontB.id);
});
});
/**
* Regression: when storage already holds the user's selection, the
* seed-defaults effect must bail out — even if it fires before the
* per-id batch returns (catalog wins the race on slow networks or
* cold reloads). Pre-fix the effect only checked fontA/fontB, both
* still undefined at this point, and clobbered storage with whatever
* the catalog had as fonts[0] / fonts[N-1].
*/
it('should not overwrite stored IDs when batch is still in flight', async () => {
const seededA = UNIFIED_FONTS.lato;
const seededB = UNIFIED_FONTS.montserrat;
mockStorage._value.fontAId = seededA.id;
mockStorage._value.fontBId = seededB.id;
// Catalog defaults differ from the stored selection — if the
// effect mis-seeds, storage will flip to roboto / open-sans.
(fontCatalogStore as any).fonts = [mockFontA, mockFontB];
// Delay the batch so the catalog-driven effect runs first.
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockImplementation(
() => new Promise(r => setTimeout(() => r([seededA, seededB]), 50)),
);
const store = new ComparisonStore();
// Let the catalog effect run; storage must be untouched.
await new Promise(r => setTimeout(r, 10));
expect(mockStorage._value.fontAId).toBe(seededA.id);
expect(mockStorage._value.fontBId).toBe(seededB.id);
// Batch resolves with the seeded selection — fontA/B must match.
await vi.waitFor(() => {
expect(store.fontA?.id).toBe(seededA.id);
expect(store.fontB?.id).toBe(seededB.id);
}, { timeout: 2000 });
expect(mockStorage._value.fontAId).toBe(seededA.id);
expect(mockStorage._value.fontBId).toBe(seededB.id);
});
});
describe('Aggregate Loading State', () => {