parent
f086a32727
commit
d97d993228
@ -0,0 +1,321 @@
|
||||
import { expect, it, vi } from 'vitest';
|
||||
import { page } from 'vitest/browser';
|
||||
import { flushSync } from 'svelte';
|
||||
import { visit } from '$uix/test/progress/progress-audit-view';
|
||||
const css = (view: Parameters<Parameters<typeof visit>[0]>[0]) =>
|
||||
getComputedStyle(view.indicator()!);
|
||||
const fill = (view: Parameters<Parameters<typeof visit>[0]>[0]) =>
|
||||
getComputedStyle(view.indicator()!, '::before');
|
||||
|
||||
it('native default Progress omits the unknown value and animates a linear track', () =>
|
||||
visit(async (view) => {
|
||||
expect(view.root().tagName).toBe('DIV');
|
||||
expect(view.root().getAttribute('role')).toBe('progressbar');
|
||||
expect(view.root().getAttribute('aria-valuemin')).toBe('0');
|
||||
expect(view.root().getAttribute('aria-valuemax')).toBe('100');
|
||||
expect(view.root().hasAttribute('aria-valuenow')).toBe(false);
|
||||
expect(view.root().hasAttribute('aria-valuetext')).toBe(false);
|
||||
expect(view.root().getAttribute('aria-label')).toBe('Loading');
|
||||
expect(view.root().getAttribute('data-state')).toBe('indeterminate');
|
||||
expect(view.root().getAttribute('data-size')).toBe('md');
|
||||
expect(view.root().getAttribute('data-shape')).toBe('linear');
|
||||
expect(view.root().getAttribute('data-orientation')).toBe('horizontal');
|
||||
expect(view.root().classList.contains('audit-progress')).toBe(true);
|
||||
expect(view.root().getAttribute('data-probe')).toBe('progress');
|
||||
expect(view.root().title).toBe('Audit Progress');
|
||||
expect(view.valueText()!.textContent).toBe(' ');
|
||||
expect(view.indicator()!.getAttribute('data-state')).toBe('indeterminate');
|
||||
expect(css(view).height).toBe('6px');
|
||||
expect(fill(view).animationName).toBe('progress-indeterminate');
|
||||
expect(fill(view).animationDuration).toBe('1.1s');
|
||||
expect(fill(view).width).toBe('147px');
|
||||
await view.capture('default');
|
||||
}));
|
||||
it('native determinate Progress publishes the value and scaled fill', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('aria-valuenow')).toBe('30');
|
||||
expect(view.root().getAttribute('aria-valuetext')).toBe('30 / 100');
|
||||
expect(view.valueText()!.textContent).toBe(' 30 / 100');
|
||||
expect(view.root().getAttribute('data-state')).toBe('loading');
|
||||
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('30');
|
||||
expect(fill(view).width).toBe('126px');
|
||||
expect(fill(view).animationName).toBe('none');
|
||||
await view.capture('determinate');
|
||||
},
|
||||
{ value: 30 }
|
||||
));
|
||||
it('native custom scale subtracts min before computing the fill percentage', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('aria-valuemin')).toBe('20');
|
||||
expect(view.root().getAttribute('aria-valuemax')).toBe('60');
|
||||
expect(view.root().getAttribute('aria-valuenow')).toBe('30');
|
||||
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('25');
|
||||
expect(fill(view).width).toBe('105px');
|
||||
await view.capture('scale');
|
||||
},
|
||||
{ value: 30, min: 20, max: 60 }
|
||||
));
|
||||
it('native completed Progress uses the loaded state and completion color', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('data-state')).toBe('loaded');
|
||||
expect(view.indicator()!.getAttribute('data-state')).toBe('loaded');
|
||||
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('100');
|
||||
expect(fill(view).width).toBe('420px');
|
||||
expect(fill(view).backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
|
||||
const completed = fill(view).backgroundColor;
|
||||
flushSync(() => view.controls().value(50));
|
||||
await vi.waitFor(() => expect(fill(view).backgroundColor).not.toBe(completed));
|
||||
expect(view.root().getAttribute('data-state')).toBe('loading');
|
||||
await view.capture('completion-to-loading');
|
||||
},
|
||||
{ value: 100, reduce: true }
|
||||
));
|
||||
for (const [size, height, diameter, thickness] of [
|
||||
['xs', 2, 32, 3],
|
||||
['sm', 4, 40, 4],
|
||||
['md', 6, 48, 6],
|
||||
['lg', 8, 56, 8],
|
||||
['xl', 12, 64, 10]
|
||||
] as const) {
|
||||
it('native linear ' + size + ' has ' + height + 'px track thickness', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.indicator()!.getBoundingClientRect().width).toBe(420);
|
||||
expect(view.indicator()!.getBoundingClientRect().height).toBe(height);
|
||||
expect(fill(view).width).toBe('210px');
|
||||
await view.capture('linear-' + size);
|
||||
},
|
||||
{ value: 50, size }
|
||||
)
|
||||
);
|
||||
it(
|
||||
'native circular ' + size + ' reserves ' + diameter + 'px with ' + thickness + 'px inset',
|
||||
() =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.indicator()!.getBoundingClientRect().width).toBe(diameter);
|
||||
expect(view.indicator()!.getBoundingClientRect().height).toBe(diameter);
|
||||
expect(fill(view).top).toBe(thickness + 'px');
|
||||
expect(css(view).backgroundImage).toContain('conic-gradient');
|
||||
expect(css(view).backgroundImage).toContain('50%');
|
||||
expect(fill(view).animationName).toBe('none');
|
||||
await view.capture('circular-' + size);
|
||||
},
|
||||
{ value: 50, size, shape: 'circular' }
|
||||
)
|
||||
);
|
||||
}
|
||||
it('native consumer aria label overrides the default translation', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('aria-label')).toBe('Downloading account');
|
||||
await view.capture('custom-label');
|
||||
},
|
||||
{ ariaLabel: 'Downloading account' }
|
||||
));
|
||||
it('native internal Label names Progress and suppresses its generic label', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('aria-labelledby')).toBe('audit-progress-label');
|
||||
expect(view.root().hasAttribute('aria-label')).toBe(false);
|
||||
expect(view.label()!.tagName).toBe('SPAN');
|
||||
expect(view.label()!.textContent).toBe('Import account');
|
||||
await view.capture('internal-label');
|
||||
},
|
||||
{ value: 42, label: true }
|
||||
));
|
||||
it('native external labelledby takes precedence over the internal Label', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('aria-labelledby')).toBe('audit-progress-external');
|
||||
expect(view.root().hasAttribute('aria-label')).toBe(false);
|
||||
expect(view.target.querySelector('#audit-progress-external')!.textContent).toBe(
|
||||
'External task'
|
||||
);
|
||||
await view.capture('external-label');
|
||||
},
|
||||
{ value: 42, label: true, externalLabel: true }
|
||||
));
|
||||
it('native default Progress label follows the active Spanish locale', () =>
|
||||
visit(async (view) => {
|
||||
view.uix.langs.setLocale('es');
|
||||
await vi.waitFor(() => expect(view.root().getAttribute('aria-label')).toBe('Cargando'));
|
||||
await view.capture('locale-es');
|
||||
}));
|
||||
it('native valueText override supplies both visible and ARIA value text', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('aria-valuetext')).toBe('25 of 100 MB');
|
||||
expect(view.valueText()!.textContent).toBe(' 25 of 100 MB');
|
||||
await view.capture('value-text');
|
||||
},
|
||||
{ value: 25, valueText: '25 of 100 MB' }
|
||||
));
|
||||
it('native ValueText snippet receives value range state percentage and text', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.valueText()!.textContent!.trim()).toBe('30|20|60|loading|25|30 / 60');
|
||||
flushSync(() => view.controls().value(60));
|
||||
expect(view.valueText()!.textContent!.trim()).toBe('60|20|60|loaded|100|60 / 60');
|
||||
await view.capture('value-snippet');
|
||||
},
|
||||
{ value: 30, min: 20, max: 60, valueSnippet: true }
|
||||
));
|
||||
it('native indeterminate Progress can retain an explicit value text', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().hasAttribute('aria-valuenow')).toBe(false);
|
||||
expect(view.root().getAttribute('aria-valuetext')).toBe('Waiting for server');
|
||||
expect(view.valueText()!.textContent).toBe(' Waiting for server');
|
||||
await view.capture('unknown-text');
|
||||
},
|
||||
{ value: null, valueText: 'Waiting for server' }
|
||||
));
|
||||
it('native vertical Progress fills from the bottom of a 160px track', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.indicator()!.getBoundingClientRect().width).toBe(6);
|
||||
expect(view.indicator()!.getBoundingClientRect().height).toBe(160);
|
||||
expect(fill(view).height).toBe('40px');
|
||||
expect(fill(view).bottom).toBe('0px');
|
||||
expect(fill(view).width).toBe('6px');
|
||||
await view.capture('vertical');
|
||||
},
|
||||
{ value: 25, orientation: 'vertical' }
|
||||
));
|
||||
it('native vertical unknown Progress uses the vertical sweep', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(fill(view).animationName).toBe('progress-indeterminate-vertical');
|
||||
expect(fill(view).height).toBe('56px');
|
||||
expect(fill(view).width).toBe('6px');
|
||||
await view.capture('vertical-unknown');
|
||||
},
|
||||
{ orientation: 'vertical' }
|
||||
));
|
||||
it('native circular unknown Progress rotates the ring rather than its hole', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(css(view).animationName).toBe('progress-ring-indeterminate');
|
||||
expect(css(view).animationDuration).toBe('1.1s');
|
||||
expect(css(view).backgroundImage).toContain('conic-gradient');
|
||||
expect(fill(view).animationName).toBe('none');
|
||||
expect(fill(view).transform).toBe('none');
|
||||
await view.capture('ring-unknown');
|
||||
},
|
||||
{ shape: 'circular' }
|
||||
));
|
||||
for (const mode of ['linear', 'vertical', 'circular'] as const) {
|
||||
it('native reduce stops ' + mode + ' animation and retains track geometry', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(css(view).animationName).toBe('none');
|
||||
expect(fill(view).animationName).toBe('none');
|
||||
expect(fill(view).transform).toBe('none');
|
||||
expect(css(view).transitionDuration).toBe(mode === 'circular' ? '0.18s' : '0s');
|
||||
expect(fill(view).transitionDuration).toBe(mode === 'circular' ? '0.18s' : '0s');
|
||||
expect(view.indicator()!.getBoundingClientRect().height).toBe(
|
||||
mode === 'circular' ? 48 : mode === 'vertical' ? 160 : 6
|
||||
);
|
||||
await view.capture('reduce-' + mode);
|
||||
},
|
||||
{
|
||||
shape: mode === 'circular' ? 'circular' : 'linear',
|
||||
orientation: mode === 'vertical' ? 'vertical' : 'horizontal',
|
||||
reduce: true
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
it('native consumer CSS fragments and recipe overrides coexist with the value channel', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('40');
|
||||
expect(css(view).height).toBe('10px');
|
||||
expect(fill(view).backgroundColor).toBe('rgb(153, 17, 0)');
|
||||
expect(getComputedStyle(view.root()).outlineWidth).toBe('2px');
|
||||
await view.capture('style');
|
||||
},
|
||||
{
|
||||
value: 40,
|
||||
style: '--progress-height-md:10px;--progress-indicator-bg:#991100;outline:2px solid #123456'
|
||||
}
|
||||
));
|
||||
it('native Progress value scale shape and size react on the same root', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.root();
|
||||
flushSync(() => {
|
||||
view.controls().value(40);
|
||||
view.controls().min(20);
|
||||
view.controls().max(60);
|
||||
view.controls().shape('circular');
|
||||
view.controls().size('lg');
|
||||
view.controls().valueText('Halfway');
|
||||
});
|
||||
expect(view.root()).toBe(root);
|
||||
expect(root.style.getPropertyValue('--_progress-value-pct')).toBe('50');
|
||||
expect(root.getAttribute('aria-valuenow')).toBe('40');
|
||||
expect(view.valueText()!.textContent).toBe(' Halfway');
|
||||
expect(view.indicator()!.getBoundingClientRect().height).toBe(56);
|
||||
flushSync(() => view.controls().value(null));
|
||||
expect(root.getAttribute('data-state')).toBe('indeterminate');
|
||||
expect(root.hasAttribute('aria-valuenow')).toBe(false);
|
||||
await view.capture('reactive');
|
||||
},
|
||||
{ value: 10, reduce: true }
|
||||
));
|
||||
it('native RTL determinate Progress anchors its fill to the logical start', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(getComputedStyle(view.root()).direction).toBe('rtl');
|
||||
expect(fill(view).right).toBe('0px');
|
||||
expect(fill(view).width).toBe('105px');
|
||||
await view.capture('rtl');
|
||||
},
|
||||
{ value: 25, label: true, dir: 'rtl' }
|
||||
));
|
||||
it('native responsive Progress resolves circular xs below the md breakpoint', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('data-shape')).toBe('linear');
|
||||
expect(view.root().getAttribute('data-size')).toBe('lg');
|
||||
await page.viewport(350, 900);
|
||||
await vi.waitFor(() => expect(view.root().getAttribute('data-shape')).toBe('circular'));
|
||||
expect(view.root().getAttribute('data-size')).toBe('xs');
|
||||
expect(view.indicator()!.getBoundingClientRect().width).toBe(32);
|
||||
expect(view.indicator()!.getBoundingClientRect().height).toBe(32);
|
||||
await view.capture('responsive');
|
||||
},
|
||||
{ value: 25, size: { base: 'xs', md: 'lg' }, shape: { base: 'circular', md: 'linear' } }
|
||||
));
|
||||
it('native Progress remains a named semantic root without optional parts', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('role')).toBe('progressbar');
|
||||
expect(view.root().getAttribute('aria-label')).toBe('Loading');
|
||||
expect(view.root().getAttribute('aria-valuenow')).toBe('40');
|
||||
expect(view.indicator()).toBeNull();
|
||||
expect(view.valueText()).toBeNull();
|
||||
expect(view.label()).toBeNull();
|
||||
await view.capture('no-parts');
|
||||
},
|
||||
{ value: 40, indicatorPart: false, valuePart: false }
|
||||
));
|
||||
|
||||
it('native Progress exposes an ordered ARIA range for 100 to 100', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.root().getAttribute('aria-valuemin')).toBe('100');
|
||||
expect(view.root().getAttribute('aria-valuemax')).toBe('100');
|
||||
expect(view.root().getAttribute('aria-valuenow')).toBe('100');
|
||||
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('0');
|
||||
expect(view.root().getAttribute('data-state')).toBe('loaded');
|
||||
expect(getComputedStyle(view.indicator()!, '::before').width).toBe('0px');
|
||||
await view.capture('negative-scale-100');
|
||||
},
|
||||
{ value: 100, min: 100, max: 100, reduce: true }
|
||||
));
|
||||
@ -0,0 +1,126 @@
|
||||
<script module lang="ts">
|
||||
import type { ProgressProps } from '$uix/eidos/components/progress';
|
||||
import type { ValueTextSnippetProps as ProgressValueTextSnippetProps } from '$soma/components/progress';
|
||||
export type Config = {
|
||||
value?: number | null;
|
||||
min?: number;
|
||||
max?: number;
|
||||
size?: ProgressProps['size'];
|
||||
shape?: ProgressProps['shape'];
|
||||
orientation?: ProgressProps['orientation'];
|
||||
valueText?: string;
|
||||
label?: boolean;
|
||||
externalLabel?: boolean;
|
||||
ariaLabel?: string;
|
||||
valuePart?: boolean;
|
||||
indicatorPart?: boolean;
|
||||
valueSnippet?: boolean;
|
||||
style?: string;
|
||||
dir?: 'ltr' | 'rtl';
|
||||
reduce?: boolean;
|
||||
viewportWidth?: number;
|
||||
};
|
||||
export type Controls = {
|
||||
value: (next: number | null) => void;
|
||||
min: (next: number) => void;
|
||||
max: (next: number) => void;
|
||||
size: (next: ProgressProps['size']) => void;
|
||||
shape: (next: ProgressProps['shape']) => void;
|
||||
orientation: (next: ProgressProps['orientation']) => void;
|
||||
valueText: (next: string | undefined) => void;
|
||||
label: (next: boolean) => void;
|
||||
labelId: (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 Progress from '$uix/eidos/components/progress';
|
||||
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 value = $state(initial.value);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let min = $state(initial.min);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let max = $state(initial.max);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let size = $state(initial.size);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let shape = $state(initial.shape);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let orientation = $state(initial.orientation);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let valueText = $state(initial.valueText);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let showLabel = $state(initial.label ?? false);
|
||||
let labelId = $state('audit-progress-label');
|
||||
onMount(() =>
|
||||
onControls({
|
||||
value: (next) => (value = next),
|
||||
min: (next) => (min = next),
|
||||
max: (next) => (max = next),
|
||||
size: (next) => (size = next),
|
||||
shape: (next) => (shape = next),
|
||||
orientation: (next) => (orientation = next),
|
||||
valueText: (next) => (valueText = next),
|
||||
label: (next) => (showLabel = next),
|
||||
labelId: (next) => (labelId = next)
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
{#snippet customValue(info: ProgressValueTextSnippetProps)}
|
||||
<span data-audit-value-snippet=""
|
||||
>{info.value}|{info.min}|{info.max}|{info.state}|{info.percentage}|{info.valueText ??
|
||||
'none'}</span
|
||||
>
|
||||
{/snippet}
|
||||
<div
|
||||
id="audit-progress-stage"
|
||||
dir={initial.dir ?? 'ltr'}
|
||||
data-motion={initial.reduce ? 'reduce' : undefined}
|
||||
style="padding:24px;font:16px Arial,sans-serif"
|
||||
>
|
||||
<div style="max-inline-size:420px">
|
||||
{#if initial.externalLabel}<p id="audit-progress-external">External task</p>{/if}
|
||||
<Progress
|
||||
id="audit-progress"
|
||||
class="audit-progress"
|
||||
data-probe="progress"
|
||||
title="Audit Progress"
|
||||
{value}
|
||||
{min}
|
||||
{max}
|
||||
{size}
|
||||
{shape}
|
||||
{orientation}
|
||||
{valueText}
|
||||
aria-label={initial.ariaLabel}
|
||||
aria-labelledby={initial.externalLabel ? 'audit-progress-external' : undefined}
|
||||
style={initial.style}
|
||||
>
|
||||
{#if showLabel}<Progress.Label id={labelId}>Import account</Progress.Label>{/if}
|
||||
{#if initial.valuePart !== false}<Progress.ValueText
|
||||
id="audit-progress-value"
|
||||
children={initial.valueSnippet ? customValue : undefined}
|
||||
/>{/if}
|
||||
{#if initial.indicatorPart !== false}<Progress.Indicator id="audit-progress-indicator" />{/if}
|
||||
</Progress>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,56 @@
|
||||
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 './progress-audit-harness.svelte';
|
||||
export function renderProgress(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-progress')!,
|
||||
label: () => target.querySelector<HTMLElement>('[data-progress-label]'),
|
||||
valueText: () => target.querySelector<HTMLElement>('#audit-progress-value'),
|
||||
indicator: () => target.querySelector<HTMLElement>('#audit-progress-indicator'),
|
||||
ready,
|
||||
async capture(name: string) {
|
||||
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
|
||||
await ready();
|
||||
await page.screenshot({ path: 'tmp/progress-' + name + '.png' });
|
||||
}
|
||||
},
|
||||
async dispose() {
|
||||
await unmount(component);
|
||||
uix.dispose();
|
||||
target.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
export type View = ReturnType<typeof renderProgress>;
|
||||
export async function visit(fn: (view: View) => Promise<void>, initial: Config = {}) {
|
||||
await page.viewport(initial.viewportWidth ?? 1100, 900);
|
||||
const view = renderProgress(initial);
|
||||
try {
|
||||
await view.ready();
|
||||
await fn(view);
|
||||
} finally {
|
||||
await view.dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue