Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion website/src/guide/findByName.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* happy-dom lays nothing out, so every element is given a box here; what is
* hidden is hidden the way the page hides it (not rendered, or `hidden`).
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, onTestFinished, vi } from 'vitest'
import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react'
import { useState } from 'react'
import {
Expand Down Expand Up @@ -566,6 +566,29 @@ describe('probeForName', () => {
expect(document.querySelector('[role="dialog"]')).toBeNull()
})

it.each([
['a popover', <Popover key="p"><PopoverTrigger asChild><button>Filters</button></PopoverTrigger><PopoverContent><button>Apply filter</button></PopoverContent></Popover>, 'Apply filter'],
['a menu', <Menu key="m" label="More actions"><DropdownMenuItem>Export</DropdownMenuItem></Menu>, 'Export'],
['a local tab', <LocalTabs key="t" panes={{ General: <button>Save</button>, Advanced: <button>Purge cache</button> }} />, 'Purge cache'],
])('finds inside %s and puts it back when its settle wait ends before the page renders', async (_, page, label) => {
render(page)
// A busy machine: the settle timer comes due before React runs the task
// that renders the open (or the restore). Modelled by ending each settle
// wait at the next microtask instead of on the clock.
const SETTLE = 7
const realSetTimeout = globalThis.setTimeout
const timers = vi.spyOn(globalThis, 'setTimeout').mockImplementation(((fn: () => void, ms?: number, ...rest: unknown[]) => {
if (ms !== SETTLE) return realSetTimeout(fn, ms, ...rest)
queueMicrotask(fn)
return 0
}) as typeof setTimeout)
// Restored even when the test times out, which a finally would not reach.
onTestFinished(() => timers.mockRestore())
expect((await probe({ label }, { settleMs: SETTLE })).result).toBe('found')
expect(document.querySelector('[role="dialog"], [role="menu"]')).toBeNull()
if (label === 'Purge cache') expect(selectedTab()).toBe('General')
})

it('keeps what it opened from being dismissed by a focus change outside it, until the person acts', async () => {
render(
<>
Expand Down
10 changes: 8 additions & 2 deletions website/src/guide/findByName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* acted since the step began. A container no primitive registered is never
* opened: the guide asks the person to open it.
*/
import { flushSync } from 'react-dom'
import { i18nT } from '../i18n/t'
import { GUIDE_PLANS } from '../uiLocations/guidePlans.gen'
import { closestRegistered, registeredId } from '../uiLocations/targetRegistry'
Expand Down Expand Up @@ -598,7 +599,12 @@ async function probeOnce(q: FindQuery, opts: ProbeOptions): Promise<ProbeResult>
const before = new Set(probeTargets().map(e => e.id))
const saved = save(el)
opened += 1
const restore = entry.open()
// Rendered before the settle wait starts, not when React next gets a
// task: on a busy machine the wait can end first, and the probe would
// then search a closed container and skip a restore that the open
// reaches the page after.
let restore: () => void = () => {}
flushSync(() => { restore = entry.open() })
// Waited out even when aborted: the restore reads the primitive's
// state, so what the open did must have rendered first.
await sleep(settleMs)
Expand All @@ -614,7 +620,7 @@ async function probeOnce(q: FindQuery, opts: ProbeOptions): Promise<ProbeResult>
}
}
if (interrupted) return { result: 'none' }
restore()
flushSync(restore)
await sleep(settleMs)
if (interrupted) return { result: 'none' }
restoreView(saved)
Expand Down
Loading