- 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
99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
})
|
|
})
|