Lixen commited on
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 |
+
})
|