From 1ad015aed6359927a9696432fb44707b6dce4156 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 1 Jun 2026 17:25:05 +0300 Subject: [PATCH] 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. --- src/widgets/ComparisonView/model/index.ts | 7 ++- .../model/stores/comparisonStore.svelte.ts | 23 +++++++--- .../model/stores/comparisonStore.test.ts | 20 ++++++--- .../ui/Character/Character.svelte | 20 ++++++--- .../ui/FontList/FontList.svelte | 12 +++-- .../ComparisonView/ui/Header/Header.svelte | 4 +- .../ComparisonView/ui/Line/Line.svelte | 44 ++++++++++--------- .../ComparisonView/ui/Sidebar/Sidebar.svelte | 13 +++--- .../ui/Sidebar/Sidebar.svelte.test.ts | 15 ++++++- .../ui/SliderArea/SliderArea.svelte | 4 +- 10 files changed, 107 insertions(+), 55 deletions(-) diff --git a/src/widgets/ComparisonView/model/index.ts b/src/widgets/ComparisonView/model/index.ts index 6cba7a6..11d6d59 100644 --- a/src/widgets/ComparisonView/model/index.ts +++ b/src/widgets/ComparisonView/model/index.ts @@ -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'; diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts index 2562cf3..b4b616a 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts @@ -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; +} diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts index 4f04d48..1aa0240 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts @@ -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(); 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( diff --git a/src/widgets/ComparisonView/ui/Character/Character.svelte b/src/widgets/ComparisonView/ui/Character/Character.svelte index 9ae91a6..ddb69a8 100644 --- a/src/widgets/ComparisonView/ui/Character/Character.svelte +++ b/src/widgets/ComparisonView/ui/Character/Character.svelte @@ -7,16 +7,25 @@ the Line container zeroes font-size to collapse inter-element whitespace. -->
comparisonStore.side = 'A'} class="flex-1 tracking-wide font-bold uppercase" > @@ -83,9 +86,9 @@ let { comparisonStore.side = 'B'} > Right Font diff --git a/src/widgets/ComparisonView/ui/Sidebar/Sidebar.svelte.test.ts b/src/widgets/ComparisonView/ui/Sidebar/Sidebar.svelte.test.ts index 14b24ce..998d77c 100644 --- a/src/widgets/ComparisonView/ui/Sidebar/Sidebar.svelte.test.ts +++ b/src/widgets/ComparisonView/ui/Sidebar/Sidebar.svelte.test.ts @@ -5,7 +5,11 @@ import { waitFor, } from '@testing-library/svelte'; import { createRawSnippet } from 'svelte'; -import { comparisonStore } from '../../model'; +import { getComparisonStore } from '../../model'; +import { + ComparisonStore, + __resetComparisonStore, +} from '../../model/stores/comparisonStore.svelte'; import Sidebar from './Sidebar.svelte'; function textSnippet(text: string) { @@ -13,10 +17,17 @@ function textSnippet(text: string) { } describe('Sidebar', () => { - afterEach(() => { + let comparisonStore!: ComparisonStore; + + beforeEach(() => { + comparisonStore = getComparisonStore(); comparisonStore.side = 'A'; }); + afterEach(() => { + __resetComparisonStore(); + }); + describe('Rendering', () => { it('renders the "Configuration" title', () => { render(Sidebar); diff --git a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte index 62a7cff..b3a62d8 100644 --- a/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte +++ b/src/widgets/ComparisonView/ui/SliderArea/SliderArea.svelte @@ -33,7 +33,7 @@ import { ensureCanvasFonts, getPretextFontString, } from '../../lib'; -import { comparisonStore } from '../../model'; +import { getComparisonStore } from '../../model'; import Line from '../Line/Line.svelte'; import Thumb from '../Thumb/Thumb.svelte'; @@ -51,6 +51,8 @@ interface Props { let { isSidebarOpen = false, class: className }: Props = $props(); +const comparisonStore = getComparisonStore(); + /** * Spring tuning for the comparison slider thumb. Lower stiffness = slower * follow; higher damping = less overshoot.