Files
summercms/admin/tests/browser/record-actions-wrap.test.ts
Jakub Zych 4c61a180d0 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
2026-10-22 12:18:34 +02:00

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)
}
}
})
})