Lixen commited on
Commit
d8c5e93
·
unverified ·
1 Parent(s): 85a0174

Add regression test for global and individual font changes (#542)

Browse files
ui/tests/components/RenderControlsPanel.test.tsx ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { screen, waitFor } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { http, HttpResponse } from 'msw'
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { RenderControlsPanel } from '@/components/panels/RenderControlsPanel'
7
+ import { useSelectionStore } from '@/lib/stores/selectionStore'
8
+ import { usePreferencesStore } from '@/lib/stores/preferencesStore'
9
+ import * as sceneActions from '@/lib/io/scene'
10
+
11
+ import { renderWithQuery } from '../helpers'
12
+ import { server } from '../msw/server'
13
+
14
+ vi.mock('@tanstack/react-virtual', () => ({
15
+ useVirtualizer: ({ count }: { count: number }) => ({
16
+ getTotalSize: () => count * 28,
17
+ getVirtualItems: () =>
18
+ Array.from({ length: count }, (_, i) => ({
19
+ index: i,
20
+ start: i * 28,
21
+ end: (i + 1) * 28,
22
+ size: 28,
23
+ key: i,
24
+ })),
25
+ measure: vi.fn(),
26
+ }),
27
+ }))
28
+
29
+ vi.mock('@/lib/io/scene', async () => {
30
+ const actual = await vi.importActual<any>('@/lib/io/scene')
31
+ return {
32
+ ...actual,
33
+ applyOp: vi.fn(),
34
+ queueAutoRender: vi.fn(),
35
+ }
36
+ })
37
+
38
+ function sceneWithTextNodes(nodes: any[]) {
39
+ const nodeMap: any = {}
40
+ nodes.forEach((n) => {
41
+ nodeMap[n.id] = {
42
+ id: n.id,
43
+ transform: { x: 0, y: 0, width: 10, height: 10, rotationDeg: 0 },
44
+ visible: true,
45
+ kind: { text: n.kind?.text ?? { style: { fontFamilies: ['Arial'] } } },
46
+ }
47
+ })
48
+ return {
49
+ epoch: 1,
50
+ scene: {
51
+ pages: {
52
+ p1: { id: 'p1', name: 'P1', nodes: nodeMap },
53
+ },
54
+ project: { name: 'Proj' },
55
+ },
56
+ }
57
+ }
58
+
59
+ describe('RenderControlsPanel Font Assignment', () => {
60
+ beforeEach(() => {
61
+ useSelectionStore.getState().setPage('p1')
62
+ useSelectionStore.getState().clear()
63
+ usePreferencesStore.getState().setDefaultFont('Arial')
64
+ vi.clearAllMocks()
65
+
66
+ server.use(
67
+ http.get('/api/v1/fonts', () =>
68
+ HttpResponse.json([
69
+ { familyName: 'Arial', postScriptName: 'Arial', source: 'system', cached: true },
70
+ { familyName: 'Roboto', postScriptName: 'Roboto', source: 'system', cached: true },
71
+ { familyName: 'Custom', postScriptName: 'Custom', source: 'system', cached: true },
72
+ ]),
73
+ ),
74
+ http.get('/api/v1/scene.json', () =>
75
+ HttpResponse.json(
76
+ sceneWithTextNodes([
77
+ { id: 't1', kind: { text: { style: { fontFamilies: ['Arial'] } } } },
78
+ { id: 't2', kind: { text: { style: { fontFamilies: ['Arial'] } } } },
79
+ ]),
80
+ ),
81
+ ),
82
+ )
83
+ })
84
+
85
+ it('applying a font to a singular text box only updates that box', async () => {
86
+ renderWithQuery(<RenderControlsPanel />)
87
+
88
+ // Select node t1
89
+ useSelectionStore.getState().select('t1', false)
90
+
91
+ // Open font select
92
+ const trigger = await screen.findByTestId('render-font-select')
93
+ await userEvent.click(trigger)
94
+
95
+ // Pick "Roboto"
96
+ const option = await screen.findByText('Roboto')
97
+ await userEvent.click(option)
98
+
99
+ // Verify applyOp was called for t1
100
+ await waitFor(() => expect(sceneActions.applyOp).toHaveBeenCalled())
101
+ const lastOp = (sceneActions.applyOp as any).mock.calls[0][0]
102
+ expect(lastOp).toHaveProperty('updateNode')
103
+ expect(lastOp.updateNode.id).toBe('t1')
104
+ expect(lastOp.updateNode.patch.data.text.style.fontFamilies).toEqual(['Roboto'])
105
+ })
106
+
107
+ it('bulk applying a font change (with selection) updates all selected boxes', async () => {
108
+ renderWithQuery(<RenderControlsPanel />)
109
+
110
+ // Select both nodes
111
+ useSelectionStore.getState().selectMany(['t1', 't2'])
112
+
113
+ // Open font select
114
+ const trigger = await screen.findByTestId('render-font-select')
115
+ await userEvent.click(trigger)
116
+
117
+ // Pick "Roboto"
118
+ const option = await screen.findByText('Roboto')
119
+ await userEvent.click(option)
120
+
121
+ // Verify applyOp was called with a batch
122
+ await waitFor(() => expect(sceneActions.applyOp).toHaveBeenCalled())
123
+ const lastOp = (sceneActions.applyOp as any).mock.calls[0][0]
124
+ expect(lastOp).toHaveProperty('batch')
125
+ expect(lastOp.batch.ops).toHaveLength(2)
126
+ })
127
+
128
+ it('changing global font (no selection) updates defaultFont in preferences', async () => {
129
+ renderWithQuery(<RenderControlsPanel />)
130
+
131
+ // No selection
132
+
133
+ // Open font select
134
+ const trigger = await screen.findByTestId('render-font-select')
135
+ await userEvent.click(trigger)
136
+
137
+ // Pick "Custom"
138
+ const option = await screen.findByText('Custom')
139
+ await userEvent.click(option)
140
+
141
+ // Verify default font changed
142
+ expect(usePreferencesStore.getState().defaultFont).toBe('Custom')
143
+ })
144
+ })
ui/tests/setup.ts CHANGED
@@ -81,3 +81,50 @@ Object.defineProperty(window, 'matchMedia', {
81
  dispatchEvent: vi.fn(),
82
  })),
