Skip to content

Commit cf20ec5

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 9ae27bf + 44db751 commit cf20ec5

13 files changed

Lines changed: 703 additions & 63 deletions

File tree

src/apps/web/src/__tests__/appUI.test.tsx

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { act } from 'react'
1+
import { act, useEffect, useState } from 'react'
22
import { createRoot } from 'react-dom/client'
33
import { MemoryRouter } from 'react-router-dom'
44
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
55

6-
import { AppUIProvider, useSidebarUI } from '../contexts/app-ui'
6+
import { AppUIProvider, useSettingsUI, useSidebarUI, useTitleBarRightPanelUI } from '../contexts/app-ui'
77
import { AuthContextBridge, type AuthContextValue } from '../contexts/auth'
88
import { DesktopTitleBar } from '../components/DesktopTitleBar'
99
import { LocaleProvider } from '../contexts/LocaleContext'
@@ -33,6 +33,41 @@ function SidebarProbe() {
3333
)
3434
}
3535

36+
function SettingsProbe() {
37+
const { settingsOpen, settingsInitialTab } = useSettingsUI()
38+
39+
return (
40+
<div>
41+
<span data-testid="settings-open">{settingsOpen ? 'open' : 'closed'}</span>
42+
<span data-testid="settings-tab">{settingsInitialTab}</span>
43+
</div>
44+
)
45+
}
46+
47+
function RightPanelShortcutProbe() {
48+
const { rightPanelOpen, setRightPanelOpen } = useSidebarUI()
49+
const { setTitleBarRightPanelClick } = useTitleBarRightPanelUI()
50+
const [visible, setVisible] = useState(false)
51+
52+
useEffect(() => {
53+
setTitleBarRightPanelClick(() => {
54+
setVisible((open) => !open)
55+
})
56+
return () => setTitleBarRightPanelClick(null)
57+
}, [setTitleBarRightPanelClick])
58+
59+
useEffect(() => {
60+
setRightPanelOpen(visible)
61+
}, [visible, setRightPanelOpen])
62+
63+
return (
64+
<div>
65+
<span data-testid="right-panel-visible">{visible ? 'open' : 'closed'}</span>
66+
<span data-testid="right-panel-icon">{rightPanelOpen ? 'open' : 'closed'}</span>
67+
</div>
68+
)
69+
}
70+
3671
describe('AppUIProvider sidebar state', () => {
3772
const authValue: AuthContextValue = {
3873
me: null,
@@ -43,11 +78,13 @@ describe('AppUIProvider sidebar state', () => {
4378
}
4479

4580
const originalInnerWidth = window.innerWidth
81+
const originalNavigatorPlatform = Object.getOwnPropertyDescriptor(window.navigator, 'platform')
4682
const originalActEnvironment = (globalThis as typeof globalThis & {
4783
IS_REACT_ACT_ENVIRONMENT?: boolean
4884
}).IS_REACT_ACT_ENVIRONMENT
4985

5086
beforeEach(() => {
87+
desktopMock.isDesktop.mockReturnValue(true)
5188
vi.useFakeTimers()
5289
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => setTimeout(() => cb(0), 0))
5390
vi.stubGlobal('cancelAnimationFrame', (id: number) => clearTimeout(id))
@@ -78,6 +115,11 @@ describe('AppUIProvider sidebar state', () => {
78115
IS_REACT_ACT_ENVIRONMENT?: boolean
79116
}).IS_REACT_ACT_ENVIRONMENT = originalActEnvironment
80117
}
118+
if (originalNavigatorPlatform) {
119+
Object.defineProperty(window.navigator, 'platform', originalNavigatorPlatform)
120+
} else {
121+
Reflect.deleteProperty(window.navigator, 'platform')
122+
}
81123
})
82124

83125
it('保留手动折叠状态,即使跨过宽度断点后再回来', async () => {
@@ -148,6 +190,112 @@ describe('AppUIProvider sidebar state', () => {
148190
})
149191
container.remove()
150192
})
193+
194+
it('全局设置快捷键打开 settings 页签并可再次关闭', async () => {
195+
desktopMock.isDesktop.mockReturnValue(false)
196+
Object.defineProperty(window.navigator, 'platform', {
197+
configurable: true,
198+
value: 'Win32',
199+
})
200+
201+
const container = document.createElement('div')
202+
document.body.appendChild(container)
203+
const root = createRoot(container)
204+
205+
await act(async () => {
206+
root.render(
207+
<MemoryRouter initialEntries={['/']}>
208+
<AuthContextBridge value={authValue}>
209+
<AppUIProvider>
210+
<SettingsProbe />
211+
</AppUIProvider>
212+
</AuthContextBridge>
213+
</MemoryRouter>,
214+
)
215+
})
216+
217+
expect(container.querySelector('[data-testid="settings-open"]')?.textContent).toBe('closed')
218+
expect(container.querySelector('[data-testid="settings-tab"]')?.textContent).toBe('account')
219+
220+
let event = new KeyboardEvent('keydown', {
221+
key: ',',
222+
code: 'Comma',
223+
ctrlKey: true,
224+
bubbles: true,
225+
cancelable: true,
226+
})
227+
await act(async () => {
228+
window.dispatchEvent(event)
229+
})
230+
231+
expect(event.defaultPrevented).toBe(true)
232+
expect(container.querySelector('[data-testid="settings-open"]')?.textContent).toBe('open')
233+
expect(container.querySelector('[data-testid="settings-tab"]')?.textContent).toBe('settings')
234+
235+
event = new KeyboardEvent('keydown', {
236+
key: ',',
237+
code: 'Comma',
238+
ctrlKey: true,
239+
bubbles: true,
240+
cancelable: true,
241+
})
242+
await act(async () => {
243+
window.dispatchEvent(event)
244+
})
245+
246+
expect(container.querySelector('[data-testid="settings-open"]')?.textContent).toBe('closed')
247+
248+
act(() => {
249+
root.unmount()
250+
})
251+
container.remove()
252+
})
253+
254+
it('右侧面板快捷键复用标题栏回调路径', async () => {
255+
desktopMock.isDesktop.mockReturnValue(false)
256+
Object.defineProperty(window.navigator, 'platform', {
257+
configurable: true,
258+
value: 'Win32',
259+
})
260+
261+
const container = document.createElement('div')
262+
document.body.appendChild(container)
263+
const root = createRoot(container)
264+
265+
await act(async () => {
266+
root.render(
267+
<MemoryRouter initialEntries={['/']}>
268+
<AuthContextBridge value={authValue}>
269+
<AppUIProvider>
270+
<RightPanelShortcutProbe />
271+
</AppUIProvider>
272+
</AuthContextBridge>
273+
</MemoryRouter>,
274+
)
275+
})
276+
277+
expect(container.querySelector('[data-testid="right-panel-visible"]')?.textContent).toBe('closed')
278+
expect(container.querySelector('[data-testid="right-panel-icon"]')?.textContent).toBe('closed')
279+
280+
await act(async () => {
281+
window.dispatchEvent(new KeyboardEvent('keydown', {
282+
key: 'b',
283+
code: 'KeyB',
284+
altKey: true,
285+
ctrlKey: true,
286+
bubbles: true,
287+
cancelable: true,
288+
}))
289+
})
290+
291+
expect(container.querySelector('[data-testid="right-panel-visible"]')?.textContent).toBe('open')
292+
expect(container.querySelector('[data-testid="right-panel-icon"]')?.textContent).toBe('open')
293+
294+
act(() => {
295+
root.unmount()
296+
})
297+
container.remove()
298+
})
151299
})
152300

