refactor(comparison): replace comparisonStore singleton with lazy getComparisonStore

Mirror the font-catalog change in ComparisonView: expose getComparisonStore()
(plus __resetComparisonStore for tests) instead of an eager comparisonStore
singleton, and consume getFontCatalog() internally. Update the model barrel and
all UI consumers (Sidebar, FontList, Header, Line, SliderArea); Character no
longer needs the store and reads everything from props.

Update both specs to the accessor: comparisonStore.test mocks getFontCatalog
with a writable stub (the real store's fonts is getter-only) and resets the
catalog between cases; Sidebar.svelte.test resolves the store via the accessor.

Also document Character's props.
This commit is contained in:
Ilia Mashkov
2026-06-01 17:25:05 +03:00
parent 10603d18bf
commit 1ad015aed6
10 changed files with 107 additions and 55 deletions
+3 -4
View File
@@ -1,4 +1,3 @@
export {
comparisonStore,
type Side,
} from './stores/comparisonStore.svelte';
export { getComparisonStore } from './stores/comparisonStore.svelte';
export type { Side } from './stores/comparisonStore.svelte';
@@ -19,9 +19,10 @@ import {
getFontUrl,
} from '$entities/Font';
import {
type FontCatalogStore,
FontsByIdsStore,
fontCatalogStore,
fontLifecycleManager,
getFontCatalog,
} from '$entities/Font/model';
import { typographySettingsStore } from '$features/AdjustTypography/model';
import { createPersistentStore } from '$shared/lib';
@@ -98,10 +99,13 @@ export class ComparisonStore {
*/
#fontsByIdsStore: FontsByIdsStore;
#fontCatalog: FontCatalogStore;
constructor() {
// Synchronously seed the batch store with any IDs already in storage
const { fontAId, fontBId } = storage.value;
this.#fontsByIdsStore = new FontsByIdsStore(fontAId && fontBId ? [fontAId, fontBId] : []);
this.#fontCatalog = getFontCatalog();
$effect.root(() => {
// Sync batch results → fontA / fontB
@@ -173,7 +177,7 @@ export class ComparisonStore {
return;
}
const fonts = fontCatalogStore.fonts;
const fonts = this.#fontCatalog.fonts;
if (fonts.length < 2) {
return;
@@ -356,7 +360,14 @@ export class ComparisonStore {
}
}
/**
* Singleton comparison store instance
*/
export const comparisonStore = new ComparisonStore();
let _comparisonStore: ComparisonStore | undefined;
export function getComparisonStore(): ComparisonStore {
return (_comparisonStore ??= new ComparisonStore());
}
// test-only reset, so specs don't share a live observer
export function __resetComparisonStore() {
_comparisonStore?.resetAll();
_comparisonStore = undefined;
}
@@ -47,6 +47,10 @@ const mockStorage = vi.hoisted(() => {
return storage;
});
// Writable catalog stub — tests assign `.fonts` directly, which the real
// FontCatalogStore forbids (getter-only). getFontCatalog returns this singleton.
const mockFontCatalog = vi.hoisted(() => ({ fonts: [] as unknown[] }));
vi.mock('$shared/lib/helpers/createPersistentStore/createPersistentStore.svelte', () => ({
createPersistentStore: vi.fn(() => mockStorage),
}));
@@ -65,7 +69,7 @@ vi.mock('$entities/Font/model', async importOriginal => {
const actual = await importOriginal<typeof import('$entities/Font/model')>();
return {
...actual,
fontCatalogStore: { fonts: [] },
getFontCatalog: () => mockFontCatalog,
fontLifecycleManager: {
touch: vi.fn(),
pin: vi.fn(),
@@ -95,21 +99,23 @@ vi.mock('$features/AdjustTypography/model', () => ({
import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts';
import {
fontCatalogStore,
fontLifecycleManager,
getFontCatalog,
} from '$entities/Font/model';
import { __resetFontCatalog } from '$entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte';
import { ComparisonStore } from './comparisonStore.svelte';
describe('ComparisonStore', () => {
const mockFontA: UnifiedFont = UNIFIED_FONTS.roboto; // id: 'roboto'
const mockFontB: UnifiedFont = UNIFIED_FONTS.openSans; // id: 'open-sans'
let fontCatalog = getFontCatalog();
beforeEach(() => {
queryClient.clear();
vi.clearAllMocks();
mockStorage._value = { fontAId: null, fontBId: null };
mockStorage._clear.mockClear();
(fontCatalogStore as any).fonts = [];
(fontCatalog as any).fonts = [];
// Default: fetchFontsByIds returns empty so tests that don't care don't hang
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([]);
@@ -126,6 +132,10 @@ describe('ComparisonStore', () => {
});
});
afterEach(() => {
__resetFontCatalog();
});
describe('Initialization', () => {
it('should create store with initial empty state', () => {
const store = new ComparisonStore();
@@ -164,7 +174,7 @@ describe('ComparisonStore', () => {
describe('Default Fallbacks', () => {
it('should update storage with default IDs when storage is empty', async () => {
(fontCatalogStore as any).fonts = [mockFontA, mockFontB];
(fontCatalog as any).fonts = [mockFontA, mockFontB];
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
new ComparisonStore();
@@ -192,7 +202,7 @@ describe('ComparisonStore', () => {
// 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];
(fontCatalog as any).fonts = [mockFontA, mockFontB];
// Delay the batch so the catalog-driven effect runs first.
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockImplementation(