test(19.6.1-03): SPA trigger, record lock and footer wrap tests
- trigger.test.ts: triggerState and applyTriggers - FormView: disable, absent-controller and hide-tab backstops, the live announcement - PermissionEditorField and formState: record-locked rows, notes and the payload omission, record lock over request lock, read-only screens - PreviewView: hide removes, disable shows no note - browser record-actions-wrap at 1024px; vitest.browser.config.ts gains the Tailwind plugin so the footer is laid out with the admin's CSS
This commit is contained in:
98
admin/tests/browser/record-actions-wrap.test.ts
Normal file
98
admin/tests/browser/record-actions-wrap.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { page } from '@vitest/browser/context'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { Pencil } from '@lucide/vue'
|
||||
import type { RecordAction } from '../../src/api/types'
|
||||
import RecordActions from '../../src/components/form/RecordActions.vue'
|
||||
import Button from '../../src/components/ui/Button.vue'
|
||||
import '../../src/styles/main.css'
|
||||
|
||||
// UI-SPEC "UI Considerations", long text: contributed record action labels
|
||||
// (D-14) of up to 40 characters next to the owner's actions in the preview
|
||||
// footer at 1024px. The footer must wrap, and no button may overlap the
|
||||
// primary Edit button. Needs real layout, so it runs in Vitest browser mode
|
||||
// (Playwright Chromium) with the admin's own Tailwind build. Fixtures are
|
||||
// neutral acme.* data.
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
const actions: RecordAction[] = [
|
||||
{ name: 'activate', label: 'Activate' },
|
||||
{ name: 'deactivate', label: 'Deactivate' },
|
||||
{ name: 'restore', label: 'Restore' },
|
||||
{ name: 'acme_reset_two_factor', label: 'Reset the two-factor authentication keys' },
|
||||
{ name: 'acme_link_frontend', label: 'Link this account to the frontend member' },
|
||||
]
|
||||
|
||||
/** The preview footer of PreviewView.vue, with the same classes. */
|
||||
const Footer = defineComponent({
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
'footer',
|
||||
{
|
||||
'data-footer': '',
|
||||
class:
|
||||
'fixed right-0 bottom-0 left-[var(--shell-offset,0px)] z-30 flex flex-wrap items-center gap-2.5 border-t border-border bg-surface px-8 py-3.5',
|
||||
},
|
||||
[
|
||||
h('div', { class: 'ml-auto flex flex-wrap items-center gap-2.5' }, [
|
||||
h(RecordActions, { source: { vendor: 'acme', plugin: 'demo', controller: 'people' }, recordId: 1, actions }),
|
||||
h(Button, { variant: 'primary', 'data-action': 'edit', icon: Pencil }, () => 'Edit person'),
|
||||
]),
|
||||
],
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
function overlaps(a: DOMRect, b: DOMRect): boolean {
|
||||
return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await page.viewport(1024, 768)
|
||||
})
|
||||
|
||||
describe('preview footer with contributed record actions at 1024px', () => {
|
||||
it('backstop: wraps onto more than one row and no button overlaps Edit', async () => {
|
||||
expect(actions.slice(3).map((action) => action.label.length)).toEqual([40, 40])
|
||||
const wrapper = mount(Footer, { attachTo: document.body })
|
||||
await flushPromises()
|
||||
expect(window.innerWidth).toBe(1024)
|
||||
|
||||
const footer = wrapper.get('[data-footer]').element as HTMLElement
|
||||
expect(getComputedStyle(footer).flexWrap).toBe('wrap')
|
||||
const buttons = Array.from(footer.querySelectorAll<HTMLButtonElement>('button'))
|
||||
expect(buttons.map((button) => button.dataset.recordAction ?? button.dataset.action)).toEqual([
|
||||
'activate',
|
||||
'deactivate',
|
||||
'restore',
|
||||
'acme_reset_two_factor',
|
||||
'acme_link_frontend',
|
||||
'edit',
|
||||
])
|
||||
const rects = buttons.map((button) => button.getBoundingClientRect())
|
||||
// Every button is laid out, inside the viewport, and none is squeezed:
|
||||
// the long labels stay on one line in full.
|
||||
for (const [index, rect] of rects.entries()) {
|
||||
expect(rect.width, buttons[index]!.textContent ?? '').toBeGreaterThan(0)
|
||||
expect(rect.left).toBeGreaterThanOrEqual(0)
|
||||
expect(rect.right).toBeLessThanOrEqual(1024)
|
||||
expect(buttons[index]!.scrollWidth).toBeLessThanOrEqual(Math.ceil(rect.width))
|
||||
}
|
||||
const rows = new Set(rects.map((rect) => Math.round(rect.top)))
|
||||
expect(rows.size).toBeGreaterThan(1)
|
||||
|
||||
const edit = rects.at(-1)!
|
||||
for (const [index, rect] of rects.slice(0, -1).entries()) {
|
||||
expect(overlaps(rect, edit), `${buttons[index]!.textContent} overlaps Edit`).toBe(false)
|
||||
}
|
||||
// No two buttons overlap each other either.
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
for (let j = i + 1; j < rects.length; j++) {
|
||||
expect(overlaps(rects[i]!, rects[j]!), `${i} and ${j}`).toBe(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user