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:
2026-06-07 16:29:56 +07:00
parent c7644ade69
commit 626fbc70d8
408 changed files with 27367 additions and 154 deletions
@@ -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();
});
});