Skip to content

Commit 31d493f

Browse files
msujawsclaude
andcommitted
fix(app): auto-fetch bugs when URL has filters and API key loads
Previously, when the page loaded with URL filters (e.g., ?whiteboard=[kanban]), bugs weren't fetched automatically even with a valid stored API key. This was due to a race condition where the filter initialization effect ran before loadApiKey() completed. Split the single effect into two: - Filter initialization (runs once on mount) - Auto-fetch (triggers when API key becomes available with URL filters) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a98b366 commit 31d493f

File tree

2 files changed

+132
-9
lines changed

2 files changed

+132
-9
lines changed

src/App.test.tsx

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { describe, it, expect, beforeEach, vi } from 'vitest'
2-
import { render, screen } from '@testing-library/react'
1+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2+
import { render, screen, waitFor } from '@testing-library/react'
33
import App from './App'
44
import { useStore } from './store'
55

66
describe('App', () => {
7+
const originalLocation = window.location
8+
79
beforeEach(() => {
810
// Reset store state before each test
911
useStore.setState({
@@ -12,11 +14,40 @@ describe('App', () => {
1214
bugs: [],
1315
isLoading: false,
1416
error: '',
15-
filters: { whiteboardTag: '', component: '' },
17+
filters: { whiteboardTag: '', component: '', excludeMetaBugs: false, sortOrder: 'priority' },
1618
changes: new Map(),
1719
isApplying: false,
1820
applyError: '',
1921
})
22+
23+
// Reset URL to base
24+
const mockLocation = {
25+
search: '',
26+
pathname: '/',
27+
href: 'http://localhost:3000/',
28+
origin: 'http://localhost:3000',
29+
host: 'localhost:3000',
30+
hostname: 'localhost',
31+
port: '3000',
32+
protocol: 'http:',
33+
hash: '',
34+
assign: vi.fn(),
35+
reload: vi.fn(),
36+
replace: vi.fn(),
37+
toString: () => 'http://localhost:3000/',
38+
}
39+
Object.defineProperty(window, 'location', {
40+
value: mockLocation,
41+
writable: true,
42+
})
43+
})
44+
45+
afterEach(() => {
46+
vi.restoreAllMocks()
47+
Object.defineProperty(window, 'location', {
48+
value: originalLocation,
49+
writable: true,
50+
})
2051
})
2152

2253
describe('without API key', () => {
@@ -143,4 +174,91 @@ describe('App', () => {
143174
const appContainer = container.querySelector('.min-h-screen')
144175
expect(appContainer).toHaveClass('bg-bg-primary')
145176
})
177+
178+
describe('auto-fetch with URL filters', () => {
179+
it('should auto-fetch bugs when URL has filters and API key is loaded', async () => {
180+
// Setup: URL has filters
181+
window.location.search = '?whiteboard=%5Bkanban%5D'
182+
183+
const fetchBugs = vi.fn().mockResolvedValue([])
184+
185+
// Simulate async API key loading - resolves after a short delay
186+
const loadApiKey = vi.fn().mockImplementation(async () => {
187+
await new Promise((resolve) => {
188+
setTimeout(resolve, 10)
189+
})
190+
useStore.setState({ apiKey: 'test-api-key', isValid: true })
191+
})
192+
193+
useStore.setState({
194+
loadApiKey,
195+
fetchBugs,
196+
apiKey: '', // No API key initially
197+
})
198+
199+
render(<App />)
200+
201+
// Wait for loadApiKey to complete and fetchBugs to be called
202+
await waitFor(() => {
203+
expect(fetchBugs).toHaveBeenCalledWith('test-api-key')
204+
})
205+
})
206+
207+
it('should not auto-fetch when URL has no filters even with API key', async () => {
208+
// Setup: URL has no filters (search is empty)
209+
window.location.search = ''
210+
211+
const fetchBugs = vi.fn().mockResolvedValue([])
212+
const loadApiKey = vi.fn().mockImplementation(() => {
213+
useStore.setState({ apiKey: 'test-api-key', isValid: true })
214+
return Promise.resolve()
215+
})
216+
217+
useStore.setState({
218+
loadApiKey,
219+
fetchBugs,
220+
apiKey: '',
221+
})
222+
223+
render(<App />)
224+
225+
// Wait a bit to ensure no auto-fetch happens
226+
await new Promise((resolve) => {
227+
setTimeout(resolve, 50)
228+
})
229+
230+
expect(fetchBugs).not.toHaveBeenCalled()
231+
})
232+
233+
it('should apply filters from URL to store before fetching', async () => {
234+
window.location.search = '?whiteboard=%5Bkanban%5D&component=Core'
235+
236+
const fetchBugs = vi.fn().mockResolvedValue([])
237+
const setFilters = vi.fn()
238+
const loadApiKey = vi.fn().mockImplementation(async () => {
239+
await new Promise((resolve) => {
240+
setTimeout(resolve, 10)
241+
})
242+
useStore.setState({ apiKey: 'test-api-key', isValid: true })
243+
})
244+
245+
useStore.setState({
246+
loadApiKey,
247+
fetchBugs,
248+
setFilters,
249+
apiKey: '',
250+
})
251+
252+
render(<App />)
253+
254+
await waitFor(() => {
255+
expect(setFilters).toHaveBeenCalledWith(
256+
expect.objectContaining({
257+
whiteboardTag: '[kanban]',
258+
component: 'Core',
259+
}),
260+
)
261+
})
262+
})
263+
})
146264
})

src/App.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ function App() {
2727
// URL filter persistence
2828
const { initialFilters, hasUrlFilters, updateUrl } = useUrlFilters()
2929
const hasInitializedFilters = useRef(false)
30+
const hasAutoFetched = useRef(false)
3031

3132
// Bugs state
3233
const bugs = useStore((state) => state.bugs)
@@ -67,7 +68,7 @@ function App() {
6768
}
6869
}, [changes.size])
6970

70-
// Initialize filters from URL on mount and auto-fetch if URL has filters
71+
// Initialize filters from URL on mount
7172
useEffect(() => {
7273
if (hasInitializedFilters.current) {
7374
return
@@ -77,13 +78,17 @@ function App() {
7778
// Apply filters from URL
7879
if (hasUrlFilters) {
7980
setFilters(initialFilters)
81+
}
82+
}, [hasUrlFilters, initialFilters, setFilters])
8083

81-
// Auto-fetch if API key is available
82-
if (apiKey) {
83-
void fetchBugs(apiKey)
84-
}
84+
// Auto-fetch when URL has filters and API key becomes available
85+
useEffect(() => {
86+
if (!hasUrlFilters || hasAutoFetched.current || !apiKey) {
87+
return
8588
}
86-
}, [hasUrlFilters, initialFilters, setFilters, apiKey, fetchBugs])
89+
hasAutoFetched.current = true
90+
void fetchBugs(apiKey)
91+
}, [hasUrlFilters, apiKey, fetchBugs])
8792

8893
// Sync filters to URL when they change (after initialization)
8994
useEffect(() => {

0 commit comments

Comments
 (0)