parent
1ba41e3254
commit
a8be47319d
@ -0,0 +1,263 @@
|
||||
import { beforeAll, expect, it, vi } from 'vitest';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import { renderColorPicker } from './test/color-picker-audit-view';
|
||||
beforeAll(async () => {
|
||||
await page.viewport(1100, 1000);
|
||||
await userEvent.keyboard('{Shift}');
|
||||
}, 60_000);
|
||||
it('edits both area axes with native keys, preserving hue and alpha', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const thumb = v.part('area-thumb');
|
||||
thumb.focus();
|
||||
await userEvent.keyboard('{ArrowRight}{Shift>}{ArrowUp}{/Shift}');
|
||||
expect(v.value()?.hsv).toEqual({ h: 210, s: 51, v: 70, a: 0.8 });
|
||||
expect(v.onValueChangeEnd).toHaveBeenCalledTimes(2);
|
||||
await userEvent.keyboard('{Home}{End}{PageDown}{PageUp}');
|
||||
expect(v.value()?.hsv.s).toBe(100);
|
||||
expect(v.value()?.hsv.v).toBe(100);
|
||||
expect(thumb.getAttribute('aria-valuetext')).toBe('saturation: 100, brightness: 100');
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('mirrors the area and horizontal channel ramp reactively in RTL', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
await userEvent.click(v.button('Toggle direction'));
|
||||
expect(v.root.dir).toBe('rtl');
|
||||
const thumb = v.part('area-thumb');
|
||||
thumb.focus();
|
||||
await userEvent.keyboard('{ArrowRight}');
|
||||
expect(v.value()?.hsv.s).toBe(49);
|
||||
expect(parseFloat(thumb.style.left)).toBe(51);
|
||||
expect(getComputedStyle(v.part('area-background'), '::before').backgroundImage).toContain(
|
||||
'to left'
|
||||
);
|
||||
expect(
|
||||
v.part('channel-slider').style.getPropertyValue('--_color-picker-channel-gradient')
|
||||
).toContain('to left');
|
||||
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1')
|
||||
await page.screenshot({
|
||||
path: 'tmp/color-picker-rtl.png',
|
||||
element: page.elementLocator(v.target)
|
||||
});
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('captures a native drag and commits once after the last continuous value', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const area = v.part('area');
|
||||
let trusted = false;
|
||||
let captured = false;
|
||||
area.addEventListener('pointermove', (e) => {
|
||||
trusted ||= e.isTrusted;
|
||||
captured ||= area.hasPointerCapture(e.pointerId);
|
||||
});
|
||||
await userEvent.dragAndDrop(
|
||||
v.part('area-thumb'),
|
||||
v.target.querySelector('[data-destination]')!
|
||||
);
|
||||
expect(trusted).toBe(true);
|
||||
expect(captured).toBe(true);
|
||||
expect(v.value()?.hsv.s).toBe(0);
|
||||
expect(v.value()?.hsv.v).toBe(0);
|
||||
expect(v.onValueChange).toHaveBeenCalled();
|
||||
expect(v.onValueChangeEnd).toHaveBeenCalledTimes(1);
|
||||
expect(v.onValueChangeEnd).toHaveBeenLastCalledWith(v.value());
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('changes area channels to RGB without a lossy HSV round trip', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const old = v.value()!.rgb;
|
||||
await userEvent.click(v.button('RGB area'));
|
||||
expect(v.part('area').dataset.xChannel).toBe('red');
|
||||
const thumb = v.part('area-thumb');
|
||||
thumb.focus();
|
||||
await userEvent.keyboard('{ArrowRight}{ArrowUp}');
|
||||
expect(v.value()?.rgb).toEqual({ r: old.r + 1, g: old.g + 1, b: old.b, a: old.a });
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('composes native hue, alpha and reactive vertical RGB sliders', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const sliders = () =>
|
||||
Array.from(document.querySelectorAll<HTMLElement>('[data-color-picker-channel-slider]'));
|
||||
let thumb = sliders()[0].querySelector<HTMLElement>('[data-slider-thumb]')!;
|
||||
thumb.focus();
|
||||
await userEvent.keyboard('{ArrowRight}');
|
||||
expect(v.value()?.hsv.h).toBe(211);
|
||||
expect(thumb.getAttribute('aria-valuemin')).toBe('0');
|
||||
expect(thumb.getAttribute('aria-valuemax')).toBe('360');
|
||||
sliders()[1].querySelector<HTMLElement>('[data-slider-thumb]')!.focus();
|
||||
await userEvent.keyboard('{Home}');
|
||||
expect(v.value()?.rgb.a).toBe(0);
|
||||
await userEvent.click(v.button('Red vertical'));
|
||||
thumb = sliders()[0].querySelector<HTMLElement>('[data-slider-thumb]')!;
|
||||
expect(thumb.getAttribute('aria-orientation')).toBe('vertical');
|
||||
expect(sliders()[0].style.getPropertyValue('--_color-picker-channel-gradient')).toContain(
|
||||
'to top'
|
||||
);
|
||||
thumb.focus();
|
||||
await userEvent.keyboard('{End}');
|
||||
expect(v.value()?.rgb.r).toBe(255);
|
||||
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1')
|
||||
await page.screenshot({
|
||||
path: 'tmp/color-picker-rgb-vertical.png',
|
||||
element: page.elementLocator(v.target)
|
||||
});
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('keeps the edit format separate and propagates it only with autoFormat', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const select = v.part('format-select');
|
||||
await userEvent.selectOptions(select, 'rgb');
|
||||
expect(v.format()).toBe('hex');
|
||||
expect(v.onFormatChange).not.toHaveBeenCalled();
|
||||
expect(
|
||||
v.part('channel-input').querySelectorAll('[data-color-picker-channel-segment]').length
|
||||
).toBeGreaterThan(1);
|
||||
await userEvent.click(v.button('Toggle auto format'));
|
||||
await userEvent.selectOptions(select, 'hsl');
|
||||
expect(v.format()).toBe('hsl');
|
||||
expect(v.onFormatChange).toHaveBeenCalledTimes(1);
|
||||
await userEvent.selectOptions(select, 'rgb');
|
||||
await userEvent.click(v.button('External HSL'));
|
||||
expect(v.format()).toBe('hsl');
|
||||
expect(v.part('channel-input').dataset.format).toBe('hsl');
|
||||
await userEvent.click(v.button('Lock format'));
|
||||
expect(v.part('format-select').tagName).toBe('SPAN');
|
||||
expect(v.part('format-select').hasAttribute('data-locked')).toBe(true);
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('commits preset radios and updates checked state and composed swatch paint', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const radios = v.radios('Presets');
|
||||
await userEvent.click(radios[1]);
|
||||
expect(v.value()?.hex).toBe('#00ff00');
|
||||
expect(radios[1].getAttribute('aria-checked')).toBe('true');
|
||||
expect(radios[0].getAttribute('aria-checked')).toBe('false');
|
||||
expect(v.onValueChangeEnd).toHaveBeenCalledTimes(1);
|
||||
expect(radios[1].querySelector('[data-color-swatch]')).not.toBeNull();
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('saves and deletes bound swatches, enforces the cap and avoids duplicates', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const initial = v.value()!.hex;
|
||||
await userEvent.click(v.button('Save color'));
|
||||
expect(v.saved()).toEqual(['#ff0000', initial]);
|
||||
expect(v.onSavedColorsChange).toHaveBeenCalledTimes(1);
|
||||
await userEvent.click(v.radios('Saved')[1]);
|
||||
await userEvent.click(v.button('Delete color'));
|
||||
expect(v.saved()).toEqual(['#ff0000']);
|
||||
expect(v.onSavedColorsChange).toHaveBeenCalledTimes(2);
|
||||
await userEvent.click(v.radios('Presets')[1]);
|
||||
await userEvent.click(v.button('Save color'));
|
||||
expect(v.saved()).toEqual(['#ff0000', '#00ff00']);
|
||||
await userEvent.click(v.radios('Presets')[2]);
|
||||
expect((v.button('Save color') as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(v.saved()).toHaveLength(2);
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('locks native edits under readonly and disabled while retaining readouts', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const original = v.value();
|
||||
await userEvent.click(v.button('Toggle readonly'));
|
||||
v.part('area-thumb').focus();
|
||||
await userEvent.keyboard('{ArrowRight}');
|
||||
await userEvent.click(v.radios('Presets')[1]);
|
||||
expect(v.value()).toEqual(original);
|
||||
expect(v.onValueChangeEnd).not.toHaveBeenCalled();
|
||||
await userEvent.click(v.button('Toggle disabled'));
|
||||
expect(v.part('area-thumb').tabIndex).toBe(-1);
|
||||
expect(v.radios('Presets')[0].disabled).toBe(true);
|
||||
await expect.element(v.part('value-text')).toHaveTextContent(original!.hex);
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('opens a native popover and restores focus and modal snapshot on Escape', async () => {
|
||||
const v = renderColorPicker({ inline: false, mode: 'modal' });
|
||||
try {
|
||||
await v.ready();
|
||||
const original = v.value();
|
||||
const trigger = v.part('trigger');
|
||||
trigger.focus();
|
||||
await userEvent.keyboard('{Enter}');
|
||||
await vi.waitFor(() => expect(v.open()).toBe(true));
|
||||
await vi.waitFor(() => expect(v.part('area-thumb')).not.toBeNull());
|
||||
await v.ready();
|
||||
v.part('area-thumb').focus();
|
||||
await userEvent.keyboard('{ArrowRight}');
|
||||
expect(v.value()).not.toEqual(original);
|
||||
expect(v.onValueChangeEnd).toHaveBeenCalledTimes(1);
|
||||
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1')
|
||||
await page.screenshot({
|
||||
path: 'tmp/color-picker-popup.png',
|
||||
element: page.elementLocator(document.body)
|
||||
});
|
||||
await userEvent.keyboard('{Escape}');
|
||||
await vi.waitFor(() => expect(v.open()).toBe(false));
|
||||
expect(v.value()).toEqual(original);
|
||||
await vi.waitFor(() => expect(document.activeElement).toBe(trigger));
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
it('updates sizes, variants, format override and checker cells on the same root', async () => {
|
||||
const v = renderColorPicker();
|
||||
try {
|
||||
await v.ready();
|
||||
const root = v.root;
|
||||
const trigger = v.part('trigger');
|
||||
const medium = getComputedStyle(trigger).minHeight;
|
||||
await userEvent.click(v.button('Next size'));
|
||||
const small = getComputedStyle(trigger).minHeight;
|
||||
expect(parseFloat(small)).toBeLessThan(parseFloat(medium));
|
||||
await userEvent.click(v.button('Next size'));
|
||||
expect(parseFloat(getComputedStyle(trigger).minHeight)).toBeGreaterThan(parseFloat(medium));
|
||||
await userEvent.click(v.button('Next variant'));
|
||||
expect(trigger.dataset.variant).toBe('outline');
|
||||
await userEvent.click(v.button('Next variant'));
|
||||
expect(trigger.dataset.variant).toBe('ghost');
|
||||
expect(v.root).toBe(root);
|
||||
await userEvent.click(v.button('Change displays'));
|
||||
expect(v.part('transparency-grid').style.backgroundSize).toBe('24px 24px');
|
||||
expect(v.target.querySelector('[data-override-display]')!.textContent).toContain('rgba(');
|
||||
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1')
|
||||
await page.screenshot({
|
||||
path: 'tmp/color-picker-displays.png',
|
||||
element: page.elementLocator(v.target)
|
||||
});
|
||||
} finally {
|
||||
await v.dispose();
|
||||
}
|
||||
}, 30_000);
|
||||
@ -0,0 +1,158 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { setActiveUix, type ActiveUix } from '$active-uix';
|
||||
import { Soma } from '$soma/core/soma.svelte';
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import { colorValueFromHsv, type ColorValue, type ColorFormat } from '$libs/color';
|
||||
import { ColorPicker } from '../index';
|
||||
import type { ColorChannel, ColorPickerSize, ColorPickerVariant } from '../types';
|
||||
let {
|
||||
uix,
|
||||
inline = true,
|
||||
mode = 'inline',
|
||||
onValueChange,
|
||||
onValueChangeEnd,
|
||||
onFormatChange,
|
||||
onOpenChange,
|
||||
onSavedColorsChange
|
||||
}: {
|
||||
uix: ActiveUix;
|
||||
inline?: boolean;
|
||||
mode?: 'inline' | 'modal';
|
||||
onValueChange: (v: ColorValue | undefined) => void;
|
||||
onValueChangeEnd: (v: ColorValue | undefined) => void;
|
||||
onFormatChange: (v: ColorFormat) => void;
|
||||
onOpenChange: (v: boolean) => void;
|
||||
onSavedColorsChange: (v: string[]) => void;
|
||||
} = $props();
|
||||
// svelte-ignore state_referenced_locally
|
||||
setActiveUix(uix);
|
||||
Soma.create();
|
||||
const eidos = ActiveEidos.create({ applyDom: false });
|
||||
onDestroy(() => eidos.dispose());
|
||||
let value = $state<ColorValue | undefined>(colorValueFromHsv({ h: 210, s: 50, v: 60, a: 0.8 }));
|
||||
let format = $state<ColorFormat>('hex');
|
||||
let open = $state(false);
|
||||
let savedColors = $state(['#ff0000']);
|
||||
let dir = $state<'ltr' | 'rtl'>('ltr');
|
||||
let size = $state<ColorPickerSize>('md');
|
||||
let variant = $state<ColorPickerVariant>('surface');
|
||||
let disabled = $state(false);
|
||||
let readonly = $state(false);
|
||||
let autoFormat = $state(false);
|
||||
let allowedFormats = $state<ColorFormat[]>(['hex', 'rgb', 'hsl']);
|
||||
let xChannel = $state<ColorChannel | undefined>();
|
||||
let yChannel = $state<ColorChannel | undefined>();
|
||||
let channel = $state<ColorChannel>('hue');
|
||||
let orientation = $state<'horizontal' | 'vertical'>('horizontal');
|
||||
let cellSize = $state(8);
|
||||
let displayFormat = $state<ColorFormat | undefined>();
|
||||
</script>
|
||||
|
||||
{#snippet body()}
|
||||
<div data-audit-body style="width: 320px; display: grid; gap: 12px;">
|
||||
<ColorPicker.Area {xChannel} {yChannel} />
|
||||
<ColorPicker.ChannelSlider {channel} {orientation} />
|
||||
<ColorPicker.ChannelSlider channel="alpha" />
|
||||
<ColorPicker.ChannelInput aria-label="Color editor"
|
||||
>{#snippet children({
|
||||
segments
|
||||
})}{#each segments as segment, i (`${segment.part}:${i}`)}<ColorPicker.ChannelSegment
|
||||
part={segment.part}>{segment.value}</ColorPicker.ChannelSegment
|
||||
>{/each}{/snippet}</ColorPicker.ChannelInput
|
||||
>
|
||||
<ColorPicker.FormatSelect />
|
||||
<ColorPicker.ValueText format={displayFormat} data-override-display />
|
||||
<ColorPicker.TransparencyGrid {cellSize} style="height: 20px;" />
|
||||
<ColorPicker.Presets aria-label="Presets" />
|
||||
<ColorPicker.SwatchGroup aria-label="Direct"
|
||||
><ColorPicker.SwatchTrigger color="#00ff00" /></ColorPicker.SwatchGroup
|
||||
>
|
||||
<ColorPicker.SavedSwatches aria-label="Saved" />
|
||||
<ColorPicker.SaveAction saveLabel="Save color" deleteLabel="Delete color" />
|
||||
<ColorPicker.EyeDropper />
|
||||
{#if !inline}<ColorPicker.Footer
|
||||
><ColorPicker.Clear>Clear</ColorPicker.Clear><ColorPicker.Cancel>Cancel</ColorPicker.Cancel
|
||||
><ColorPicker.Close>Done</ColorPicker.Close></ColorPicker.Footer
|
||||
>{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<section style="width: 900px; padding: 16px; background: white; color: black;">
|
||||
<h1>ColorPicker audit</h1>
|
||||
<form onsubmit={(e) => e.preventDefault()}>
|
||||
<ColorPicker
|
||||
bind:value
|
||||
bind:format
|
||||
bind:open
|
||||
bind:savedColors
|
||||
{inline}
|
||||
{mode}
|
||||
{dir}
|
||||
{size}
|
||||
{variant}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{autoFormat}
|
||||
{allowedFormats}
|
||||
{onValueChange}
|
||||
{onValueChangeEnd}
|
||||
{onFormatChange}
|
||||
{onOpenChange}
|
||||
{onSavedColorsChange}
|
||||
maxSavedColors={2}
|
||||
presets={['#ff0000', '#00ff00', '#0000ff']}
|
||||
{...{ name: 'brandColor' }}
|
||||
>
|
||||
<ColorPicker.Label>Accent</ColorPicker.Label>
|
||||
<ColorPicker.Control
|
||||
><ColorPicker.Trigger><ColorPicker.ValueText /></ColorPicker.Trigger
|
||||
><ColorPicker.ValueSwatch /></ColorPicker.Control
|
||||
>
|
||||
<ColorPicker.HiddenInput />
|
||||
{#if inline}{@render body()}{:else}<ColorPicker.Portal
|
||||
><ColorPicker.Content motion="none">{@render body()}</ColorPicker.Content
|
||||
></ColorPicker.Portal
|
||||
>{/if}
|
||||
</ColorPicker>
|
||||
</form>
|
||||
<button onclick={() => (dir = dir === 'ltr' ? 'rtl' : 'ltr')}>Toggle direction</button>
|
||||
<button onclick={() => (size = size === 'md' ? 'sm' : size === 'sm' ? 'lg' : 'md')}
|
||||
>Next size</button
|
||||
>
|
||||
<button
|
||||
onclick={() =>
|
||||
(variant = variant === 'surface' ? 'outline' : variant === 'outline' ? 'ghost' : 'surface')}
|
||||
>Next variant</button
|
||||
>
|
||||
<button onclick={() => (disabled = !disabled)}>Toggle disabled</button><button
|
||||
onclick={() => (readonly = !readonly)}>Toggle readonly</button
|
||||
>
|
||||
<button onclick={() => (autoFormat = !autoFormat)}>Toggle auto format</button><button
|
||||
onclick={() => (format = 'hsl')}>External HSL</button
|
||||
><button onclick={() => (allowedFormats = ['hex'])}>Lock format</button>
|
||||
<button
|
||||
onclick={() => {
|
||||
xChannel = 'red';
|
||||
yChannel = 'green';
|
||||
}}>RGB area</button
|
||||
>
|
||||
<button
|
||||
onclick={() => {
|
||||
channel = 'red';
|
||||
orientation = 'vertical';
|
||||
}}>Red vertical</button
|
||||
><button
|
||||
onclick={() => {
|
||||
cellSize = 12;
|
||||
displayFormat = 'rgb';
|
||||
}}>Change displays</button
|
||||
>
|
||||
<output data-value>{JSON.stringify(value)}</output><output data-format>{format}</output><output
|
||||
data-open>{String(open)}</output
|
||||
><output data-saved>{JSON.stringify(savedColors)}</output>
|
||||
<div
|
||||
data-destination
|
||||
style="position: absolute; left: 0; top: 850px; width: 12px; height: 12px; background: gray;"
|
||||
></div>
|
||||
</section>
|
||||
@ -0,0 +1,72 @@
|
||||
import { flushSync, mount, unmount } from 'svelte';
|
||||
import { expect, vi } from 'vitest';
|
||||
import { createActiveUix } from '$active-uix';
|
||||
import type { ColorValue } from '$libs/color';
|
||||
import '../../../index.css';
|
||||
import ColorPickerAuditHarness from './color-picker-audit-harness.svelte';
|
||||
export function renderColorPicker(options: { inline?: boolean; mode?: 'inline' | 'modal' } = {}) {
|
||||
const uix = createActiveUix({
|
||||
langs: { schema: {}, defaultLocale: 'es' },
|
||||
prefs: { storage: false },
|
||||
projectPrefs: false
|
||||
});
|
||||
const target = document.createElement('div');
|
||||
document.body.append(target);
|
||||
const callbacks = {
|
||||
onValueChange: vi.fn(),
|
||||
onValueChangeEnd: vi.fn(),
|
||||
onFormatChange: vi.fn(),
|
||||
onOpenChange: vi.fn(),
|
||||
onSavedColorsChange: vi.fn()
|
||||
};
|
||||
const component = mount(ColorPickerAuditHarness, {
|
||||
target,
|
||||
props: { uix, ...options, ...callbacks }
|
||||
});
|
||||
flushSync();
|
||||
return {
|
||||
target,
|
||||
uix,
|
||||
...callbacks,
|
||||
get root() {
|
||||
return target.querySelector<HTMLElement>('[data-color-picker]')!;
|
||||
},
|
||||
part(name: string) {
|
||||
return document.querySelector<HTMLElement>(`[data-color-picker-${name}]`)!;
|
||||
},
|
||||
button(text: string) {
|
||||
return Array.from(document.querySelectorAll('button')).find(
|
||||
(b) => b.textContent?.trim() === text
|
||||
)!;
|
||||
},
|
||||
value(): ColorValue | undefined {
|
||||
return JSON.parse(target.querySelector('output[data-value]')!.textContent!);
|
||||
},
|
||||
format() {
|
||||
return target.querySelector('output[data-format]')!.textContent!;
|
||||
},
|
||||
open() {
|
||||
return target.querySelector('output[data-open]')!.textContent === 'true';
|
||||
},
|
||||
saved(): string[] {
|
||||
return JSON.parse(target.querySelector('output[data-saved]')!.textContent!);
|
||||
},
|
||||
radios(label: string) {
|
||||
return Array.from(
|
||||
document
|
||||
.querySelector(`[role="radiogroup"][aria-label="${label}"]`)!
|
||||
.querySelectorAll<HTMLButtonElement>('[role="radio"]')
|
||||
);
|
||||
},
|
||||
async ready() {
|
||||
await new Promise<void>((r) => requestAnimationFrame(() => r()));
|
||||
await new Promise<void>((r) => requestAnimationFrame(() => r()));
|
||||
await vi.waitFor(() => expect(this.root).not.toBeNull());
|
||||
},
|
||||
async dispose() {
|
||||
await unmount(component);
|
||||
uix.dispose();
|
||||
target.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Reference in new issue