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

@@ -5,11 +5,14 @@ import FormView from '../../src/views/FormView.vue'
import { recordCrumb } from '../../src/state/useBreadcrumbs'
import { toasts } from '../../src/state/useToasts'
import { WIDGET_EVENT } from '../../src/components/form/formContext'
import { readListState, writeListState } from '../../src/state/listState'
import {
clone,
extensionFormSchemaFixture,
formSchemaFixture,
langFixture,
listFixture,
listSchemaFixture,
recordFixture,
rosterFormSchemaFixture,
rosterRecordFixture,
@@ -662,3 +665,83 @@ describe('password and preset on the form (UI-SPEC S7, D-19, D-27 G7)', () => {
expect(body.name).toEqual({ en: 'Ada\nLovelace', pl: 'Ada po polsku' })
})
})
describe('returning to the list keeps its query (D-01)', () => {
const LIST_ID = 'acme.demo.widgets'
const STORED = { sort: 'code', dir: 'desc' as const, filter: { active: 'true' } }
/** The form routes plus the list schema and rows, so the form can return to the list. */
function roundTrip(overrides: Record<string, Route> = {}): Record<string, Route> {
return routes({
[`GET ${BASE}/schema/list`]: { body: listSchemaFixture },
[`GET ${BASE}`]: { body: listFixture },
[`PUT ${RECORD}`]: { body: recordFixture },
[`DELETE ${RECORD}`]: { body: { data: { deleted: true }, meta: {} } },
...overrides,
})
}
/** The list lands on its path with the stored query, and every list request carries it. */
function expectRestored(router: { currentRoute: { value: { path: string; query: Record<string, unknown> } } }, calls: Request[]) {
expect(router.currentRoute.value.path).toBe('/acme/demo/widgets')
expect(router.currentRoute.value.query).toEqual({ sort: 'code', dir: 'desc', 'filter[active]': 'true' })
const lists = requestsTo(calls, 'GET', BASE)
expect(lists.length).toBeGreaterThanOrEqual(1)
for (const request of lists) {
const query = queryOf(request)
expect(query.get('sort')).toBe('code')
expect(query.get('dir')).toBe('desc')
expect(query.get('filter[active]')).toBe('true')
}
expect(readListState(LIST_ID)).toEqual(STORED)
}
beforeEach(() => {
writeListState(LIST_ID, STORED)
})
it('tracer: the back action lands on the list with the stored query before the first list request', async () => {
const { wrapper, calls, router } = await mountApp('/acme/demo/widgets/1', roundTrip())
expect(requestsTo(calls, 'GET', BASE)).toHaveLength(0)
await wrapper.find('[data-action="back"]').trigger('click')
await flushPromises()
expectRestored(router, calls)
})
it('cancel lands on the list with the stored query', async () => {
const { wrapper, calls, router } = await mountApp('/acme/demo/widgets/1', roundTrip())
await wrapper.find('[data-action="cancel"]').trigger('click')
await flushPromises()
expectRestored(router, calls)
})
it('save and close follows a redirectClose to the list and lands with the stored query', async () => {
expect(formSchemaFixture.data.redirects.update.redirectClose).toBe('acme/demo/widgets')
const { wrapper, calls, router } = await mountApp('/acme/demo/widgets/1', roundTrip())
await wrapper.find('[data-action="save-close"]').trigger('click')
await flushPromises()
expect(requestsTo(calls, 'PUT', RECORD)).toHaveLength(1)
expectRestored(router, calls)
})
it('delete, once confirmed, lands on the list with the stored query', async () => {
const { wrapper, calls, router } = await mountApp('/acme/demo/widgets/1', roundTrip(), { attach: true })
await wrapper.find('[data-action="delete"]').trigger('click')
await flushPromises()
;(document.body.querySelector('[data-confirm-dialog] [data-action="confirm"]') as HTMLElement).click()
await flushPromises()
expect(requestsTo(calls, 'DELETE', RECORD)).toHaveLength(1)
expectRestored(router, calls)
})
it('lands on the unfiltered list when nothing is stored', async () => {
writeListState(LIST_ID, {})
const { wrapper, calls, router } = await mountApp('/acme/demo/widgets/1', roundTrip())
await wrapper.find('[data-action="back"]').trigger('click')
await flushPromises()
expect(router.currentRoute.value.fullPath).toBe('/acme/demo/widgets')
const lists = requestsTo(calls, 'GET', BASE)
expect(lists).toHaveLength(1)
expect([...queryOf(lists[0]!).keys()]).toEqual([])
})
})