Skip to content

Commit 0f096f3

Browse files
authored
Slide panel tests (#174)
* Fix folder test warnings
1 parent 0624602 commit 0f096f3

File tree

6 files changed

+216
-17
lines changed

6 files changed

+216
-17
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![npm](https://img.shields.io/npm/v/hyperparam)](https://www.npmjs.com/package/hyperparam)
44
[![workflow status](https://github.com/hyparam/hyperparam-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/hyparam/hyperparam-cli/actions)
55
[![mit license](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6-
![coverage](https://img.shields.io/badge/Coverage-56-darkred)
6+
![coverage](https://img.shields.io/badge/Coverage-59-darkred)
77

88
This is the hyperparam cli tool.
99

src/components/Folder.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default function Folder({ source, config }: FolderProps) {
6666
searchElement?.addEventListener('keyup', handleKeyup)
6767
// Clean up event listener
6868
return () => searchElement?.removeEventListener('keyup', handleKeyup)
69-
})
69+
}, [filtered, source.prefix])
7070

7171
// Jump to search box if user types '/'
7272
useEffect(() => {
@@ -80,7 +80,7 @@ export default function Folder({ source, config }: FolderProps) {
8080
}
8181
document.addEventListener('keydown', handleKeydown)
8282
return () => { document.removeEventListener('keydown', handleKeydown) }
83-
})
83+
}, [])
8484

8585
return <Layout error={error} title={source.prefix}>
8686
<Breadcrumb source={source} config={config}>

src/components/viewers/SlidePanel.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export default function SlidePanel({
3939
// Load initial panel width from localStorage if available
4040
const [panelWidth, setPanelWidth] = useState<number>(() => {
4141
const savedWidth = typeof window !== 'undefined' ? localStorage.getItem('panelWidth') : null
42-
return savedWidth ? parseInt(savedWidth, 10) : defaultWidth
42+
const parsedWidth = savedWidth ? parseInt(savedWidth, 10) : NaN
43+
return !isNaN(parsedWidth) ? parsedWidth : defaultWidth
4344
})
4445

4546
useEffect(() => {

test/components/Folder.test.tsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,16 @@ describe('Folder Component', () => {
4646

4747
it('displays the spinner while loading', async () => {
4848
vi.mocked(fetch).mockResolvedValueOnce({
49-
json: () => Promise.resolve([]),
49+
// resolve in 50ms
50+
json: () => new Promise(resolve => setTimeout(() => { resolve([]) }, 50)),
5051
ok: true,
5152
} as Response)
5253

5354
const source = getHyperparamSource('', { endpoint })
5455
assert(source?.kind === 'directory')
5556

5657
const { container } = await act(() => render(<Folder source={source} />))
57-
expect(container.querySelector('.spinner')).toBeDefined()
58+
expect(container.querySelector('.spinner')).toBeTruthy()
5859
})
5960

6061
it('handles file listing errors', async () => {
@@ -111,8 +112,10 @@ describe('Folder Component', () => {
111112
const { getByPlaceholderText, getByText, queryByText } = render(<Folder source={dirSource} />)
112113

113114
// Type a search query
114-
const searchInput = getByPlaceholderText('Search...')
115-
fireEvent.keyUp(searchInput, { target: { value: 'file1' } })
115+
const searchInput = getByPlaceholderText('Search...') as HTMLInputElement
116+
act(() => {
117+
fireEvent.keyUp(searchInput, { target: { value: 'file1' } })
118+
})
116119

117120
// Only matching files are displayed
118121
await waitFor(() => {
@@ -122,7 +125,10 @@ describe('Folder Component', () => {
122125
})
123126

124127
// Clear search with escape key
125-
fireEvent.keyUp(searchInput, { key: 'Escape' })
128+
act(() => {
129+
fireEvent.keyUp(searchInput, { key: 'Escape' })
130+
})
131+
126132
await waitFor(() => {
127133
expect(getByText('file1.txt')).toBeDefined()
128134
expect(getByText('folder1/')).toBeDefined()
@@ -151,19 +157,23 @@ describe('Folder Component', () => {
151157
const { getByPlaceholderText, getByText } = render(<Folder source={dirSource} />)
152158

153159
// Type a search query and hit enter
154-
const searchInput = getByPlaceholderText('Search...')
160+
const searchInput = getByPlaceholderText('Search...') as HTMLInputElement
155161
act(() => {
156162
fireEvent.keyUp(searchInput, { target: { value: 'file1' } })
157163
})
158164

159165
await waitFor(() => {
160166
expect(getByText('file1.txt')).toBeDefined()
161167
})
162-
fireEvent.keyUp(searchInput, { key: 'Enter' })
168+
169+
act(() => {
170+
fireEvent.keyUp(searchInput, { key: 'Enter' })
171+
})
172+
163173
expect(location.href).toBe('/files?key=file1.txt')
164174
})
165175

166-
it('jumps to search box when user types /', () => {
176+
it('jumps to search box when user types /', async () => {
167177
const dirSource: DirSource = {
168178
sourceId: 'test-source',
169179
sourceParts: [{ text: 'test-source', sourceId: 'test-source' }],
@@ -173,21 +183,34 @@ describe('Folder Component', () => {
173183
}
174184
const { getByPlaceholderText } = render(<Folder source={dirSource} />)
175185

186+
// Wait for component to settle
187+
await waitFor(() => {
188+
expect(fetch).toHaveBeenCalled()
189+
})
190+
176191
const searchInput = getByPlaceholderText('Search...') as HTMLInputElement
192+
177193
// Typing / should focus the search box
178-
fireEvent.keyDown(document.body, { key: '/' })
194+
act(() => {
195+
fireEvent.keyDown(document.body, { key: '/' })
196+
})
179197
expect(document.activeElement).toBe(searchInput)
180198

181199
// Typing inside the search box should work including /
182200
act(() => {
183201
fireEvent.keyUp(searchInput, { target: { value: 'file1/' } })
184-
expect(searchInput.value).toBe('file1/')
185202
})
203+
expect(searchInput.value).toBe('file1/')
186204

187205
// Unfocus and re-focus should select all text in search box
188-
searchInput.blur()
206+
act(() => {
207+
searchInput.blur()
208+
})
189209
expect(document.activeElement).not.toBe(searchInput)
190-
fireEvent.keyDown(document.body, { key: '/' })
210+
211+
act(() => {
212+
fireEvent.keyDown(document.body, { key: '/' })
213+
})
191214
expect(document.activeElement).toBe(searchInput)
192215
expect(searchInput.selectionStart).toBe(0)
193216
expect(searchInput.selectionEnd).toBe(searchInput.value.length)

test/components/Markdown.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,6 @@ describe('Markdown with nested elements', () => {
283283
const text = '![alt'
284284
const { container } = render(<Markdown text={text} />)
285285

286-
console.log(container.innerHTML)
287286
expect(container.textContent).toBe('![alt')
288287
expect(container.querySelector('img')).toBeNull()
289288
})
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */
2+
import React from 'react'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
import { act, fireEvent, render } from '@testing-library/react'
5+
import SlidePanel from '../../../src/components/viewers/SlidePanel.js'
6+
7+
describe('SlidePanel', () => {
8+
// Minimal localStorage mock
9+
const localStorageMock = (() => {
10+
let store: Record<string, string> = {}
11+
return {
12+
getItem: (key: string) => store[key] ?? null,
13+
setItem: (key: string, value: string) => { store[key] = value },
14+
clear: () => { store = {} },
15+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
16+
removeItem: (key: string) => { delete store[key] },
17+
}
18+
})()
19+
20+
beforeEach(() => {
21+
vi.stubGlobal('localStorage', localStorageMock)
22+
localStorage.clear()
23+
})
24+
25+
it('renders main and panel content', () => {
26+
const { getByText } = render(
27+
<SlidePanel
28+
mainContent={<div data-testid="main-content">Main</div>}
29+
panelContent={<div data-testid="panel-content">Panel</div>}
30+
isPanelOpen
31+
/>
32+
)
33+
expect(getByText('Main')).toBeDefined()
34+
expect(getByText('Panel')).toBeDefined()
35+
})
36+
37+
it('does not render the resizer if panel is closed', () => {
38+
const { container } = render(
39+
<SlidePanel
40+
mainContent={<div>Main</div>}
41+
panelContent={<div>Panel</div>}
42+
isPanelOpen={false}
43+
/>
44+
)
45+
const resizer = container.querySelector('.resizer')
46+
expect(resizer).toBeNull()
47+
})
48+
49+
it('uses default width of 400 when localStorage is empty', () => {
50+
const { container } = render(
51+
<SlidePanel
52+
mainContent={<div>Main</div>}
53+
panelContent={<div>Panel</div>}
54+
isPanelOpen
55+
/>
56+
)
57+
const panel = container.querySelector('.slidePanel') as HTMLElement
58+
expect(panel.style.width).toBe('400px')
59+
})
60+
61+
it('loads width from localStorage if present', () => {
62+
localStorage.setItem('panelWidth', '250')
63+
const { container } = render(
64+
<SlidePanel
65+
mainContent={<div>Main</div>}
66+
panelContent={<div>Panel</div>}
67+
isPanelOpen
68+
/>
69+
)
70+
const panel = container.querySelector('.slidePanel') as HTMLElement
71+
expect(panel.style.width).toBe('250px')
72+
})
73+
74+
it('falls back to default width if localStorage width is invalid', () => {
75+
localStorage.setItem('panelWidth', 'not-a-number')
76+
const { container } = render(
77+
<SlidePanel
78+
mainContent={<div>Main</div>}
79+
panelContent={<div>Panel</div>}
80+
isPanelOpen
81+
/>
82+
)
83+
const panel = container.querySelector('.slidePanel') as HTMLElement
84+
// parseInt of 'not-a-number' yields NaN so default width of 400 is expected
85+
expect(panel.style.width).toBe('400px')
86+
})
87+
88+
it('respects minWidth from config', () => {
89+
const { container } = render(
90+
<SlidePanel
91+
mainContent={<div>Main</div>}
92+
panelContent={<div>Panel</div>}
93+
isPanelOpen
94+
config={{ slidePanel: { minWidth: 300 } }}
95+
/>
96+
)
97+
const resizer = container.querySelector('.resizer') as HTMLElement
98+
const panel = container.querySelector('.slidePanel') as HTMLElement
99+
expect(panel.style.width).toBe('400px')
100+
101+
// Simulate mousedown on resizer with clientX 800
102+
act(() => {
103+
fireEvent.mouseDown(resizer, { clientX: 800 })
104+
})
105+
106+
// Simulate mousemove on document with clientX such that new width is less than minWidth
107+
act(() => {
108+
fireEvent.mouseMove(document, { clientX: 950 })
109+
fireEvent.mouseUp(document)
110+
})
111+
112+
// resizingClientX was set to 800 + 400 = 1200 so new width = max(300, 1200 - 950) = 300
113+
expect(panel.style.width).toBe('300px')
114+
})
115+
116+
it('handles dragging to resize', () => {
117+
const { container } = render(
118+
<SlidePanel
119+
mainContent={<div>Main</div>}
120+
panelContent={<div>Panel</div>}
121+
isPanelOpen
122+
/>
123+
)
124+
const resizer = container.querySelector('.resizer') as HTMLElement
125+
const panel = container.querySelector('.slidePanel') as HTMLElement
126+
expect(panel.style.width).toBe('400px')
127+
128+
// Mock panel's offsetWidth to be 400px
129+
Object.defineProperty(panel, 'offsetWidth', { value: 400, configurable: true })
130+
131+
// Simulate mousedown
132+
act(() => {
133+
fireEvent.mouseDown(resizer, { clientX: 800 })
134+
})
135+
136+
// Simulate dragging
137+
act(() => {
138+
fireEvent.mouseMove(document, { clientX: 750 })
139+
})
140+
141+
// End dragging
142+
act(() => {
143+
fireEvent.mouseUp(document)
144+
})
145+
146+
// Expected new width = 1200 - 750 = 450
147+
expect(panel.style.width).toBe('450px')
148+
expect(localStorage.getItem('panelWidth')).toBe('450')
149+
})
150+
151+
it('uses config defaultWidth if valid', () => {
152+
const { container } = render(
153+
<SlidePanel
154+
mainContent={<div>Main</div>}
155+
panelContent={<div>Panel</div>}
156+
isPanelOpen
157+
config={{ slidePanel: { defaultWidth: 500 } }}
158+
/>
159+
)
160+
const panel = container.querySelector('.slidePanel') as HTMLElement
161+
expect(panel.style.width).toBe('500px')
162+
})
163+
164+
it('ignores negative config.defaultWidth and uses 400 instead', () => {
165+
const { container } = render(
166+
<SlidePanel
167+
mainContent={<div>Main</div>}
168+
panelContent={<div>Panel</div>}
169+
isPanelOpen
170+
config={{ slidePanel: { defaultWidth: -10 } }}
171+
/>
172+
)
173+
const panel = container.querySelector('.slidePanel') as HTMLElement
174+
expect(panel.style.width).toBe('400px')
175+
})
176+
})

0 commit comments

Comments
 (0)