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