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).
|
* 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 DocSyncPanel from './DocSyncPanel'
|
||||||
import type { DocSyncLink, DocSyncProvider } from './useDocSync'
|
import type { DocSyncLink, DocSyncProvider } from './useDocSync'
|
||||||
|
|
||||||
@@ -142,6 +163,7 @@ const confirmUnlink = () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
slowCard = false
|
||||||
serverHas({})
|
serverHas({})
|
||||||
deleteLink.mockResolvedValue({})
|
deleteLink.mockResolvedValue({})
|
||||||
updateLink.mockResolvedValue({})
|
updateLink.mockResolvedValue({})
|
||||||
@@ -301,6 +323,39 @@ describe('DocSyncPanel: the store list', () => {
|
|||||||
expect(second).toHaveAttribute('aria-current', 'true')
|
expect(second).toHaveAttribute('aria-current', 'true')
|
||||||
expect(within(sidebar()).getByRole('button', { name: /Paperless-ngx/ })).not.toHaveAttribute('aria-current')
|
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', () => {
|
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
|
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
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (sync.links.length === 0) setSelected(null)
|
setSelected(prev => (sync.links.some(l => l.id === prev) ? prev : (sync.links[0]?.id ?? null)))
|
||||||
else if (!sync.links.some(l => l.id === selected)) setSelected(sync.links[0].id)
|
}, [sync.links])
|
||||||
}, [sync.links, selected])
|
|
||||||
|
|
||||||
const attention = ATTENTION_STATES.reduce((n, k) => n + (sync.itemCounts[k] ?? 0), 0)
|
const attention = ATTENTION_STATES.reduce((n, k) => n + (sync.itemCounts[k] ?? 0), 0)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user