fix(primitives): eslint/tsconfig migration, asChild refactor, type fixes
- Migrate to eslint flat config + composite tsconfig. - Complete the asChild→as="template" refactor (remove asChild prop + :as-child bindings across components, matching Primitive's slot model). - Fix test type errors and source type-safety (useGraceArea hull/point math, FocusScope/util ref typing). Note: ~53 vue-tsc errors remain (HTML attr/event passthrough typing on transparent wrapper components + a couple of duplicate-export naming collisions) — not gated by CI (build/lint/test green); pending a component-attribute-typing design decision.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// Regression tests for confirmed bugs from Phase 1 audit:
|
||||
// 1. ToastRoot tabindex must be -1 (programmatic only), not 0 (conflicts with roving focus).
|
||||
// 2. ToastViewport hotkey must validate against empty array (otherwise vacuous-truth
|
||||
// focuses the viewport on every keystroke).
|
||||
// 3. ToastRoot must restart the auto-dismiss timer when `duration` changes reactively.
|
||||
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { defineComponent, h, nextTick, ref } from 'vue';
|
||||
import { ToastProvider, ToastRoot, ToastViewport } from '../index';
|
||||
|
||||
function createHarness(props: {
|
||||
duration?: number;
|
||||
hotkey?: string[];
|
||||
} = {}) {
|
||||
return defineComponent({
|
||||
components: { ToastProvider, ToastViewport, ToastRoot },
|
||||
setup() {
|
||||
const duration = ref(props.duration);
|
||||
return { duration, hotkey: props.hotkey };
|
||||
},
|
||||
render() {
|
||||
return h(ToastProvider, {}, {
|
||||
default: () => [
|
||||
h(ToastViewport, { hotkey: this.hotkey ?? ['F8'] }),
|
||||
h(ToastRoot, { duration: this.duration }, { default: () => 'Toast body' }),
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('toast — bug regression', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('ToastRoot has tabindex="-1" (programmatic focus only)', () => {
|
||||
const wrapper = mount(createHarness({ duration: Infinity }), { attachTo: document.body });
|
||||
const toast = wrapper.find('[role="status"]');
|
||||
expect(toast.exists()).toBe(true);
|
||||
expect(toast.attributes('tabindex')).toBe('-1');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('ToastViewport ignores empty hotkey array (does not focus on every keypress)', async () => {
|
||||
const wrapper = mount(createHarness({ duration: Infinity, hotkey: [] }), { attachTo: document.body });
|
||||
const viewport = wrapper.find('[role="region"]').element as HTMLElement;
|
||||
const focusSpy = vi.spyOn(viewport, 'focus');
|
||||
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F8' }));
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||
|
||||
expect(focusSpy).not.toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('ToastViewport responds to F8 when hotkey=["F8"]', () => {
|
||||
const wrapper = mount(createHarness({ duration: Infinity }), { attachTo: document.body });
|
||||
const viewport = wrapper.find('[role="region"]').element as HTMLElement;
|
||||
const focusSpy = vi.spyOn(viewport, 'focus');
|
||||
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F8' }));
|
||||
|
||||
expect(focusSpy).toHaveBeenCalledOnce();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('ToastRoot restarts auto-dismiss timer when duration prop changes reactively', async () => {
|
||||
const Harness = createHarness({ duration: 1000 });
|
||||
const wrapper = mount(Harness, { attachTo: document.body });
|
||||
|
||||
// Bump duration to 5000ms BEFORE original 1000ms elapses.
|
||||
vi.advanceTimersByTime(500);
|
||||
wrapper.vm.duration = 5000;
|
||||
await nextTick();
|
||||
|
||||
// Original 1000ms total would have fired by 1100ms — but the watcher restarted the
|
||||
// timer with the new duration. Toast must still be open.
|
||||
vi.advanceTimersByTime(600); // 1100ms elapsed total
|
||||
await nextTick();
|
||||
expect(wrapper.find('[role="status"]').exists()).toBe(true);
|
||||
|
||||
// Now advance the full new duration (5000ms) — toast should close.
|
||||
vi.advanceTimersByTime(5000);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(wrapper.find('[role="status"]').exists()).toBe(false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('ToastRoot timer does not fire when duration=Infinity', () => {
|
||||
const wrapper = mount(createHarness({ duration: Infinity }), { attachTo: document.body });
|
||||
vi.advanceTimersByTime(60_000);
|
||||
expect(wrapper.find('[role="status"]').exists()).toBe(true);
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user