test(admin): list state round trip and storage coverage

- Form back, cancel, save-and-close and delete land on the list with the stored query; every list request after the return carries it
- listState helpers: key prefix, malformed stored values removed, empty query stores nothing, throwing storage
- ListView entry: URL wins, cleared controls forget the query, same-list navigation, 422 forgets, page clamp
- Logout removes every list key and keeps the sidebar preference
This commit is contained in:
2026-10-09 22:42:36 +02:00
parent 3b8527b397
commit a8bce9c01f
4 changed files with 404 additions and 0 deletions

View File

@@ -515,3 +515,126 @@ describe('list state in the URL and browser storage (D-01)', () => {
expect(readListState('acme.demo.widgets')).toEqual({ search: 'blue' })
})
})
describe('list state entry logic (D-01)', () => {
const ID = 'acme.demo.widgets'
const KEY = `summer-admin.list:${ID}`
function listQueries(calls: Request[]): URLSearchParams[] {
return requestsTo(calls, 'GET', BASE).map(queryOf)
}
it('makes no list request without the stored params when the URL has none', async () => {
writeListState(ID, { search: 'blue', sort: 'code', dir: 'desc', per_page: 50, filter: { active: 'true' } })
const { calls, router } = await mountApp('/acme/demo/widgets', routes())
await flushPromises()
const sent = listQueries(calls)
expect(sent.length).toBeGreaterThanOrEqual(1)
for (const query of sent) {
expect(query.get('search')).toBe('blue')
expect(query.get('sort')).toBe('code')
expect(query.get('dir')).toBe('desc')
expect(query.get('per_page')).toBe('50')
expect(query.get('filter[active]')).toBe('true')
}
expect(router.currentRoute.value.query).toEqual({
search: 'blue',
sort: 'code',
dir: 'desc',
per_page: '50',
'filter[active]': 'true',
})
})
it('lets every list param of the URL win and overwrites the whole stored query', async () => {
writeListState(ID, { search: 'old', sort: 'code', dir: 'desc', filter: { active: 'true' } })
const { calls } = await mountApp('/acme/demo/widgets?sort=name&dir=asc&filter[active]=false', routes())
await flushPromises()
const sent = listQueries(calls)
expect(sent).toHaveLength(1)
expect(sent[0]!.get('sort')).toBe('name')
expect(sent[0]!.get('search')).toBeNull()
expect(sent[0]!.get('filter[active]')).toBe('false')
expect(readListState(ID)).toEqual({ sort: 'name', dir: 'asc', filter: { active: 'false' } })
})
it('forgets the stored query when every control is cleared, so the next entry loads unfiltered', async () => {
const first = await mountApp('/acme/demo/widgets?search=blue&sort=code&dir=desc&filter[active]=true', routes())
await flushPromises()
expect(window.localStorage.getItem(KEY)).not.toBeNull()
await first.wrapper.find('[data-list-search]').setValue('')
await wait(350)
await flushPromises()
expect(first.router.currentRoute.value.query.search).toBeUndefined()
await first.wrapper.find('[data-sort="code"]').trigger('click')
await flushPromises()
expect(first.router.currentRoute.value.query.sort).toBeUndefined()
expect(window.localStorage.getItem(KEY)).not.toBeNull()
await first.wrapper.find('select[data-filter="active"]').setValue('')
await flushPromises()
expect(first.router.currentRoute.value.query).toEqual({})
expect(window.localStorage.getItem(KEY)).toBeNull()
first.wrapper.unmount()
const second = await mountApp('/acme/demo/widgets', routes())
await flushPromises()
const sent = listQueries(second.calls)
expect(sent).toHaveLength(1)
expect([...sent[0]!.keys()]).toEqual([])
expect(second.router.currentRoute.value.fullPath).toBe('/acme/demo/widgets')
})
it('keeps the filters when the same list is opened again from the navigation', async () => {
const { calls, router } = await mountApp('/acme/demo/widgets?sort=code&dir=desc&filter[active]=true', routes())
await flushPromises()
const before = requestsTo(calls, 'GET', BASE).length
await router.push('/acme/demo/widgets')
await flushPromises()
expect(router.currentRoute.value.query).toEqual({ sort: 'code', dir: 'desc', 'filter[active]': 'true' })
for (const query of listQueries(calls).slice(before)) {
expect(query.get('sort')).toBe('code')
expect(query.get('filter[active]')).toBe('true')
}
expect(listQueries(calls).every((query) => query.get('filter[active]') === 'true')).toBe(true)
expect(readListState(ID)).toEqual({ sort: 'code', dir: 'desc', filter: { active: 'true' } })
})
it('drops the stored query when the list answers 422', async () => {
const invalid = {
status: 422,
body: { error: { code: 'validation_failed', message: 'Invalid', details: { sort: ['unknown column'] } } },
}
writeListState(ID, { sort: 'gone', dir: 'asc' })
const { calls } = await mountApp('/acme/demo/widgets', routes({ [`GET ${BASE}`]: invalid }))
await flushPromises()
expect(queryOf(requestsTo(calls, 'GET', BASE)[0]!).get('sort')).toBe('gone')
expect(window.localStorage.getItem(KEY)).toBeNull()
expect(readListState(ID)).toEqual({})
})
it.each([
['from the URL', '/acme/demo/widgets?page=5', false],
['restored from storage', '/acme/demo/widgets', true],
])('moves a page beyond last_page %s to last_page and requests that page', async (_label, url, stored) => {
if (stored) {
writeListState(ID, { page: 5 })
}
const pageOf = (request: Request) => Number(queryOf(request).get('page') ?? '1')
const { calls, router } = await mountApp(
url,
routes({
[`GET ${BASE}`]: (request: Request): Reply => {
const page = pageOf(request)
return { body: { data: page <= 2 ? listFixture.data : [], meta: { page, per_page: 20, total: 25, last_page: 2 } } }
},
}),
)
await flushPromises()
expect(requestsTo(calls, 'GET', BASE).map(pageOf)).toEqual([5, 2])
expect(router.currentRoute.value.query).toEqual({ page: '2' })
expect(readListState(ID)).toEqual({ page: 2 })
})
})