parent
e60e079a26
commit
153b5fc17d
@ -0,0 +1,323 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { page, userEvent } from 'vitest/browser';
|
||||
import { flushSync } from 'svelte';
|
||||
import { visit, type View } from '$uix/test/empty-state/empty-state-audit-view';
|
||||
const node = (view: View, part: string) => view.node('#audit-empty-state-' + part);
|
||||
const css = (view: View, part?: string) => getComputedStyle(part ? node(view, part) : view.root());
|
||||
it('native default EmptyState is a passive centered surface with real heading and composed actions', () =>
|
||||
visit(async (view) => {
|
||||
expect(view.root().tagName).toBe('DIV');
|
||||
expect(view.root().getAttribute('data-size')).toBe('md');
|
||||
expect(view.root().hasAttribute('role')).toBe(false);
|
||||
expect(view.root().hasAttribute('aria-live')).toBe(false);
|
||||
expect(view.root().hasAttribute('tabindex')).toBe(false);
|
||||
expect(node(view, 'title').tagName).toBe('H3');
|
||||
expect(node(view, 'description').tagName).toBe('P');
|
||||
expect(node(view, 'media').getAttribute('data-kind')).toBe('icon');
|
||||
expect(css(view).display).toBe('flex');
|
||||
expect(css(view).flexDirection).toBe('column');
|
||||
expect(css(view).alignItems).toBe('center');
|
||||
expect(css(view).textAlign).toBe('center');
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: 'No projects yet', level: 3 }))
|
||||
.toBeVisible();
|
||||
await view.capture('default');
|
||||
}));
|
||||
for (const size of ['sm', 'md', 'lg'] as const) {
|
||||
it(
|
||||
'native EmptyState ' + size + ' density paints padding typography and proportional icon plate',
|
||||
() =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('data-size')).toBe(size);
|
||||
expect(parseFloat(css(view).paddingBlockStart)).toBeGreaterThan(0);
|
||||
expect(parseFloat(css(view, 'title').fontSize)).toBeGreaterThan(
|
||||
parseFloat(css(view, 'description').fontSize)
|
||||
);
|
||||
expect(node(view, 'media').getBoundingClientRect().width).toBeCloseTo(
|
||||
parseFloat(css(view, 'media').fontSize) * 2,
|
||||
1
|
||||
);
|
||||
expect(css(view, 'media').backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
|
||||
await view.capture('size-' + size);
|
||||
},
|
||||
{ size }
|
||||
)
|
||||
);
|
||||
}
|
||||
for (const level of [2, 3, 4, 5, 6] as const) {
|
||||
it('native EmptyState heading level ' + level + ' remains decoupled from visual typography', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(node(view, 'title').tagName).toBe('H' + level);
|
||||
expect(css(view, 'title').marginTop).toBe('0px');
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: 'No projects yet', level }))
|
||||
.toBeVisible();
|
||||
await view.capture('level-' + level);
|
||||
},
|
||||
{ level }
|
||||
)
|
||||
);
|
||||
}
|
||||
it('native EmptyState reactive heading level preserves ID copy and visual size', () =>
|
||||
visit(async (view) => {
|
||||
const font = css(view, 'title').fontSize;
|
||||
flushSync(() => view.controls().level(5));
|
||||
expect(node(view, 'title').tagName).toBe('H5');
|
||||
expect(node(view, 'title').id).toBe('audit-empty-state-title');
|
||||
expect(css(view, 'title').fontSize).toBe(font);
|
||||
expect(node(view, 'title').textContent).toBe('No projects yet');
|
||||
await view.capture('reactive-level');
|
||||
}));
|
||||
it('native EmptyState media kind leaves consumer art without icon plate chrome', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(node(view, 'media').getAttribute('data-kind')).toBe('media');
|
||||
expect(css(view, 'media').backgroundColor).toBe('rgba(0, 0, 0, 0)');
|
||||
expect(css(view, 'media').borderRadius).toBe('0px');
|
||||
expect(node(view, 'image').getAttribute('alt')).toBe('Project illustration');
|
||||
await expect.element(page.getByRole('img', { name: 'Project illustration' })).toBeVisible();
|
||||
await view.capture('media-art');
|
||||
},
|
||||
{ kind: 'media', image: true }
|
||||
));
|
||||
it('native EmptyState reactive media kind removes and restores its plate on the same node', () =>
|
||||
visit(async (view) => {
|
||||
const media = node(view, 'media');
|
||||
const width = media.getBoundingClientRect().width;
|
||||
flushSync(() => view.controls().kind('media'));
|
||||
expect(node(view, 'media')).toBe(media);
|
||||
expect(css(view, 'media').backgroundColor).toBe('rgba(0, 0, 0, 0)');
|
||||
flushSync(() => view.controls().kind('icon'));
|
||||
expect(media.getBoundingClientRect().width).toBe(width);
|
||||
await view.capture('media-roundtrip');
|
||||
}));
|
||||
it('native EmptyState Actions label creates a named group with two consumer buttons', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(node(view, 'actions').getAttribute('role')).toBe('group');
|
||||
expect(node(view, 'actions').getAttribute('aria-label')).toBe('Project actions');
|
||||
await expect
|
||||
.element(page.getByRole('group', { name: 'Project actions', exact: true }))
|
||||
.toBeVisible();
|
||||
expect(node(view, 'actions').querySelectorAll('button').length).toBe(2);
|
||||
await view.capture('named-actions');
|
||||
},
|
||||
{ label: 'Project actions' }
|
||||
));
|
||||
it('native unnamed EmptyState Actions stays a plain wrapping row', () =>
|
||||
visit(async (view) => {
|
||||
expect(node(view, 'actions').hasAttribute('role')).toBe(false);
|
||||
expect(node(view, 'actions').hasAttribute('aria-label')).toBe(false);
|
||||
expect(css(view, 'actions').flexWrap).toBe('wrap');
|
||||
await view.capture('unnamed-actions');
|
||||
}));
|
||||
it('native EmptyState action group label changes and clears reactively', () =>
|
||||
visit(async (view) => {
|
||||
const actions = node(view, 'actions');
|
||||
flushSync(() => view.controls().label('Recovery actions'));
|
||||
expect(node(view, 'actions')).toBe(actions);
|
||||
expect(actions.getAttribute('aria-label')).toBe('Recovery actions');
|
||||
flushSync(() => view.controls().label(undefined));
|
||||
expect(actions.hasAttribute('role')).toBe(false);
|
||||
expect(actions.hasAttribute('aria-label')).toBe(false);
|
||||
await view.capture('label-clear');
|
||||
}));
|
||||
it('native EmptyState consumer root role name and live policy pass through', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('role')).toBe('status');
|
||||
expect(view.root().getAttribute('aria-label')).toBe('Project availability');
|
||||
expect(view.root().getAttribute('aria-live')).toBe('polite');
|
||||
await view.capture('explicit-root-aria');
|
||||
},
|
||||
{ rootRole: 'status', rootLabel: 'Project availability', rootLive: 'polite' }
|
||||
));
|
||||
it('native EmptyState all composed parts preserve consumer IDs classes and data attributes', () =>
|
||||
visit(async (view) => {
|
||||
expect(view.root().className).toBe('audit-root');
|
||||
expect(view.root().getAttribute('data-probe')).toBe('root');
|
||||
for (const part of ['media', 'title', 'description', 'actions']) {
|
||||
expect(node(view, part).className).toBe('audit-' + part);
|
||||
expect(node(view, part).getAttribute('data-probe')).toBe(part);
|
||||
}
|
||||
await view.capture('passthrough');
|
||||
}));
|
||||
it('native EmptyState omitted parts do not create fallback wrappers or content', () =>
|
||||
visit(async (view) => {
|
||||
flushSync(() => {
|
||||
view.controls().media(false);
|
||||
view.controls().description(false);
|
||||
view.controls().actions(false);
|
||||
});
|
||||
expect(view.root().children.length).toBe(1);
|
||||
expect(view.root().firstElementChild).toBe(node(view, 'title'));
|
||||
await view.capture('title-only');
|
||||
}));
|
||||
it('native EmptyState empty composition remains a passive padded root', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().children.length).toBe(0);
|
||||
expect(view.root().getBoundingClientRect().height).toBeGreaterThan(0);
|
||||
await view.capture('empty');
|
||||
},
|
||||
{ media: false, title: false, description: false, actions: false }
|
||||
));
|
||||
it('native EmptyState consumer copy updates on the existing heading', () =>
|
||||
visit(async (view) => {
|
||||
const title = node(view, 'title');
|
||||
flushSync(() => view.controls().copy('No matching projects'));
|
||||
expect(node(view, 'title')).toBe(title);
|
||||
expect(title.textContent).toBe('No matching projects');
|
||||
await view.capture('copy-update');
|
||||
}));
|
||||
it('native EmptyState density updates preserve root and grow its padding and heading', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.root();
|
||||
const small = {
|
||||
padding: parseFloat(css(view).paddingTop),
|
||||
font: parseFloat(css(view, 'title').fontSize)
|
||||
};
|
||||
flushSync(() => view.controls().size('lg'));
|
||||
expect(view.root()).toBe(root);
|
||||
expect(parseFloat(css(view).paddingTop)).toBeGreaterThan(small.padding);
|
||||
expect(parseFloat(css(view, 'title').fontSize)).toBeGreaterThan(small.font);
|
||||
await view.capture('size-update');
|
||||
},
|
||||
{ size: 'sm' }
|
||||
));
|
||||
it('native EmptyState responsive size follows live viewport and restores the same root', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.root();
|
||||
await expect.poll(() => root.getAttribute('data-size')).toBe('lg');
|
||||
await page.viewport(900, 900);
|
||||
await view.ready();
|
||||
await expect.poll(() => root.getAttribute('data-size')).toBe('md');
|
||||
await page.viewport(350, 900);
|
||||
await view.ready();
|
||||
await expect.poll(() => root.getAttribute('data-size')).toBe('sm');
|
||||
await page.viewport(1100, 900);
|
||||
await view.ready();
|
||||
await expect.poll(() => root.getAttribute('data-size')).toBe('lg');
|
||||
expect(view.root()).toBe(root);
|
||||
await view.capture('responsive');
|
||||
},
|
||||
{ size: { base: 'sm', md: 'md', lg: 'lg' } }
|
||||
));
|
||||
it('native EmptyState wraps description and actions inside a 350px viewport', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getBoundingClientRect().right).toBeLessThanOrEqual(350);
|
||||
expect(node(view, 'description').scrollWidth).toBeLessThanOrEqual(
|
||||
node(view, 'description').clientWidth
|
||||
);
|
||||
expect(node(view, 'actions').getBoundingClientRect().height).toBeGreaterThan(
|
||||
node(view, 'create').getBoundingClientRect().height
|
||||
);
|
||||
expect(node(view, 'import').getBoundingClientRect().right).toBeLessThanOrEqual(350);
|
||||
await view.capture('narrow');
|
||||
},
|
||||
{ viewportWidth: 350 }
|
||||
));
|
||||
it('native EmptyState RTL preserves centered geometry and reverses the action row', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(css(view).direction).toBe('rtl');
|
||||
expect(node(view, 'create').getBoundingClientRect().left).toBeGreaterThan(
|
||||
node(view, 'import').getBoundingClientRect().left
|
||||
);
|
||||
expect(css(view).paddingLeft).toBe(css(view).paddingRight);
|
||||
await view.capture('rtl');
|
||||
},
|
||||
{ dir: 'rtl' }
|
||||
));
|
||||
it('native EmptyState public tokens retune spacing typography media and description measure', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(css(view).gap).toBe('20px');
|
||||
expect(css(view).paddingTop).toBe('22px');
|
||||
expect(css(view, 'title').fontSize).toBe('26px');
|
||||
expect(css(view, 'media').borderRadius).toBe('7px');
|
||||
expect(node(view, 'description').getBoundingClientRect().width).toBeCloseTo(240, 0);
|
||||
await view.capture('tokens');
|
||||
},
|
||||
{
|
||||
style:
|
||||
'--empty-state-gap:20px;--empty-state-padding-block-md:22px;--empty-state-title-font-size-md:26px;--empty-state-media-radius:7px;--empty-state-description-measure:240px;'
|
||||
}
|
||||
));
|
||||
it('native EmptyState adds only its consumer buttons to sequential Tab order', () =>
|
||||
visit(async (view) => {
|
||||
await userEvent.click(page.getByRole('button', { name: 'Before', exact: true }));
|
||||
await userEvent.tab();
|
||||
expect(document.activeElement).toBe(node(view, 'create'));
|
||||
await userEvent.tab();
|
||||
expect(document.activeElement).toBe(node(view, 'import'));
|
||||
await userEvent.tab();
|
||||
expect(document.activeElement).toBe(node(view, 'after'));
|
||||
await view.capture('tab');
|
||||
}));
|
||||
it('native EmptyState without actions adds no Tab stop', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
await userEvent.click(page.getByRole('button', { name: 'Before', exact: true }));
|
||||
await userEvent.tab();
|
||||
expect(document.activeElement).toBe(node(view, 'after'));
|
||||
await view.capture('no-actions-tab');
|
||||
},
|
||||
{ actions: false }
|
||||
));
|
||||
for (const activation of ['click', 'Enter', 'Space'] as const) {
|
||||
it(
|
||||
'native EmptyState consumer Button ' +
|
||||
activation +
|
||||
' activates once without committing the surface',
|
||||
() =>
|
||||
visit(async (view) => {
|
||||
const button = node(view, 'create');
|
||||
if (activation === 'click')
|
||||
await userEvent.click(page.getByRole('button', { name: 'Create project', exact: true }));
|
||||
else {
|
||||
button.focus();
|
||||
await userEvent.keyboard(activation === 'Enter' ? '{Enter}' : ' ');
|
||||
}
|
||||
expect(node(view, 'count').textContent).toBe('1');
|
||||
expect(button.getAttribute('data-event')).toBe('contact-activate');
|
||||
expect(view.root().hasAttribute('data-event')).toBe(false);
|
||||
expect(view.root().isConnected).toBe(true);
|
||||
await view.capture('activation-' + activation.toLowerCase());
|
||||
})
|
||||
);
|
||||
}
|
||||
it('native EmptyState removal belongs to the consumer action handler', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
await userEvent.click(page.getByRole('button', { name: 'Create project', exact: true }));
|
||||
expect(view.root()).toBeNull();
|
||||
expect(node(view, 'count').textContent).toBe('1');
|
||||
await view.capture('consumer-removal');
|
||||
},
|
||||
{ removeOnAction: true }
|
||||
));
|
||||
it('native nested EmptyState keeps independent density and heading semantics', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(node(view, 'nested').getAttribute('data-size')).toBe('sm');
|
||||
expect(node(view, 'nested').querySelector('[data-empty-state-title]')!.tagName).toBe('H4');
|
||||
expect(node(view, 'title').tagName).toBe('H3');
|
||||
await view.capture('nested');
|
||||
},
|
||||
{ nested: true }
|
||||
));
|
||||
it('native EmptyState reduced motion stays static', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(css(view).animationName).toBe('none');
|
||||
expect(css(view).transitionDuration).toBe('0s');
|
||||
await view.capture('reduce');
|
||||
},
|
||||
{ reduce: true }
|
||||
));
|
||||
@ -0,0 +1,170 @@
|
||||
<script module lang="ts">
|
||||
import type {
|
||||
EmptyStateProps,
|
||||
EmptyStateMediaProps,
|
||||
EmptyStateTitleProps
|
||||
} from '$uix/eidos/components/empty-state';
|
||||
export type Config = {
|
||||
size?: EmptyStateProps['size'];
|
||||
kind?: EmptyStateMediaProps['kind'];
|
||||
level?: EmptyStateTitleProps['level'];
|
||||
label?: string;
|
||||
style?: string;
|
||||
media?: boolean;
|
||||
title?: boolean;
|
||||
description?: boolean;
|
||||
actions?: boolean;
|
||||
image?: boolean;
|
||||
nested?: boolean;
|
||||
rootRole?: EmptyStateProps['role'];
|
||||
rootLabel?: string;
|
||||
rootLive?: EmptyStateProps['aria-live'];
|
||||
removeOnAction?: boolean;
|
||||
dir?: 'ltr' | 'rtl';
|
||||
reduce?: boolean;
|
||||
viewportWidth?: number;
|
||||
};
|
||||
export type Controls = {
|
||||
size: (next: EmptyStateProps['size']) => void;
|
||||
kind: (next: EmptyStateMediaProps['kind']) => void;
|
||||
level: (next: EmptyStateTitleProps['level']) => void;
|
||||
label: (next: string | undefined) => void;
|
||||
media: (next: boolean) => void;
|
||||
title: (next: boolean) => void;
|
||||
description: (next: boolean) => void;
|
||||
actions: (next: boolean) => void;
|
||||
copy: (next: string) => void;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { setActiveUix, type ActiveUix } from '$active-uix';
|
||||
import { Soma } from '$soma/core/soma.svelte';
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import EmptyState from '$uix/eidos/components/empty-state';
|
||||
import Button from '$uix/eidos/components/button';
|
||||
let {
|
||||
uix,
|
||||
initial = {},
|
||||
onControls
|
||||
}: { uix: ActiveUix; initial?: Config; onControls: (controls: Controls) => void } = $props();
|
||||
// svelte-ignore state_referenced_locally
|
||||
setActiveUix(uix);
|
||||
Soma.create();
|
||||
const eidos = ActiveEidos.create({ applyDom: false });
|
||||
onDestroy(() => eidos.dispose());
|
||||
// svelte-ignore state_referenced_locally
|
||||
let size = $state(initial.size);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let kind = $state(initial.kind);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let level = $state(initial.level);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let label = $state(initial.label);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let media = $state(initial.media ?? true);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let title = $state(initial.title ?? true);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let description = $state(initial.description ?? true);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let actions = $state(initial.actions ?? true);
|
||||
let copy = $state('No projects yet');
|
||||
let count = $state(0);
|
||||
let visible = $state(true);
|
||||
const image =
|
||||
'data:image/svg+xml,' +
|
||||
encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="800" height="120" viewBox="0 0 800 120"><rect width="800" height="120" fill="#dae9f2"/><path d="M360 70h80V40h-80z" fill="#247095"/></svg>'
|
||||
);
|
||||
onMount(() =>
|
||||
onControls({
|
||||
size: (next) => (size = next),
|
||||
kind: (next) => (kind = next),
|
||||
level: (next) => (level = next),
|
||||
label: (next) => (label = next),
|
||||
media: (next) => (media = next),
|
||||
title: (next) => (title = next),
|
||||
description: (next) => (description = next),
|
||||
actions: (next) => (actions = next),
|
||||
copy: (next) => (copy = next)
|
||||
})
|
||||
);
|
||||
function activate() {
|
||||
count += 1;
|
||||
if (initial.removeOnAction) visible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="audit-empty-state-stage"
|
||||
dir={initial.dir ?? 'ltr'}
|
||||
data-motion={initial.reduce ? 'reduce' : undefined}
|
||||
style="padding:24px;font:16px Arial,sans-serif;color:rgb(10,20,30)"
|
||||
>
|
||||
<div style="max-inline-size:520px">
|
||||
<button id="audit-empty-state-before" type="button">Before</button>
|
||||
{#if visible}
|
||||
<EmptyState
|
||||
id="audit-empty-state"
|
||||
class="audit-root"
|
||||
data-probe="root"
|
||||
{size}
|
||||
style={initial.style}
|
||||
role={initial.rootRole}
|
||||
aria-label={initial.rootLabel}
|
||||
aria-live={initial.rootLive}
|
||||
>
|
||||
{#if media}
|
||||
<EmptyState.Media
|
||||
id="audit-empty-state-media"
|
||||
class="audit-media"
|
||||
data-probe="media"
|
||||
{kind}
|
||||
>
|
||||
{#if initial.image}<img
|
||||
id="audit-empty-state-image"
|
||||
src={image}
|
||||
alt="Project illustration"
|
||||
width="128"
|
||||
height="48"
|
||||
/>
|
||||
{:else}<span aria-hidden="true" data-audit-glyph>◇</span>{/if}
|
||||
</EmptyState.Media>
|
||||
{/if}
|
||||
{#if title}<EmptyState.Title
|
||||
id="audit-empty-state-title"
|
||||
class="audit-title"
|
||||
data-probe="title"
|
||||
{level}>{copy}</EmptyState.Title
|
||||
>{/if}
|
||||
{#if description}<EmptyState.Description
|
||||
id="audit-empty-state-description"
|
||||
class="audit-description"
|
||||
data-probe="description"
|
||||
>Create your first project to get started. You can also import a project from an
|
||||
existing workspace.</EmptyState.Description
|
||||
>{/if}
|
||||
{#if actions}<EmptyState.Actions
|
||||
id="audit-empty-state-actions"
|
||||
class="audit-actions"
|
||||
data-probe="actions"
|
||||
{label}
|
||||
>
|
||||
<Button id="audit-empty-state-create" onclick={activate}>Create project</Button>
|
||||
<Button id="audit-empty-state-import" variant="outline" onclick={activate}
|
||||
>Import project</Button
|
||||
>
|
||||
</EmptyState.Actions>{/if}
|
||||
{#if initial.nested}<EmptyState id="audit-empty-state-nested" size="sm"
|
||||
><EmptyState.Title level={4}>Nested empty</EmptyState.Title><EmptyState.Description
|
||||
>Independent compact content</EmptyState.Description
|
||||
></EmptyState
|
||||
>{/if}
|
||||
</EmptyState>
|
||||
{/if}
|
||||
<button id="audit-empty-state-after" type="button">After</button>
|
||||
<output id="audit-empty-state-count">{count}</output>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,54 @@
|
||||
import { mount, unmount, flushSync } from 'svelte';
|
||||
import { page } from 'vitest/browser';
|
||||
import { createActiveUix } from '$active-uix';
|
||||
import '$uix/eidos/index.css';
|
||||
import Harness, { type Config, type Controls } from './empty-state-audit-harness.svelte';
|
||||
export function renderEmptyState(initial: Config = {}) {
|
||||
const uix = createActiveUix({
|
||||
langs: { schema: {}, defaultLocale: 'en' },
|
||||
prefs: { storage: false },
|
||||
projectPrefs: false
|
||||
});
|
||||
const target = document.createElement('div');
|
||||
document.body.append(target);
|
||||
let controls!: Controls;
|
||||
const component = mount(Harness, {
|
||||
target,
|
||||
props: { uix, initial, onControls: (value) => (controls = value) }
|
||||
});
|
||||
flushSync();
|
||||
const ready = async () =>
|
||||
new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
||||
);
|
||||
return {
|
||||
uix,
|
||||
target,
|
||||
controls: () => controls,
|
||||
root: () => target.querySelector<HTMLElement>('#audit-empty-state')!,
|
||||
node: (selector: string) => target.querySelector<HTMLElement>(selector)!,
|
||||
ready,
|
||||
async capture(name: string) {
|
||||
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
|
||||
await ready();
|
||||
await page.screenshot({ path: 'tmp/empty-state-' + name + '.png' });
|
||||
}
|
||||
},
|
||||
async dispose() {
|
||||
await unmount(component);
|
||||
uix.dispose();
|
||||
target.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
export type View = ReturnType<typeof renderEmptyState>;
|
||||
export async function visit(fn: (view: View) => Promise<void>, initial: Config = {}) {
|
||||
await page.viewport(initial.viewportWidth ?? 1100, 900);
|
||||
const view = renderEmptyState(initial);
|
||||
try {
|
||||
await view.ready();
|
||||
await fn(view);
|
||||
} finally {
|
||||
await view.dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue