Skip to content

Commit cff5a1c

Browse files
committed
fix(query-sync): switch to throttle; repair stale megapixels e2e spec
The first debounce attempt had no useEffect dep array, so the timer was reset on every render (not just state changes) — if the tool re-renders frequently, the URL never updates. Switched to a throttle keyed on the serialized query string: effect only re-runs when the URL would actually change, and max one replaceState per 200ms window. Also fixes pre-existing megapixels e2e failures masked by the earlier break: - Filter pagead/googleads CSP warnings from console-error assertion - Click the wrapping label instead of the display:none checkbox input - Drop the removed `custom-mp-name` field; the form now auto-labels from the MP value
1 parent 339eb28 commit cff5a1c

4 files changed

Lines changed: 34 additions & 20 deletions

File tree

src/app/[locale]/fov-simulator/_components/querySync.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,22 @@ export function stateToQueryString(state: FovSimulatorState): string {
6060
export function useQuerySync(state: FovSimulatorState): void {
6161
const isFirstRender = useRef(true)
6262
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
63+
const qs = stateToQueryString(state)
64+
const qsRef = useRef(qs)
65+
qsRef.current = qs
6366

6467
useEffect(() => {
6568
if (isFirstRender.current) {
6669
isFirstRender.current = false
6770
return
6871
}
69-
if (timerRef.current) clearTimeout(timerRef.current)
72+
if (timerRef.current) return
7073
timerRef.current = setTimeout(() => {
71-
const qs = stateToQueryString(state)
72-
const newUrl = `${window.location.pathname}?${qs}`
74+
const newUrl = `${window.location.pathname}?${qsRef.current}`
7375
window.history.replaceState(null, '', newUrl)
76+
timerRef.current = null
7477
}, 200)
75-
return () => { if (timerRef.current) clearTimeout(timerRef.current) }
76-
}, [state])
78+
}, [qs])
79+
80+
useEffect(() => () => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null } }, [])
7781
}

src/app/[locale]/perspective-compression-simulator/_components/compressionState.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,24 @@ function stateToQueryString(state: State): string {
8787
export function useQuerySync(state: State): void {
8888
const isFirstRender = useRef(true)
8989
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
90+
const qs = stateToQueryString(state)
91+
const qsRef = useRef(qs)
92+
qsRef.current = qs
9093

9194
useEffect(() => {
9295
if (isFirstRender.current) {
9396
isFirstRender.current = false
9497
return
9598
}
96-
if (timerRef.current) clearTimeout(timerRef.current)
99+
if (timerRef.current) return
97100
timerRef.current = setTimeout(() => {
98-
const qs = stateToQueryString(state)
99-
const newUrl = `${window.location.pathname}?${qs}`
101+
const newUrl = `${window.location.pathname}?${qsRef.current}`
100102
window.history.replaceState(null, '', newUrl)
103+
timerRef.current = null
101104
}, 200)
102-
return () => { if (timerRef.current) clearTimeout(timerRef.current) }
103-
}, [state])
105+
}, [qs])
106+
107+
useEffect(() => () => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null } }, [])
104108
}
105109

106110
export const LOG_MIN = Math.log(14)

src/e2e/tools/megapixels-size-visualizer.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ test.describe('Megapixels Size Visualizer', () => {
1818
!e.includes('favicon') &&
1919
!e.includes('cookieyes') &&
2020
!e.includes('adsbygoogle') &&
21+
!e.includes('googlesyndication') &&
22+
!e.includes('googleads') &&
2123
!e.includes('_vercel/speed-insights'),
2224
)
2325
expect(critical).toEqual([])
@@ -31,17 +33,18 @@ test.describe('Megapixels Size Visualizer', () => {
3133

3234
test('toggling an MP updates the URL', async ({ page }) => {
3335
await page.goto(URL)
34-
const checkbox = page.locator('[data-testid="mp-toggle-mp_45"]')
36+
const checkbox = page.locator('[data-testid="mp-toggle-mp_45"]').first()
3537
await expect(checkbox).toBeChecked()
36-
await checkbox.uncheck()
38+
// The native input is display:none with a styled label sibling; click the enclosing label.
39+
await checkbox.locator('xpath=ancestor::label').first().click()
40+
await expect(checkbox).not.toBeChecked()
3741
await expect(page).toHaveURL(/show=/)
3842
})
3943

4044
test('adding a custom MP persists it in the checkbox list', async ({ page }) => {
4145
await page.goto(URL)
42-
await page.locator('[data-testid="custom-mp-name"]').first().fill('MyCam')
4346
await page.locator('[data-testid="custom-mp-value"]').first().fill('75')
4447
await page.locator('[data-testid="custom-mp-add"]').first().click()
45-
await expect(page.getByText(/MyCam/).first()).toBeVisible()
48+
await expect(page.getByText(/75 MP/).first()).toBeVisible()
4649
})
4750
})

src/lib/utils/querySync.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,23 @@ export function stateToQuery<S extends Record<string, unknown>>(state: S, schema
5252
return parts.join('&')
5353
}
5454

55-
/** Sync state to URL query params (replaceState). Skips first render. Debounced to stay under browser throttling limits (Safari: 100 calls / 10s). */
55+
/** Sync state to URL query params (replaceState). Skips first render. Throttled to stay under browser limits (Safari: 100 calls / 10s). */
5656
export function useToolQuerySync<S extends Record<string, unknown>>(state: S, schema: { [K in keyof S]: ParamDef<S[K]> }): void {
5757
const isFirst = useRef(true)
5858
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
59+
const qs = stateToQuery(state, schema)
60+
const qsRef = useRef(qs)
61+
qsRef.current = qs
5962
useEffect(() => {
6063
if (isFirst.current) { isFirst.current = false; return }
61-
if (timerRef.current) clearTimeout(timerRef.current)
64+
if (timerRef.current) return
6265
timerRef.current = setTimeout(() => {
63-
const qs = stateToQuery(state, schema)
64-
const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname
66+
const url = qsRef.current ? `${window.location.pathname}?${qsRef.current}` : window.location.pathname
6567
window.history.replaceState(null, '', url)
68+
timerRef.current = null
6669
}, 200)
67-
return () => { if (timerRef.current) clearTimeout(timerRef.current) }
68-
})
70+
}, [qs])
71+
useEffect(() => () => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null } }, [])
6972
}
7073

7174
/**

0 commit comments

Comments
 (0)