153301
describe('DesktopTitleBar update entry', () => {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { afterEach, describe, expect, it } from 'vitest'
2+
3+
import { SHORTCUTS, formatShortcut, matchesShortcut } from '../shortcuts'
4+
5+
const originalPlatform = navigator.platform
6+
7+
afterEach(() => {
8+
Object.defineProperty(navigator, 'platform', {
9+
configurable: true,
10+
value: originalPlatform,
11+
})
12+
})
13+
14+
function setPlatform(platform: string) {
15+
Object.defineProperty(navigator, 'platform', {
16+
configurable: true,
17+
value: platform,
18+
})
19+
}
20+
21+
describe('shortcuts', () => {
22+
it('区分侧栏和右侧面板的 B 快捷键', () => {
23+
setPlatform('MacIntel')
24+
25+
const sidebarEvent = new KeyboardEvent('keydown', { key: 'b', metaKey: true })
26+
const rightPanelEvent = new KeyboardEvent('keydown', { key: 'b', altKey: true, metaKey: true })
27+
28+
expect(matchesShortcut(sidebarEvent, SHORTCUTS.toggleSidebar)).toBe(true)
29+
expect(matchesShortcut(sidebarEvent, SHORTCUTS.toggleRightPanel)).toBe(false)
30+
expect(matchesShortcut(rightPanelEvent, SHORTCUTS.toggleRightPanel)).toBe(true)
31+
expect(matchesShortcut(rightPanelEvent, SHORTCUTS.toggleSidebar)).toBe(false)
32+
})
33+
34+
it('使用物理按键匹配 Option 改写后的字符', () => {
35+
setPlatform('MacIntel')
36+
37+
const rightPanelEvent = new KeyboardEvent('keydown', {
38+
key: '∫',
39+
code: 'KeyB',
40+
altKey: true,
41+
metaKey: true,
42+
})
43+
44+
expect(matchesShortcut(rightPanelEvent, SHORTCUTS.toggleRightPanel)).toBe(true)
45+
})
46+
47+
it('拒绝混入非当前平台的主修饰键', () => {
48+
setPlatform('MacIntel')
49+
expect(matchesShortcut(new KeyboardEvent('keydown', {
50+
key: ',',
51+
code: 'Comma',
52+
ctrlKey: true,
53+
metaKey: true,
54+
}), SHORTCUTS.openSettings)).toBe(false)
55+
56+
setPlatform('Win32')
57+
expect(matchesShortcut(new KeyboardEvent('keydown', {
58+
key: ',',
59+
code: 'Comma',
60+
ctrlKey: true,
61+
metaKey: true,
62+
}), SHORTCUTS.openSettings)).toBe(false)
63+
})
64+
65+
it('按平台显示修饰键', () => {
66+
expect(formatShortcut(SHORTCUTS.toggleSidebar.binding, 'mac')).toEqual(['⌘', 'B'])
67+
expect(formatShortcut(SHORTCUTS.toggleRightPanel.binding, 'mac')).toEqual(['⌥', '⌘', 'B'])
68+
expect(formatShortcut(SHORTCUTS.toggleSidebar.binding, 'other')).toEqual(['Ctrl', 'B'])
69+
expect(formatShortcut(SHORTCUTS.toggleRightPanel.binding, 'other')).toEqual(['Alt', 'Ctrl', 'B'])
70+
})
71+
})

0 commit comments

Comments
 (0)