test(12.2-05): SPA unit tests for the phase and the check-phase12.2 gate
- sessionKey, dateFormat, FileuploadField (protected thumbnails and keyboard reorder backstops), RelationChildModal, RelationPivotModal, RelationManager row actions and create-screen deferral, date and time list cells; typed deferred relation-schema and file-list fixtures - scripts/check-phase12.2.sh: go, security (named tests, refuses missing or skipped), spa, openapi, dist, docs, hygiene and app stages, a detector self-test, one PASS or FAIL line per stage under --all
This commit is contained in:
119
admin/tests/relation/RelationPivotModal.test.ts
Normal file
119
admin/tests/relation/RelationPivotModal.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// RelationPivotModal (Phase 12.2, D-14; UI-SPEC section 4, pivot modal):
|
||||
// loads the link's pivot form values with the form's session key, saves
|
||||
// only the pivot form fields, toasts pivot_saved, keeps a 422 in the
|
||||
// dialog, closes and reloads on a 404, and asks before a dirty close.
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import RelationPivotModal from '../../src/components/relation/RelationPivotModal.vue'
|
||||
import { SESSION_HEADER } from '../../src/app/sessionKey'
|
||||
import { toasts } from '../../src/state/useToasts'
|
||||
import { deferredRelationSchemaFixture } from '../fixtures/typed'
|
||||
import { API, mockApi, requestsTo, resetState, type Route } from '../helpers'
|
||||
|
||||
const source = { vendor: 'acme', plugin: 'demo', controller: 'gadgets' }
|
||||
const PIVOT = `${API}/acme/demo/gadgets/5/relations/members/pivot/12`
|
||||
const FORM_KEY = 'p'.repeat(43)
|
||||
const schema = deferredRelationSchemaFixture.members.data
|
||||
|
||||
function dialog(): HTMLElement {
|
||||
return document.body.querySelector<HTMLElement>('[data-relation-pivot-modal]')!
|
||||
}
|
||||
|
||||
function setNote(value: string): void {
|
||||
const input = dialog().querySelector<HTMLInputElement>('#pivot-members-note')!
|
||||
input.value = value
|
||||
input.dispatchEvent(new Event('input'))
|
||||
}
|
||||
|
||||
function click(selector: string): void {
|
||||
;(document.body.querySelector(selector) as HTMLElement).click()
|
||||
}
|
||||
|
||||
async function open(routes: Record<string, Route>) {
|
||||
const calls = mockApi(routes)
|
||||
const wrapper = mount(RelationPivotModal, {
|
||||
props: { open: true, source, ownerId: 5, relation: 'members', schema, childId: 12, childName: 'ada@example.test', sessionKey: FORM_KEY },
|
||||
attachTo: document.body,
|
||||
})
|
||||
await flushPromises()
|
||||
return { wrapper, calls }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetState()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
describe('relation pivot modal', () => {
|
||||
it('loads the link details, saves the pivot fields only and toasts pivot_saved', async () => {
|
||||
const { wrapper, calls } = await open({
|
||||
[`GET ${PIVOT}`]: { body: { data: { id: 12, note: 'Lead' }, meta: {} } },
|
||||
[`PUT ${PIVOT}`]: { body: { data: { id: 12, note: 'Owner' }, meta: {} } },
|
||||
})
|
||||
expect(dialog().textContent).toContain('Link details')
|
||||
expect(dialog().textContent).toContain('ada@example.test')
|
||||
const [get] = requestsTo(calls, 'GET', PIVOT)
|
||||
expect(get!.headers.get(SESSION_HEADER)).toBe(FORM_KEY)
|
||||
expect(dialog().querySelector<HTMLInputElement>('#pivot-members-note')!.value).toBe('Lead')
|
||||
setNote('Owner')
|
||||
await flushPromises()
|
||||
click('[data-action="save-pivot"]')
|
||||
await flushPromises()
|
||||
const [put] = requestsTo(calls, 'PUT', PIVOT)
|
||||
expect(await put!.clone().json()).toEqual({ note: 'Owner' })
|
||||
expect(put!.headers.get(SESSION_HEADER)).toBe(FORM_KEY)
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
||||
expect(toasts.value.at(-1)).toMatchObject({ text: 'Link details saved', tone: 'success' })
|
||||
})
|
||||
|
||||
it('keeps a 422 in the dialog under its field', async () => {
|
||||
const { wrapper } = await open({
|
||||
[`GET ${PIVOT}`]: { body: { data: { id: 12, note: 'Lead' }, meta: {} } },
|
||||
[`PUT ${PIVOT}`]: { status: 422, body: { error: { code: 'validation_failed', message: 'Invalid', details: { note: ['The note field is required.'] } } } },
|
||||
})
|
||||
setNote('')
|
||||
click('[data-action="save-pivot"]')
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('update:open')).toBeUndefined()
|
||||
expect(dialog().textContent).toContain('The note field is required.')
|
||||
expect(dialog().querySelector('#pivot-members-note')!.getAttribute('aria-invalid')).toBe('true')
|
||||
})
|
||||
|
||||
it('closes, toasts child_gone and emits gone when the link is gone', async () => {
|
||||
const { wrapper } = await open({ [`GET ${PIVOT}`]: { status: 404, body: { error: { code: 'not_found', message: 'Not found', details: {} } } } })
|
||||
expect(wrapper.emitted('gone')).toHaveLength(1)
|
||||
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
||||
expect(toasts.value.at(-1)).toMatchObject({ text: 'backend::lang.relation.child_gone', tone: 'danger' })
|
||||
})
|
||||
|
||||
it('toasts the generic error and keeps the input when the save fails otherwise', async () => {
|
||||
const { wrapper } = await open({
|
||||
[`GET ${PIVOT}`]: { body: { data: { id: 12, note: 'Lead' }, meta: {} } },
|
||||
[`PUT ${PIVOT}`]: { status: 500, body: { error: { code: 'error', message: 'Boom', details: {} } } },
|
||||
})
|
||||
setNote('Changed')
|
||||
click('[data-action="save-pivot"]')
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('update:open')).toBeUndefined()
|
||||
expect(dialog().querySelector<HTMLInputElement>('#pivot-members-note')!.value).toBe('Changed')
|
||||
expect(toasts.value.at(-1)?.tone).toBe('danger')
|
||||
})
|
||||
|
||||
it('asks before closing with unsaved changes', async () => {
|
||||
const { wrapper } = await open({ [`GET ${PIVOT}`]: { body: { data: { id: 12, note: 'Lead' }, meta: {} } } })
|
||||
setNote('Changed')
|
||||
await flushPromises()
|
||||
click('[data-relation-pivot-modal] [data-action="cancel"]')
|
||||
await flushPromises()
|
||||
expect(document.body.querySelector('[data-confirm-dialog]')).not.toBeNull()
|
||||
click('[data-confirm-dialog] [data-action="confirm"]')
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user