83
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  dispatchEvent: vi.fn(),
82
  })),
83
  })
84
+
85
+ // Mock localStorage for zustand persist
86
+ const localStorageMock = (() => {
87
+ let store: Record<string, string> = {}
88
+ return {
89
+ getItem: vi.fn((key: string) => store[key] || null),
90
+ setItem: vi.fn((key: string, value: string) => {
91
+ store[key] = value.toString()
92
+ }),
93
+ removeItem: vi.fn((key: string) => {
94
+ delete store[key]
95
+ }),
96
+ clear: vi.fn(() => {
97
+ store = {}
98
+ }),
99
+ length: 0,
100
+ key: vi.fn((index: number) => Object.keys(store)[index] || null),
101
+ }
102
+ })()
103
+
104
+ Object.defineProperty(window, 'localStorage', {
105
+ value: localStorageMock,
106
+ })
107
+
108
+ // FontFace API stubs for jsdom
109
+ class StubFontFace {
110
+ constructor(family: string, source: string | ArrayBuffer | ArrayBufferView, descriptors?: any) {}
111
+ load() {
112
+ return Promise.resolve(this)
113
+ }
114
+ }
115
+ Object.defineProperty(globalThis, 'FontFace', {
116
+ value: StubFontFace,
117
+ writable: true,
118
+ })
119
+
120
+ Object.defineProperty(document, 'fonts', {
121
+ value: {
122
+ add: vi.fn(),
123
+ delete: vi.fn(),
124
+ clear: vi.fn(),
125
+ check: vi.fn(() => true),
126
+ load: vi.fn(() => Promise.resolve([])),
127
+ ready: Promise.resolve([]),
128
+ },
129
+ writable: true,
130
+ })