parent
ea6e1aef12
commit
cbb75903fe
@ -0,0 +1,171 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { page } from 'vitest/browser';
|
||||
import { visit } from '../../../test/box/box-audit-view';
|
||||
|
||||
it('native default box renders a div with the contract marker', () =>
|
||||
visit(async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.tagName).toBe('DIV');
|
||||
expect(root.getAttribute('data-box')).toBe('');
|
||||
expect(root.textContent?.trim()).toBe('Layout box');
|
||||
await view.capture('default');
|
||||
}));
|
||||
|
||||
it('native semantic element preserves the box recipe', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.node().tagName).toBe('SECTION');
|
||||
expect(view.node().getAttribute('data-box')).toBe('');
|
||||
await view.capture('section');
|
||||
},
|
||||
{ as: 'section' }
|
||||
));
|
||||
|
||||
it('native numeric width and height become CSS lengths', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.style.getPropertyValue('--box-width').trim()).toBe('120px');
|
||||
expect(root.style.getPropertyValue('--box-height').trim()).toBe('60px');
|
||||
expect(getComputedStyle(root).width).toBe('120px');
|
||||
await view.capture('lengths');
|
||||
},
|
||||
{ width: 120, height: 60 }
|
||||
));
|
||||
|
||||
it('native min and max width constrain the rendered box', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.style.getPropertyValue('--box-min-width').trim()).toBe('80px');
|
||||
expect(root.style.getPropertyValue('--box-max-width').trim()).toBe('160px');
|
||||
expect(getComputedStyle(root).width).toBe('160px');
|
||||
await view.capture('clamped');
|
||||
},
|
||||
{ width: 220, minWidth: 80, maxWidth: 160 }
|
||||
));
|
||||
|
||||
it('native padding side overrides the shared spacing', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const style = view.node().style;
|
||||
expect(style.getPropertyValue('--box-padding').trim()).toBe('var(--space-4)');
|
||||
expect(style.getPropertyValue('--box-padding-top').trim()).toBe('var(--space-6)');
|
||||
expect(getComputedStyle(view.node()).paddingTop).not.toBe(
|
||||
getComputedStyle(view.node()).paddingBottom
|
||||
);
|
||||
await view.capture('padding');
|
||||
},
|
||||
{ padding: 4, paddingTop: 6, width: 180 }
|
||||
));
|
||||
|
||||
it('native inline margin uses the spacing token path', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(view.node().style.getPropertyValue('--box-margin-inline').trim()).toBe(
|
||||
'var(--space-3)'
|
||||
);
|
||||
await view.capture('margin');
|
||||
},
|
||||
{ marginX: 3, width: 180 }
|
||||
));
|
||||
|
||||
it('native display and overflow axes reach the recipe', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(getComputedStyle(view.node()).display).toBe('flex');
|
||||
expect(getComputedStyle(view.node()).overflowX).toBe('auto');
|
||||
await view.capture('display');
|
||||
},
|
||||
{ display: 'flex', overflowX: 'auto', width: 180 }
|
||||
));
|
||||
|
||||
it('native LTR left and top position from the physical edges', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node().getBoundingClientRect();
|
||||
const stage = view.stage().getBoundingClientRect();
|
||||
expect(root.left - stage.left).toBe(10);
|
||||
expect(root.top - stage.top).toBe(5);
|
||||
await view.capture('position-ltr');
|
||||
},
|
||||
{ position: 'absolute', left: 10, top: 5, width: 20, height: 20, dir: 'ltr' }
|
||||
));
|
||||
|
||||
it('native flex shorthand expands and explicit grow wins', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const style = view.node().style;
|
||||
expect(style.getPropertyValue('--box-grow').trim()).toBe('3');
|
||||
expect(style.getPropertyValue('--box-shrink').trim()).toBe('1');
|
||||
expect(style.getPropertyValue('--box-basis').trim()).toBe('0%');
|
||||
await view.capture('flex');
|
||||
},
|
||||
{ flex: 2, grow: 3 }
|
||||
));
|
||||
|
||||
it('native grid area stamps its guard and grid item variables', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.getAttribute('data-box-area')).toBe('');
|
||||
expect(root.style.getPropertyValue('--box-grid-area').trim()).toBe('1 / 2 / 3 / 4');
|
||||
expect(root.style.getPropertyValue('--box-grid-column').trim()).toBe('2 / 4');
|
||||
await view.capture('grid');
|
||||
},
|
||||
{ gridArea: '1 / 2 / 3 / 4', gridColumn: '2 / 4' }
|
||||
));
|
||||
|
||||
it('native consumer style follows internal declarations', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.style.getPropertyValue('--box-width').trim()).toBe('80px');
|
||||
expect(getComputedStyle(root).width).toBe('80px');
|
||||
await view.capture('consumer-style');
|
||||
},
|
||||
{ width: 120, style: '--box-width:80px;color:rgb(20, 40, 60)' }
|
||||
));
|
||||
|
||||
it('native nested Box does not inherit the parent padding axis', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const inner = view.inner()!;
|
||||
expect(inner.getAttribute('data-box')).toBe('');
|
||||
expect(inner.style.getPropertyValue('--box-padding')).toBe('');
|
||||
expect(getComputedStyle(inner).paddingTop).toBe('0px');
|
||||
await view.capture('nested');
|
||||
},
|
||||
{ nested: true, padding: 4, width: 220 }
|
||||
));
|
||||
|
||||
it('native global attributes and class reach the chosen element', () =>
|
||||
visit(async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.classList.contains('audit-box')).toBe(true);
|
||||
expect(root.getAttribute('data-probe')).toBe('box');
|
||||
expect(root.getAttribute('title')).toBe('Audit Box');
|
||||
}));
|
||||
|
||||
it('native reactive text and width update the same box', () =>
|
||||
visit(async (view) => {
|
||||
const original = view.node();
|
||||
view.controls().text('Changed box');
|
||||
view.controls().width(210);
|
||||
await view.ready();
|
||||
expect(view.node()).toBe(original);
|
||||
expect(view.node().textContent?.trim()).toBe('Changed box');
|
||||
expect(getComputedStyle(view.node()).width).toBe('210px');
|
||||
}));
|
||||
|
||||
it('native 350px box remains inside its viewport', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
await page.viewport(350, 900);
|
||||
await view.ready();
|
||||
const rect = view.node().getBoundingClientRect();
|
||||
expect(rect.left >= 0 && rect.right <= 350).toBe(true);
|
||||
await view.capture('350');
|
||||
},
|
||||
{ width: 180, stageWidth: 300 }
|
||||
));
|
||||
@ -0,0 +1,108 @@
|
||||
<script module lang="ts">
|
||||
import type { BoxProps } from '$uix/eidos/components/box';
|
||||
export type Config = {
|
||||
text?: string;
|
||||
as?: BoxProps['as'];
|
||||
width?: BoxProps['width'];
|
||||
minWidth?: BoxProps['minWidth'];
|
||||
maxWidth?: BoxProps['maxWidth'];
|
||||
height?: BoxProps['height'];
|
||||
padding?: BoxProps['padding'];
|
||||
paddingTop?: BoxProps['paddingTop'];
|
||||
marginX?: BoxProps['marginX'];
|
||||
display?: BoxProps['display'];
|
||||
gap?: BoxProps['gap'];
|
||||
position?: BoxProps['position'];
|
||||
left?: BoxProps['left'];
|
||||
right?: BoxProps['right'];
|
||||
top?: BoxProps['top'];
|
||||
overflowX?: BoxProps['overflowX'];
|
||||
flex?: BoxProps['flex'];
|
||||
grow?: BoxProps['grow'];
|
||||
gridArea?: BoxProps['gridArea'];
|
||||
gridColumn?: BoxProps['gridColumn'];
|
||||
style?: BoxProps['style'];
|
||||
nested?: boolean;
|
||||
dir?: 'ltr' | 'rtl';
|
||||
stageWidth?: number;
|
||||
};
|
||||
export type Controls = {
|
||||
text: (next: string) => void;
|
||||
width: (next: BoxProps['width']) => void;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { setActiveUix, type ActiveUix } from '$active-uix';
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import Box from '$uix/eidos/components/box';
|
||||
let {
|
||||
uix,
|
||||
initial = {},
|
||||
onControls
|
||||
}: {
|
||||
uix: ActiveUix;
|
||||
initial?: Config;
|
||||
onControls: (controls: Controls) => void;
|
||||
} = $props();
|
||||
// svelte-ignore state_referenced_locally
|
||||
setActiveUix(uix);
|
||||
const eidos = ActiveEidos.create({ applyDom: false });
|
||||
onDestroy(() => eidos.dispose());
|
||||
// svelte-ignore state_referenced_locally
|
||||
let text = $state(initial.text ?? 'Layout box');
|
||||
// svelte-ignore state_referenced_locally
|
||||
let width = $state(initial.width);
|
||||
onMount(() => onControls({ text: (next) => (text = next), width: (next) => (width = next) }));
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="audit-stage"
|
||||
data-audit-stage
|
||||
dir={initial.dir}
|
||||
style={`position:relative;width:${initial.stageWidth ?? 480}px;min-height:160px;padding:24px;font-family:Arial,sans-serif`}
|
||||
>
|
||||
<Box
|
||||
id="audit-box"
|
||||
class="audit-box"
|
||||
data-probe="box"
|
||||
title="Audit Box"
|
||||
as={initial.as}
|
||||
{width}
|
||||
minWidth={initial.minWidth}
|
||||
maxWidth={initial.maxWidth}
|
||||
height={initial.height}
|
||||
padding={initial.padding}
|
||||
paddingTop={initial.paddingTop}
|
||||
marginX={initial.marginX}
|
||||
display={initial.display}
|
||||
gap={initial.gap}
|
||||
position={initial.position}
|
||||
left={initial.left}
|
||||
right={initial.right}
|
||||
top={initial.top}
|
||||
overflowX={initial.overflowX}
|
||||
flex={initial.flex}
|
||||
grow={initial.grow}
|
||||
gridArea={initial.gridArea}
|
||||
gridColumn={initial.gridColumn}
|
||||
style={initial.style}
|
||||
>
|
||||
{text}
|
||||
{#if initial.nested}
|
||||
<Box id="audit-inner" class="audit-inner">Inner box</Box>
|
||||
{/if}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.audit-box) {
|
||||
background: rgb(232, 240, 252);
|
||||
outline: 1px solid rgb(66, 90, 128);
|
||||
}
|
||||
:global(.audit-inner) {
|
||||
background: rgb(255, 250, 220);
|
||||
outline: 1px solid rgb(160, 130, 70);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,58 @@
|
||||
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 './box-audit-harness.svelte';
|
||||
|
||||
export function renderBox(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 node = () => target.querySelector<HTMLElement>('#audit-box')!;
|
||||
const stage = () => target.querySelector<HTMLElement>('#audit-stage')!;
|
||||
const inner = () => target.querySelector<HTMLElement>('#audit-inner');
|
||||
const ready = async () =>
|
||||
new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
||||
);
|
||||
return {
|
||||
target,
|
||||
node,
|
||||
stage,
|
||||
inner,
|
||||
controls: () => controls,
|
||||
ready,
|
||||
async capture(name: string) {
|
||||
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
|
||||
await ready();
|
||||
await page.screenshot({ path: 'tmp/box-' + name + '.png' });
|
||||
}
|
||||
},
|
||||
async dispose() {
|
||||
await unmount(component);
|
||||
uix.dispose();
|
||||
target.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
export type View = ReturnType<typeof renderBox>;
|
||||
export async function visit(fn: (view: View) => Promise<void>, initial: Config = {}) {
|
||||
await page.viewport(1100, 900);
|
||||
const view = renderBox(initial);
|
||||
try {
|
||||
await view.ready();
|
||||
await fn(view);
|
||||
} finally {
|
||||
await view.dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue