parent
7e5e47a134
commit
6cfe429336
@ -0,0 +1,195 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { page } from 'vitest/browser';
|
||||
import { visit } from '../../../test/container/container-audit-view';
|
||||
|
||||
it('native default container is one Box root with xl measure and padding', () =>
|
||||
visit(async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.tagName).toBe('DIV');
|
||||
expect(root.hasAttribute('data-box')).toBe(true);
|
||||
expect(root.hasAttribute('data-container')).toBe(true);
|
||||
expect(root.getAttribute('data-size')).toBe('xl');
|
||||
expect(root.getAttribute('data-align')).toBe('center');
|
||||
expect(getComputedStyle(root).width).toBe('1000px');
|
||||
expect(getComputedStyle(root).maxWidth).toBe('1280px');
|
||||
expect(getComputedStyle(root).paddingLeft).not.toBe('0px');
|
||||
await view.capture('default');
|
||||
}));
|
||||
|
||||
it('native sm size caps a wide parent at 480px', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.getAttribute('data-size')).toBe('sm');
|
||||
expect(getComputedStyle(root).maxWidth).toBe('480px');
|
||||
expect(getComputedStyle(root).width).toBe('480px');
|
||||
await view.capture('sm');
|
||||
},
|
||||
{ size: 'sm' }
|
||||
));
|
||||
|
||||
it('native md size caps a wide parent at 768px', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(getComputedStyle(view.node()).maxWidth).toBe('768px');
|
||||
await view.capture('md');
|
||||
},
|
||||
{ size: 'md' }
|
||||
));
|
||||
|
||||
it('native full size removes the cap but preserves default padding', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(getComputedStyle(root).maxWidth).toBe('none');
|
||||
expect(getComputedStyle(root).width).toBe('1000px');
|
||||
expect(getComputedStyle(root).paddingLeft).not.toBe('0px');
|
||||
await view.capture('full');
|
||||
},
|
||||
{ size: 'full' }
|
||||
));
|
||||
|
||||
it('native paddingX zero removes the default gutter', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
expect(getComputedStyle(view.node()).paddingLeft).toBe('0px');
|
||||
expect(getComputedStyle(view.node()).paddingRight).toBe('0px');
|
||||
await view.capture('padding-zero');
|
||||
},
|
||||
{ paddingX: 0, size: 'sm' }
|
||||
));
|
||||
|
||||
it('native semantic section retains Box and Container markers', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.tagName).toBe('SECTION');
|
||||
expect(root.hasAttribute('data-box')).toBe(true);
|
||||
expect(root.hasAttribute('data-container')).toBe(true);
|
||||
await view.capture('section');
|
||||
},
|
||||
{ as: 'section', size: 'sm' }
|
||||
));
|
||||
|
||||
it('native left alignment starts at the parent content edge', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node().getBoundingClientRect();
|
||||
const stage = view.stage().getBoundingClientRect();
|
||||
expect(root.left - stage.left).toBe(24);
|
||||
await view.capture('align-left');
|
||||
},
|
||||
{ size: 'sm', align: 'left' }
|
||||
));
|
||||
|
||||
it('native center alignment balances the side margins', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node().getBoundingClientRect();
|
||||
const stage = view.stage().getBoundingClientRect();
|
||||
expect(root.left - stage.left).toBe(284);
|
||||
expect(stage.right - root.right).toBe(284);
|
||||
await view.capture('align-center');
|
||||
},
|
||||
{ size: 'sm', align: 'center' }
|
||||
));
|
||||
|
||||
it('native right alignment ends at the parent content edge', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node().getBoundingClientRect();
|
||||
const stage = view.stage().getBoundingClientRect();
|
||||
expect(stage.right - root.right).toBe(24);
|
||||
await view.capture('align-right');
|
||||
},
|
||||
{ size: 'sm', align: 'right' }
|
||||
));
|
||||
|
||||
it('native Container is an inline-size query container', () =>
|
||||
visit(async (view) => {
|
||||
expect(getComputedStyle(view.node()).containerType).toBe('inline-size');
|
||||
}));
|
||||
|
||||
it('native consumer style can override the Box max-width variable', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(getComputedStyle(root).maxWidth).toBe('320px');
|
||||
expect(getComputedStyle(root).width).toBe('320px');
|
||||
await view.capture('consumer-style');
|
||||
},
|
||||
{ size: 'sm', style: '--box-max-width:320px;color:rgb(20, 40, 60)' }
|
||||
));
|
||||
|
||||
it('native inherited Box width and maxWidth props are shadowed by size', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.style.getPropertyValue('--box-width').trim()).toBe('100%');
|
||||
expect(root.style.getPropertyValue('--box-max-width').trim()).toBe(
|
||||
'var(--container-width-sm)'
|
||||
);
|
||||
expect(getComputedStyle(root).width).toBe('480px');
|
||||
await view.capture('shadowed-width');
|
||||
},
|
||||
{ size: 'sm', width: 200, maxWidth: 320 }
|
||||
));
|
||||
|
||||
it('native inherited Box margins are shadowed by alignment', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.style.getPropertyValue('--box-margin-left').trim()).toBe('auto');
|
||||
expect(root.style.getPropertyValue('--box-margin-inline').trim()).toBe('var(--space-0)');
|
||||
expect(root.getBoundingClientRect().left - view.stage().getBoundingClientRect().left).toBe(
|
||||
284
|
||||
);
|
||||
await view.capture('shadowed-margin');
|
||||
},
|
||||
{ size: 'sm', align: 'center', marginLeft: 0, marginX: 0 }
|
||||
));
|
||||
|
||||
it('native nested Container writes its own width cap', () =>
|
||||
visit(
|
||||
async (view) => {
|
||||
const inner = view.inner()!;
|
||||
expect(inner.hasAttribute('data-container')).toBe(true);
|
||||
expect(inner.getAttribute('data-size')).toBe('sm');
|
||||
expect(getComputedStyle(inner).maxWidth).toBe('480px');
|
||||
await view.capture('nested');
|
||||
},
|
||||
{ size: 'md', nested: true }
|
||||
));
|
||||
|
||||
it('native global attributes and class reach the root', () =>
|
||||
visit(async (view) => {
|
||||
const root = view.node();
|
||||
expect(root.classList.contains('audit-container')).toBe(true);
|
||||
expect(root.getAttribute('data-probe')).toBe('container');
|
||||
expect(root.getAttribute('title')).toBe('Audit Container');
|
||||
}));
|
||||
|
||||
it('native reactive size and align update the same root', () =>
|
||||
visit(async (view) => {
|
||||
const root = view.node();
|
||||
view.controls().size('sm');
|
||||
view.controls().align('left');
|
||||
await view.ready();
|
||||
expect(view.node()).toBe(root);
|
||||
expect(root.getAttribute('data-size')).toBe('sm');
|
||||
expect(root.getAttribute('data-align')).toBe('left');
|
||||
expect(getComputedStyle(root).width).toBe('480px');
|
||||
await view.capture('reactive');
|
||||
}));
|
||||
|
||||
it('native 350px Container stays inside the 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');
|
||||
},
|
||||
{ stageWidth: 300 }
|
||||
));
|
||||
@ -0,0 +1,88 @@
|
||||
<script module lang="ts">
|
||||
import type { ContainerProps } from '$uix/eidos/components/container';
|
||||
export type Config = {
|
||||
as?: ContainerProps['as'];
|
||||
size?: ContainerProps['size'];
|
||||
align?: ContainerProps['align'];
|
||||
paddingX?: ContainerProps['paddingX'];
|
||||
width?: ContainerProps['width'];
|
||||
maxWidth?: ContainerProps['maxWidth'];
|
||||
marginLeft?: ContainerProps['marginLeft'];
|
||||
marginX?: ContainerProps['marginX'];
|
||||
style?: ContainerProps['style'];
|
||||
nested?: boolean;
|
||||
stageWidth?: number;
|
||||
};
|
||||
export type Controls = {
|
||||
size: (next: ContainerProps['size']) => void;
|
||||
align: (next: ContainerProps['align']) => void;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { setActiveUix, type ActiveUix } from '$active-uix';
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import Container from '$uix/eidos/components/container';
|
||||
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 size = $state(initial.size);
|
||||
// svelte-ignore state_referenced_locally
|
||||
let align = $state(initial.align);
|
||||
onMount(() => onControls({ size: (next) => (size = next), align: (next) => (align = next) }));
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="audit-stage"
|
||||
data-audit-stage
|
||||
style={`position:relative;width:${initial.stageWidth ?? 1000}px;min-height:160px;padding:24px;font-family:Arial,sans-serif`}
|
||||
>
|
||||
<Container
|
||||
id="audit-container"
|
||||
class="audit-container"
|
||||
data-probe="container"
|
||||
title="Audit Container"
|
||||
as={initial.as}
|
||||
{size}
|
||||
{align}
|
||||
paddingX={initial.paddingX}
|
||||
width={initial.width}
|
||||
maxWidth={initial.maxWidth}
|
||||
marginLeft={initial.marginLeft}
|
||||
marginX={initial.marginX}
|
||||
style={initial.style}
|
||||
>
|
||||
<div class="audit-content">Content measure</div>
|
||||
{#if initial.nested}
|
||||
<Container id="audit-inner" class="audit-inner" size="sm">Inner measure</Container>
|
||||
{/if}
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.audit-container) {
|
||||
background: rgb(232, 240, 252);
|
||||
outline: 1px solid rgb(66, 90, 128);
|
||||
}
|
||||
:global(.audit-content) {
|
||||
background: rgb(107, 156, 206);
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
:global(.audit-inner) {
|
||||
background: rgb(255, 250, 220);
|
||||
outline: 1px solid rgb(160, 130, 70);
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,60 @@
|
||||
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 './container-audit-harness.svelte';
|
||||
|
||||
export function renderContainer(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-container')!;
|
||||
const stage = () => target.querySelector<HTMLElement>('#audit-stage')!;
|
||||
const inner = () => target.querySelector<HTMLElement>('#audit-inner');
|
||||
const content = () => target.querySelector<HTMLElement>('.audit-content')!;
|
||||
const ready = async () =>
|
||||
new Promise<void>((resolve) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
|
||||
);
|
||||
return {
|
||||
target,
|
||||
node,
|
||||
stage,
|
||||
inner,
|
||||
content,
|
||||
controls: () => controls,
|
||||
ready,
|
||||
async capture(name: string) {
|
||||
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
|
||||
await ready();
|
||||
await page.screenshot({ path: 'tmp/container-' + name + '.png' });
|
||||
}
|
||||
},
|
||||
async dispose() {
|
||||
await unmount(component);
|
||||
uix.dispose();
|
||||
target.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
export type View = ReturnType<typeof renderContainer>;
|
||||
export async function visit(fn: (view: View) => Promise<void>, initial: Config = {}) {
|
||||
await page.viewport(1100, 900);
|
||||
const view = renderContainer(initial);
|
||||
try {
|
||||
await view.ready();
|
||||
await fn(view);
|
||||
} finally {
|
||||
await view.dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue