Files
tools/vue/primitives/src/overlays/drawer/__test__/Drawer.test.ts
T
robonen eefd7abf83 feat(primitives): media-editor components, category reorg, perf + type cleanup
Reorganize components into category folders (forms/canvas/overlays/etc.); add the
media-editor headless family (timeline, curve-editor, waveform, crop, color
picker, etc.); apply perf fixes (O(1) collection lookups, plain-object drag
state, gesture-leak teardown, shallowRef color state, rect caching) and replace
source `any` with proper types.
2026-06-15 16:54:29 +07:00

222 lines
6.4 KiB
TypeScript

import type { VueWrapper } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { defineComponent, h, nextTick, ref } from 'vue';
import {
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHandle,
DrawerOverlay,
DrawerPortal,
DrawerRoot,
DrawerTitle,
DrawerTrigger,
} from '../index';
const wrappers: Array<VueWrapper<any>> = [];
afterEach(() => {
while (wrappers.length) wrappers.pop()!.unmount();
document.body.innerHTML = '';
document.body.removeAttribute('style');
document.getElementById('robonen-drawer')?.remove();
});
function track<T extends VueWrapper<any>>(w: T): T {
wrappers.push(w);
return w;
}
/** Drains Vue's scheduler (including `flush: 'post'` watchers). */
async function flush(): Promise<void> {
await nextTick();
await nextTick();
await nextTick();
}
function $<T extends Element = HTMLElement>(selector: string): T | null {
return document.querySelector<T>(selector);
}
function $content(): HTMLElement | null {
return $('[data-drawer]');
}
function $trigger(): HTMLElement {
return $<HTMLElement>('[aria-haspopup="dialog"]')!;
}
function $close(): HTMLButtonElement | undefined {
return [...document.querySelectorAll('button')].find(b => b.textContent === 'Close');
}
interface MountOptions {
open?: boolean;
defaultOpen?: boolean;
modal?: boolean;
dismissible?: boolean;
direction?: 'top' | 'bottom' | 'left' | 'right';
withHandle?: boolean;
onUpdateOpen?: (v: boolean) => void;
onClose?: () => void;
}
function mountDrawer(options: MountOptions = {}) {
const { withHandle = true } = options;
const Wrapper = defineComponent({
setup() {
return () => h(
DrawerRoot,
{
open: options.open,
defaultOpen: options.defaultOpen,
modal: options.modal ?? true,
dismissible: options.dismissible ?? true,
direction: options.direction ?? 'bottom',
'onUpdate:open': options.onUpdateOpen,
onClose: options.onClose,
},
{
default: () => [
h(DrawerTrigger, null, { default: () => 'Open' }),
h(DrawerPortal, null, {
default: () => [
h(DrawerOverlay, { 'data-testid': 'overlay' }),
h(DrawerContent, null, {
default: () => [
withHandle ? h(DrawerHandle) : null,
h(DrawerTitle, null, { default: () => 'Title' }),
h(DrawerDescription, null, { default: () => 'Desc' }),
h(DrawerClose, null, { default: () => 'Close' }),
],
}),
],
}),
],
},
);
},
});
return track(mount(Wrapper, { attachTo: document.body }));
}
describe('Drawer / markup', () => {
it('renders closed by default', () => {
mountDrawer();
expect($trigger().getAttribute('data-state')).toBe('closed');
expect($content()).toBeNull();
});
it('injects the critical drawer stylesheet once', async () => {
mountDrawer({ defaultOpen: true });
await flush();
const tags = document.querySelectorAll('#robonen-drawer');
expect(tags.length).toBe(1);
expect(tags[0]!.textContent).toContain('@keyframes slideFromBottom');
});
it('exposes drawer data attributes on the content when open', async () => {
mountDrawer({ defaultOpen: true, direction: 'right' });
await flush();
const content = $content()!;
expect(content.getAttribute('data-state')).toBe('open');
expect(content.getAttribute('data-drawer-direction')).toBe('right');
expect(content.hasAttribute('data-drawer')).toBe(true);
});
it('renders the handle without throwing (handleRef wiring)', async () => {
mountDrawer({ defaultOpen: true, withHandle: true });
await flush();
expect($('[data-drawer-handle]')).toBeTruthy();
expect($('[data-drawer-handle-hitarea]')).toBeTruthy();
});
});
describe('Drawer / open state', () => {
it('opens when the trigger is clicked (uncontrolled)', async () => {
mountDrawer();
$trigger().click();
await flush();
expect($content()).toBeTruthy();
});
it('closes when DrawerClose is clicked', async () => {
mountDrawer({ defaultOpen: true });
await flush();
$close()!.click();
await flush();
// Presence keeps the node for the exit animation; data-state flips to closed.
expect($content()?.getAttribute('data-state') ?? 'closed').toBe('closed');
});
it('emits update:open when the trigger is clicked (controlled)', async () => {
const onUpdateOpen = vi.fn();
mountDrawer({ open: false, onUpdateOpen });
$trigger().click();
await flush();
expect(onUpdateOpen).toHaveBeenCalledWith(true);
});
it('emits close exactly once when dismissed via DrawerClose', async () => {
const onClose = vi.fn();
mountDrawer({ defaultOpen: true, onClose });
await flush();
$close()!.click();
await flush();
expect(onClose).toHaveBeenCalledTimes(1);
});
it('emits close when a controlled drawer is closed by flipping v-model:open', async () => {
// Regression: closing purely by setting the bound `open` prop to false (not
// via a dialog dismissal) must still run the close side effects.
const onClose = vi.fn();
const state = ref(true);
const Wrapper = defineComponent({
setup() {
return () => h(
DrawerRoot,
{
open: state.value,
'onUpdate:open': (v: boolean) => { state.value = v; },
onClose,
},
{
default: () => h(DrawerPortal, null, {
default: () => h(DrawerContent, null, {
default: () => h(DrawerTitle, null, { default: () => 'Title' }),
}),
}),
},
);
},
});
track(mount(Wrapper, { attachTo: document.body }));
await flush();
expect($content()).toBeTruthy();
state.value = false;
await flush();
expect(onClose).toHaveBeenCalledTimes(1);
});
});
describe('Drawer / overlay', () => {
it('renders an overlay for modal drawers', async () => {
mountDrawer({ defaultOpen: true, modal: true });
await flush();
expect($('[data-drawer-overlay]')).toBeTruthy();
});
it('omits the overlay for non-modal drawers', async () => {
mountDrawer({ defaultOpen: true, modal: false });
await flush();
expect($('[data-drawer-overlay]')).toBeNull();
});
});