mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(docsync): a store picked before the panel settles stays picked
The effect that keeps the selection on a bound store read the pick from its own render. When a click landed after the card was committed but before that effect ran, the effect then wrote the first store back over the click. On a loaded CI runner this is what made FE-DOCSYNC-PANEL-008 fail now and then. The effect now reads the current pick when the update runs and only follows the list. FE-DOCSYNC-PANEL-023 forces that order and fails on the old effect.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-DOCSYNC-PANEL-001 to FE-DOCSYNC-PANEL-022
|
||||
// FE-DOCSYNC-PANEL-001 to FE-DOCSYNC-PANEL-023
|
||||
|
||||
/**
|
||||
* The document-sync dialog shell (#2391).
|
||||
@@ -49,6 +49,27 @@ vi.mock('../../../api/client', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
/** Set by the one test that needs a render slower than React's time slice. */
|
||||
let slowCard = false
|
||||
|
||||
vi.mock('./DocSyncBinding', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('./DocSyncBinding')>()
|
||||
const Card = actual.default
|
||||
return {
|
||||
...actual,
|
||||
default: function SlowableBinding(props: Parameters<typeof Card>[0]) {
|
||||
// Busy for longer than the scheduler's 5 ms slice, so the commit that
|
||||
// shows the card leaves its effects for a later task, as a loaded CI
|
||||
// runner does.
|
||||
if (slowCard) {
|
||||
const end = performance.now() + 20
|
||||
while (performance.now() < end) { /* busy */ }
|
||||
}
|
||||
return <Card {...props} />
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
import DocSyncPanel from './DocSyncPanel'
|
||||
import type { DocSyncLink, DocSyncProvider } from './useDocSync'
|
||||
|
||||
@@ -142,6 +163,7 @@ const confirmUnlink = () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
slowCard = false
|
||||
serverHas({})
|
||||
deleteLink.mockResolvedValue({})
|
||||
updateLink.mockResolvedValue({})
|
||||
@@ -301,6 +323,39 @@ describe('DocSyncPanel: the store list', () => {
|
||||
expect(second).toHaveAttribute('aria-current', 'true')
|
||||
expect(within(sidebar()).getByRole('button', { name: /Paperless-ngx/ })).not.toHaveAttribute('aria-current')
|
||||
})
|
||||
|
||||
it('FE-DOCSYNC-PANEL-023: a store picked before the list-following effect has run stays picked', async () => {
|
||||
serverHas({
|
||||
providers: [provider('paperless', 'Paperless-ngx'), provider('nextcloud', 'Nextcloud')],
|
||||
links: [link(1, 'paperless'), link(2, 'nextcloud')],
|
||||
})
|
||||
slowCard = true
|
||||
|
||||
render(<DocSyncPanel tripId={7} canManage onClose={onClose} />)
|
||||
|
||||
// Outside act, as during the wait inside findBy*: React commits the card on
|
||||
// its own scheduler, and the click lands the moment the card is in the DOM,
|
||||
// before the effect that follows the list has had its turn.
|
||||
const env = globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
const previous = env.IS_REACT_ACT_ENVIRONMENT
|
||||
env.IS_REACT_ACT_ENVIRONMENT = false
|
||||
try {
|
||||
await new Promise<void>(resolve => {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (!document.querySelector('article')) return
|
||||
observer.disconnect()
|
||||
fireEvent.click(within(sidebar()).getByRole('button', { name: /Nextcloud/ }))
|
||||
resolve()
|
||||
})
|
||||
observer.observe(document.body, { childList: true, subtree: true })
|
||||
})
|
||||
} finally {
|
||||
env.IS_REACT_ACT_ENVIRONMENT = previous
|
||||
}
|
||||
|
||||
expect(within(binding()).getByText('Folder 2')).toBeInTheDocument()
|
||||
expect(within(binding()).queryByText('Folder 1')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('DocSyncPanel: a member rather than the owner', () => {
|
||||
|
||||
@@ -54,11 +54,12 @@ export default function DocSyncPanel({
|
||||
const activeProvider = active ? sync.providers.find(p => p.id === active.providerId) : undefined
|
||||
|
||||
// Follow the list: a freshly bound store should be the one on screen, and a
|
||||
// removed one must not leave the detail column pointing at nothing.
|
||||
// removed one must not leave the detail column pointing at nothing. The pick
|
||||
// is read when the update runs, not from this render: a store clicked before
|
||||
// the effect got its turn was otherwise switched straight back.
|
||||
useEffect(() => {
|
||||
if (sync.links.length === 0) setSelected(null)
|
||||
else if (!sync.links.some(l => l.id === selected)) setSelected(sync.links[0].id)
|
||||
}, [sync.links, selected])
|
||||
setSelected(prev => (sync.links.some(l => l.id === prev) ? prev : (sync.links[0]?.id ?? null)))
|
||||
}, [sync.links])
|
||||
|
||||
const attention = ATTENTION_STATES.reduce((n, k) => n + (sync.itemCounts[k] ?? 0), 0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user