2026-03-02 22:18:05 +03:00
|
|
|
/**
|
2026-04-15 22:25:34 +03:00
|
|
|
* Unit tests for ComparisonStore (TanStack Query refactor)
|
2026-03-02 22:18:05 +03:00
|
|
|
*
|
2026-04-15 22:25:34 +03:00
|
|
|
* Uses the real BatchFontStore so Svelte $state reactivity works correctly.
|
|
|
|
|
* Controls network behaviour via vi.spyOn on the proxyFonts API layer.
|
2026-03-02 22:18:05 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/** @vitest-environment jsdom */
|
|
|
|
|
|
|
|
|
|
import type { UnifiedFont } from '$entities/Font';
|
|
|
|
|
import { UNIFIED_FONTS } from '$entities/Font/lib/mocks';
|
2026-04-15 22:25:34 +03:00
|
|
|
import { queryClient } from '$shared/api/queryClient';
|
2026-03-02 22:18:05 +03:00
|
|
|
import {
|
|
|
|
|
beforeEach,
|
|
|
|
|
describe,
|
|
|
|
|
expect,
|
|
|
|
|
it,
|
|
|
|
|
vi,
|
|
|
|
|
} from 'vitest';
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── Persistent-store mock ─────────────────────────────────────────────────────
|
2026-03-02 22:18:05 +03:00
|
|
|
|
|
|
|
|
const mockStorage = vi.hoisted(() => {
|
|
|
|
|
const storage: any = {};
|
2026-04-15 22:25:34 +03:00
|
|
|
storage._value = { fontAId: null, fontBId: null };
|
2026-03-02 22:18:05 +03:00
|
|
|
storage._clear = vi.fn(() => {
|
2026-04-15 22:25:34 +03:00
|
|
|
storage._value = { fontAId: null, fontBId: null };
|
2026-03-02 22:18:05 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(storage, 'value', {
|
|
|
|
|
get() {
|
|
|
|
|
return storage._value;
|
|
|
|
|
},
|
|
|
|
|
set(v: any) {
|
|
|
|
|
storage._value = v;
|
|
|
|
|
},
|
|
|
|
|
enumerable: true,
|
|
|
|
|
configurable: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(storage, 'clear', {
|
|
|
|
|
value: storage._clear,
|
|
|
|
|
enumerable: true,
|
|
|
|
|
configurable: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return storage;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.mock('$shared/lib/helpers/createPersistentStore/createPersistentStore.svelte', () => ({
|
|
|
|
|
createPersistentStore: vi.fn(() => mockStorage),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── $entities/Font mock — keep real BatchFontStore, stub singletons ───────────
|
|
|
|
|
|
2026-04-16 10:55:41 +03:00
|
|
|
vi.mock('$entities/Font', async importOriginal => {
|
|
|
|
|
const actual = await importOriginal<typeof import('$entities/Font')>();
|
2026-04-15 22:25:34 +03:00
|
|
|
const { BatchFontStore } = await import(
|
|
|
|
|
'$entities/Font/model/store/batchFontStore.svelte'
|
|
|
|
|
);
|
|
|
|
|
return {
|
2026-04-16 10:55:41 +03:00
|
|
|
...actual,
|
2026-04-15 22:25:34 +03:00
|
|
|
BatchFontStore,
|
|
|
|
|
fontStore: { fonts: [] },
|
|
|
|
|
appliedFontsManager: {
|
|
|
|
|
touch: vi.fn(),
|
2026-04-16 10:55:41 +03:00
|
|
|
pin: vi.fn(),
|
|
|
|
|
unpin: vi.fn(),
|
2026-04-15 22:25:34 +03:00
|
|
|
getFontStatus: vi.fn(),
|
|
|
|
|
ready: vi.fn(() => Promise.resolve()),
|
|
|
|
|
},
|
|
|
|
|
getFontUrl: vi.fn(() => 'https://example.com/font.woff2'),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── $features/SetupFont mock ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
vi.mock('$features/SetupFont', () => ({
|
|
|
|
|
DEFAULT_TYPOGRAPHY_CONTROLS_DATA: [],
|
|
|
|
|
createTypographyControlManager: vi.fn(() => ({
|
|
|
|
|
weight: 400,
|
|
|
|
|
renderedSize: 48,
|
|
|
|
|
reset: vi.fn(),
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-16 10:55:41 +03:00
|
|
|
vi.mock('$features/SetupFont/model', () => ({
|
|
|
|
|
typographySettingsStore: {
|
|
|
|
|
weight: 400,
|
|
|
|
|
renderedSize: 48,
|
|
|
|
|
reset: vi.fn(),
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── Imports (after mocks) ─────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-16 10:55:41 +03:00
|
|
|
import {
|
|
|
|
|
appliedFontsManager,
|
|
|
|
|
fontStore,
|
|
|
|
|
} from '$entities/Font';
|
2026-04-15 22:25:34 +03:00
|
|
|
import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts';
|
2026-03-02 22:18:05 +03:00
|
|
|
import { ComparisonStore } from './comparisonStore.svelte';
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-03-02 22:18:05 +03:00
|
|
|
describe('ComparisonStore', () => {
|
2026-04-15 22:25:34 +03:00
|
|
|
const mockFontA: UnifiedFont = UNIFIED_FONTS.roboto; // id: 'roboto'
|
|
|
|
|
const mockFontB: UnifiedFont = UNIFIED_FONTS.openSans; // id: 'open-sans'
|
2026-03-02 22:18:05 +03:00
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2026-04-15 22:25:34 +03:00
|
|
|
queryClient.clear();
|
2026-03-02 22:18:05 +03:00
|
|
|
vi.clearAllMocks();
|
2026-04-15 22:25:34 +03:00
|
|
|
mockStorage._value = { fontAId: null, fontBId: null };
|
2026-03-02 22:18:05 +03:00
|
|
|
mockStorage._clear.mockClear();
|
2026-04-10 08:06:51 +03:00
|
|
|
(fontStore as any).fonts = [];
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// Default: fetchFontsByIds returns empty so tests that don't care don't hang
|
|
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([]);
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// document.fonts: check returns true so #checkFontsLoaded resolves immediately
|
2026-03-02 22:18:05 +03:00
|
|
|
Object.defineProperty(document, 'fonts', {
|
2026-04-15 22:25:34 +03:00
|
|
|
value: {
|
|
|
|
|
check: vi.fn(() => true),
|
|
|
|
|
load: vi.fn(() => Promise.resolve()),
|
|
|
|
|
ready: Promise.resolve({} as FontFaceSet),
|
|
|
|
|
},
|
2026-03-02 22:18:05 +03:00
|
|
|
writable: true,
|
|
|
|
|
configurable: true,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── Initialization ────────────────────────────────────────────────────────
|
2026-03-02 22:18:05 +03:00
|
|
|
|
|
|
|
|
describe('Initialization', () => {
|
|
|
|
|
it('should create store with initial empty state', () => {
|
|
|
|
|
const store = new ComparisonStore();
|
|
|
|
|
expect(store.fontA).toBeUndefined();
|
|
|
|
|
expect(store.fontB).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── Restoration from Storage ──────────────────────────────────────────────
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
describe('Restoration from Storage (via BatchFontStore)', () => {
|
|
|
|
|
it('should restore fontA and fontB from stored IDs', async () => {
|
2026-03-02 22:18:05 +03:00
|
|
|
mockStorage._value.fontAId = mockFontA.id;
|
|
|
|
|
mockStorage._value.fontBId = mockFontB.id;
|
2026-04-15 22:25:34 +03:00
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
|
2026-03-02 22:18:05 +03:00
|
|
|
|
|
|
|
|
const store = new ComparisonStore();
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(store.fontA?.id).toBe(mockFontA.id);
|
|
|
|
|
expect(store.fontB?.id).toBe(mockFontB.id);
|
|
|
|
|
}, { timeout: 2000 });
|
2026-03-02 22:18:05 +03:00
|
|
|
});
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
it('should handle fetch errors during restoration gracefully', async () => {
|
2026-03-02 22:18:05 +03:00
|
|
|
mockStorage._value.fontAId = mockFontA.id;
|
|
|
|
|
mockStorage._value.fontBId = mockFontB.id;
|
2026-04-15 22:25:34 +03:00
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockRejectedValue(new Error('Network fail'));
|
2026-03-02 22:18:05 +03:00
|
|
|
|
|
|
|
|
const store = new ComparisonStore();
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// Store stays in valid state — no throw, fonts remain undefined
|
|
|
|
|
await vi.waitFor(() => expect(store.isLoading).toBe(true)); // stuck loading since no fonts
|
2026-03-02 22:18:05 +03:00
|
|
|
expect(store.fontA).toBeUndefined();
|
|
|
|
|
expect(store.fontB).toBeUndefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── Default Fallbacks ─────────────────────────────────────────────────────
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
describe('Default Fallbacks', () => {
|
|
|
|
|
it('should update storage with default IDs when storage is empty', async () => {
|
2026-04-10 08:06:51 +03:00
|
|
|
(fontStore as any).fonts = [mockFontA, mockFontB];
|
2026-04-15 22:25:34 +03:00
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
new ComparisonStore();
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(mockStorage._value.fontAId).toBe(mockFontA.id);
|
|
|
|
|
expect(mockStorage._value.fontBId).toBe(mockFontB.id);
|
|
|
|
|
});
|
2026-03-02 22:18:05 +03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── Loading State ─────────────────────────────────────────────────────────
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
describe('Aggregate Loading State', () => {
|
|
|
|
|
it('should be loading initially when storage has IDs', async () => {
|
|
|
|
|
mockStorage._value.fontAId = mockFontA.id;
|
|
|
|
|
mockStorage._value.fontBId = mockFontB.id;
|
|
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockImplementation(
|
|
|
|
|
() => new Promise(r => setTimeout(() => r([mockFontA, mockFontB]), 50)),
|
|
|
|
|
);
|
2026-03-02 22:18:05 +03:00
|
|
|
|
|
|
|
|
const store = new ComparisonStore();
|
2026-04-15 22:25:34 +03:00
|
|
|
expect(store.isLoading).toBe(true);
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
await vi.waitFor(() => expect(store.isLoading).toBe(false), { timeout: 2000 });
|
2026-03-02 22:18:05 +03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
// ── Reset ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-03-02 22:18:05 +03:00
|
|
|
describe('Reset Functionality', () => {
|
|
|
|
|
it('should reset all state and clear storage', () => {
|
|
|
|
|
const store = new ComparisonStore();
|
|
|
|
|
store.resetAll();
|
|
|
|
|
expect(mockStorage._clear).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
it('should clear fontA and fontB on reset', async () => {
|
2026-03-02 22:18:05 +03:00
|
|
|
mockStorage._value.fontAId = mockFontA.id;
|
|
|
|
|
mockStorage._value.fontBId = mockFontB.id;
|
2026-04-15 22:25:34 +03:00
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
|
2026-03-02 22:18:05 +03:00
|
|
|
|
|
|
|
|
const store = new ComparisonStore();
|
2026-04-15 22:25:34 +03:00
|
|
|
await vi.waitFor(() => expect(store.fontA?.id).toBe(mockFontA.id), { timeout: 2000 });
|
2026-03-02 22:18:05 +03:00
|
|
|
|
2026-04-15 22:25:34 +03:00
|
|
|
store.resetAll();
|
|
|
|
|
expect(store.fontA).toBeUndefined();
|
|
|
|
|
expect(store.fontB).toBeUndefined();
|
2026-03-02 22:18:05 +03:00
|
|
|
});
|
|
|
|
|
});
|
2026-04-16 10:55:41 +03:00
|
|
|
|
|
|
|
|
// ── Pin / Unpin ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
describe('Pin / Unpin (eviction guard)', () => {
|
|
|
|
|
it('pins fontA and fontB when they are loaded', async () => {
|
|
|
|
|
mockStorage._value.fontAId = mockFontA.id;
|
|
|
|
|
mockStorage._value.fontBId = mockFontB.id;
|
|
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
|
|
|
|
|
|
|
|
|
|
new ComparisonStore();
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
|
|
|
|
|
mockFontA.id,
|
|
|
|
|
400,
|
|
|
|
|
mockFontA.features?.isVariable,
|
|
|
|
|
);
|
|
|
|
|
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
|
|
|
|
|
mockFontB.id,
|
|
|
|
|
400,
|
|
|
|
|
mockFontB.features?.isVariable,
|
|
|
|
|
);
|
|
|
|
|
}, { timeout: 2000 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('unpins the old font when fontA is replaced', async () => {
|
|
|
|
|
mockStorage._value.fontAId = mockFontA.id;
|
|
|
|
|
mockStorage._value.fontBId = mockFontB.id;
|
|
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
|
|
|
|
|
|
|
|
|
|
const store = new ComparisonStore();
|
|
|
|
|
await vi.waitFor(() => expect(store.fontA?.id).toBe(mockFontA.id), { timeout: 2000 });
|
|
|
|
|
|
|
|
|
|
const mockFontC: typeof mockFontA = { ...mockFontA, id: 'playfair', name: 'Playfair Display' };
|
|
|
|
|
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontC, mockFontB]);
|
|
|
|
|
store.fontA = mockFontC;
|
|
|
|
|
|
|
|
|
|
await vi.waitFor(() => {
|
|
|
|
|
expect(appliedFontsManager.unpin).toHaveBeenCalledWith(
|
|
|
|
|
mockFontA.id,
|
|
|
|
|
400,
|
|
|
|
|
mockFontA.features?.isVariable,
|
|
|
|
|
);
|
|
|
|
|
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
|
|
|
|
|
mockFontC.id,
|
|
|
|
|
400,
|
|
|
|
|
mockFontC.features?.isVariable,
|
|
|
|
|
);
|
|
|
|
|
}, { timeout: 2000 });
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-03-02 22:18:05 +03:00
|
|
|
});
|