fix(host): review round 14 — shared hanging-lister fake; create-supersession and closing-commit contracts documented
This commit is contained in:
@@ -12,7 +12,10 @@
|
|||||||
* panes away from the display root. Selecting in the
|
* panes away from the display root. Selecting in the
|
||||||
* right column shifts the view one level deeper. "New folder" opens a nested
|
* right column shifts the view one level deeper. "New folder" opens a nested
|
||||||
* create dialog targeting the selected folder (or the level itself) and
|
* create dialog targeting the selected folder (or the level itself) and
|
||||||
* selects the created folder. Open adopts the selected folder, falling back
|
* selects the created folder — unless a newer pick or crumb jump supersedes
|
||||||
|
* the post-create relist, in which case neither the level nor the selection
|
||||||
|
* refreshes (see closeCreateDialog's two-stage parking for the matching
|
||||||
|
* focus story). Open adopts the selected folder, falling back
|
||||||
* to the listed level. Pure consumer of the injected browse calls — the
|
* to the listed level. Pure consumer of the injected browse calls — the
|
||||||
* owning flow decides what "Open" means and owns the workspace-creation
|
* owning flow decides what "Open" means and owns the workspace-creation
|
||||||
* error surface. Hidden entries are host-flagged and hidden by default; the
|
* error surface. Hidden entries are host-flagged and hidden by default; the
|
||||||
@@ -275,8 +278,10 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
|
|||||||
/**
|
/**
|
||||||
* Whether the focused element sits among the miller rows — probed before
|
* Whether the focused element sits among the miller rows — probed before
|
||||||
* a landing replaces the row nodes, to decide focus parking. A probe
|
* a landing replaces the row nodes, to decide focus parking. A probe
|
||||||
* only: it never gates the landing itself (a torn-down ref in a close
|
* only: it never gates the landing itself — a torn-down ref in a close
|
||||||
* race merely skips the parking).
|
* race merely skips the parking, and committing the landing into a
|
||||||
|
* closing dialog is safe (the component already renders null, and the
|
||||||
|
* open effect resets parent/selected/child on the next open).
|
||||||
* @returns true when `document.activeElement` is inside the miller row.
|
* @returns true when `document.activeElement` is inside the miller row.
|
||||||
*/
|
*/
|
||||||
const focusInMillerRows = useCallback((): boolean => {
|
const focusInMillerRows = useCallback((): boolean => {
|
||||||
|
|||||||
@@ -100,6 +100,22 @@ function mount(overrides: Partial<Parameters<typeof DirectoryBrowser>[0]> = {})
|
|||||||
return { view, props, listDirectory, createDirectory, onOpen, onClose }
|
return { view, props, listDirectory, createDirectory, onOpen, onClose }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A listDirectory fake whose explicit HOME listings hang for manual
|
||||||
|
* settlement — the initial open lists home through the absent-path form,
|
||||||
|
* so only relists and parent legs are held.
|
||||||
|
*/
|
||||||
|
function hangingHomeLister() {
|
||||||
|
const settlers: { resolve: (value: DirectoryListing) => void; reject: (reason: unknown) => void }[] = []
|
||||||
|
const listDirectory = vi.fn(async (path?: string) => {
|
||||||
|
if (path === HOME) {
|
||||||
|
return new Promise<DirectoryListing>((resolve, reject) => { settlers.push({ resolve, reject }) })
|
||||||
|
}
|
||||||
|
return listingFor(path)
|
||||||
|
})
|
||||||
|
return { listDirectory, settlers }
|
||||||
|
}
|
||||||
|
|
||||||
/** The rendered level columns, left-to-right. */
|
/** The rendered level columns, left-to-right. */
|
||||||
function columns(): HTMLElement[] {
|
function columns(): HTMLElement[] {
|
||||||
return screen.getAllByRole('list')
|
return screen.getAllByRole('list')
|
||||||
@@ -270,15 +286,7 @@ describe('DirectoryBrowser', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('a pick during the post-create relist supersedes it: late settlements drop', async () => {
|
it('a pick during the post-create relist supersedes it: late settlements drop', async () => {
|
||||||
const settlers: { resolve: (value: DirectoryListing) => void; reject: (reason: unknown) => void }[] = []
|
const { listDirectory, settlers } = hangingHomeLister()
|
||||||
const listDirectory = vi.fn(async (path?: string) => {
|
|
||||||
// Explicit HOME requests are the relists; the initial open lists home
|
|
||||||
// through the absent-path form.
|
|
||||||
if (path === HOME) {
|
|
||||||
return new Promise<DirectoryListing>((resolve, reject) => { settlers.push({ resolve, reject }) })
|
|
||||||
}
|
|
||||||
return listingFor(path)
|
|
||||||
})
|
|
||||||
mount({ listDirectory })
|
mount({ listDirectory })
|
||||||
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'browser.newFolder' }))
|
fireEvent.click(screen.getByRole('button', { name: 'browser.newFolder' }))
|
||||||
@@ -291,18 +299,11 @@ describe('DirectoryBrowser', () => {
|
|||||||
await waitFor(() => { expect(columns()).toHaveLength(2) })
|
await waitFor(() => { expect(columns()).toHaveLength(2) })
|
||||||
// The stale relist settles late and must not land or select ghost.
|
// The stale relist settles late and must not land or select ghost.
|
||||||
await act(async () => { settlers[0]!.resolve({ ...listingFor(HOME), entries: [] }) })
|
await act(async () => { settlers[0]!.resolve({ ...listingFor(HOME), entries: [] }) })
|
||||||
expect(within(columns()[0]!).getByText('Documents')).toBeTruthy()
|
|
||||||
expect(rowButton(within(columns()[0]!).getByRole('listitem')).getAttribute('aria-current')).toBe('true')
|
expect(rowButton(within(columns()[0]!).getByRole('listitem')).getAttribute('aria-current')).toBe('true')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('a rejection of a superseded post-create relist is equally silent', async () => {
|
it('a rejection of a superseded post-create relist is equally silent', async () => {
|
||||||
const settlers: { resolve: (value: DirectoryListing) => void; reject: (reason: unknown) => void }[] = []
|
const { listDirectory, settlers } = hangingHomeLister()
|
||||||
const listDirectory = vi.fn(async (path?: string) => {
|
|
||||||
if (path === HOME) {
|
|
||||||
return new Promise<DirectoryListing>((resolve, reject) => { settlers.push({ resolve, reject }) })
|
|
||||||
}
|
|
||||||
return listingFor(path)
|
|
||||||
})
|
|
||||||
mount({ listDirectory })
|
mount({ listDirectory })
|
||||||
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'browser.newFolder' }))
|
fireEvent.click(screen.getByRole('button', { name: 'browser.newFolder' }))
|
||||||
@@ -395,13 +396,7 @@ describe('DirectoryBrowser', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('re-parks focus on the re-selected row when the upgrade displaces focused rows', async () => {
|
it('re-parks focus on the re-selected row when the upgrade displaces focused rows', async () => {
|
||||||
const settlers: ((value: DirectoryListing) => void)[] = []
|
const { listDirectory, settlers } = hangingHomeLister()
|
||||||
const listDirectory = vi.fn(async (path?: string) => {
|
|
||||||
if (path === HOME) {
|
|
||||||
return new Promise<DirectoryListing>((resolve) => { settlers.push(resolve) })
|
|
||||||
}
|
|
||||||
return listingFor(path)
|
|
||||||
})
|
|
||||||
mount({ listDirectory })
|
mount({ listDirectory })
|
||||||
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
await waitFor(() => { expect(screen.getByRole('listitem')).toBeTruthy() })
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
|
fireEvent.click(screen.getByRole('button', { name: 'browser.editPath' }))
|
||||||
@@ -414,7 +409,7 @@ describe('DirectoryBrowser', () => {
|
|||||||
await waitFor(() => { expect(settlers).toHaveLength(1) })
|
await waitFor(() => { expect(settlers).toHaveLength(1) })
|
||||||
// The upgrade replaces every committed row node; focus re-parks on the
|
// The upgrade replaces every committed row node; focus re-parks on the
|
||||||
// re-selected row instead of falling to body.
|
// re-selected row instead of falling to body.
|
||||||
await act(async () => { settlers[0]!(listingFor(HOME)) })
|
await act(async () => { settlers[0]!.resolve(listingFor(HOME)) })
|
||||||
await waitFor(() => { expect(columns()).toHaveLength(2) })
|
await waitFor(() => { expect(columns()).toHaveLength(2) })
|
||||||
expect(document.activeElement?.textContent).toBe('Documents')
|
expect(document.activeElement?.textContent).toBe('Documents')
|
||||||
expect(document.activeElement?.getAttribute('aria-current')).toBe('true')
|
expect(document.activeElement?.getAttribute('aria-current')).toBe('true')
|
||||||
|
|||||||
Reference in New Issue
Block a user