You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/rtl-lint.test.ts

366 lines
9.7 KiB

import { describe, expect, it } from 'vitest';
import { lintRtlGeometry, lintRtlMirror } from './rtl-lint';
describe('lintRtlGeometry — RTL-1', () => {
// The regression this guard exists for: `slider.css` before `013ceac57`.
// Measured in Chrome at the time — root/thumb/ticks at x=961, rail and fill
// at x=955, a full track-width off-axis.
const SLIDER_BEFORE = `
[data-slider][data-orientation='vertical']::before {
inset-block: 0;
inset-inline-start: 50%;
inline-size: var(--_slider-track-size);
transform: translateX(-50%);
}
`;
// The fix that landed: same logical anchor, logical offset.
const SLIDER_AFTER = `
[data-slider][data-orientation='vertical']::before {
inset-block: 0;
inset-inline-start: 50%;
inline-size: var(--_slider-track-size);
margin-inline-start: calc(var(--_slider-track-size) / -2);
}
`;
it('catches the historical slider defect', () => {
const findings = lintRtlGeometry(SLIDER_BEFORE);
expect(findings).toHaveLength(1);
expect(findings[0].line).toBe(6);
expect(findings[0].translate).toBe('transform: translateX(-50%)');
expect(findings[0].anchors).toEqual(['inset-inline-start: 50%']);
expect(findings[0].selector).toBe("[data-slider][data-orientation='vertical']::before");
});
it('passes the fix that replaced it', () => {
expect(lintRtlGeometry(SLIDER_AFTER)).toEqual([]);
});
it('does NOT flag the block axis — it never flips', () => {
const css = `
[data-slider-thumb] {
inset-block-start: 50%;
transform: translateY(-50%);
}
`;
expect(lintRtlGeometry(css)).toEqual([]);
});
it('flags the X component of a two-axis translate', () => {
const css = `
[data-dialog-content] {
inset-inline-start: 50%;
inset-block-start: 50%;
transform: translate(-50%, -50%);
}
`;
expect(lintRtlGeometry(css)).toHaveLength(1);
});
it('reads the independent `translate` property, X component first', () => {
const flagged = `
[data-menu-dial] {
inset-inline-start: 50%;
translate: -50% 0;
}
`;
const blockAxisOnly = `
[data-carousel-trigger] {
inset-inline-start: 50%;
translate: 0 -50%;
}
`;
expect(lintRtlGeometry(flagged)).toHaveLength(1);
expect(lintRtlGeometry(blockAxisOnly)).toEqual([]);
expect(lintRtlGeometry('[data-x] { inset-inline-start: 0; translate: none; }')).toEqual([]);
});
it('treats a var() X as displacing — the sign is unknown at author time', () => {
// `sliding-indicator.css`: soma writes the offset rect into the HOST's
// namespace and the shared file reads it through the `--_indicator-*`
// alias each consuming recipe declares (firma §14, 2026-08-26).
const css = `
[data-sliding-indicator] {
inset-inline-start: 0;
translate: var(--_indicator-x, 0px) var(--_indicator-y, 0px);
}
`;
const findings = lintRtlGeometry(css);
expect(findings).toHaveLength(1);
// The comma inside var() must not split the X component off.
expect(findings[0].translate).toBe(
'translate: var(--_indicator-x, 0px) var(--_indicator-y, 0px)'
);
});
it('ignores a zero X displacement', () => {
const css = `
[data-x] {
margin-inline-start: 4px;
transform: translateX(0) translateY(-50%);
}
`;
expect(lintRtlGeometry(css)).toEqual([]);
});
it('ignores padding-inline — it cannot fight a translate', () => {
const css = `
[data-chat-log-pill] {
padding-inline: var(--chat-log-pill-padding-inline);
transform: translateX(-50%);
}
`;
expect(lintRtlGeometry(css)).toEqual([]);
});
it('flags margin-inline anchors too', () => {
const css = `
[data-switch-thumb] {
margin-inline-start: var(--switch-thumb-inset);
transform: translateX(var(--_switch-thumb-offset));
}
`;
expect(lintRtlGeometry(css)).toHaveLength(1);
});
it('judges each nested block on its own declarations', () => {
// The anchor is the parent's; the child rule may match another element.
const css = `
[data-parent] {
inset-inline-start: 50%;
& [data-child] {
transform: translateX(-50%);
}
}
`;
expect(lintRtlGeometry(css)).toEqual([]);
});
it('sees inside at-rules', () => {
const css = `
@media (min-width: 40rem) {
[data-x] {
inset-inline-start: 50%;
transform: translateX(-50%);
}
}
`;
expect(lintRtlGeometry(css)).toHaveLength(1);
});
it('honours the rtl-physical escape hatch, same line or the line above', () => {
const above = `
[data-cropper-handle][data-corner='nw'] {
inset-inline-start: 0;
/* rtl-physical: compass corner of an image that does not flip */
transform: translate(-50%, -50%);
}
`;
const sameLine = `
[data-x] {
inset-inline-start: 0;
transform: translateX(-50%); /* rtl-physical: deliberate */
}
`;
expect(lintRtlGeometry(above)).toEqual([]);
expect(lintRtlGeometry(sameLine)).toEqual([]);
});
it('ignores custom-property declarations — the consumer gets judged', () => {
const css = `
[data-x] {
inset-inline-start: 50%;
--x-offset: translateX(-50%);
}
`;
expect(lintRtlGeometry(css)).toEqual([]);
});
it('does not read commented-out code', () => {
const css = `
[data-x] {
inset-inline-start: 50%;
/* transform: translateX(-50%); */
margin-inline-start: calc(var(--w) / -2);
}
`;
expect(lintRtlGeometry(css)).toEqual([]);
});
});
describe('lintRtlMirror — RTL-2', () => {
// The two regressions this guard exists for, verbatim from `dee2c9e3a^`.
// Both shipped: the migration of §6.3 swapped `[dir='rtl']` for `:dir(rtl)`
// without asking whether the BODY was right, so a mechanical rewrite carried
// the defect across untouched.
const FEED_BEFORE = `
[data-feed-thread] {
padding-inline-start: var(--_feed-thread-indent);
border-inline-start: var(--border-width-medium) solid var(--_feed-palette-element);
}
[data-feed-root]:dir(rtl) [data-feed-thread] {
border-inline-start: 0;
border-inline-end: var(--border-width-medium) solid var(--_feed-palette-element);
}
`;
const TREE_VIEW_BEFORE = `
[data-tree-view-root][data-indent-guides]:dir(rtl) [data-tree-view-branch]::before {
inset-inline-start: auto;
inset-inline-end: calc(var(--tree-depth, 0) * var(--_indent));
}
`;
it('catches the feed rail, which left the indent on the other edge', () => {
const findings = lintRtlMirror(FEED_BEFORE);
expect(findings).toHaveLength(1);
expect(findings[0].line).toBe(9);
expect(findings[0].family).toBe('border-inline');
expect(findings[0].neutral).toBe('border-inline-start: 0');
expect(findings[0].selector).toBe('[data-feed-root]:dir(rtl) [data-feed-thread]');
});
it('catches the tree-view indent guide, released with `auto`', () => {
const findings = lintRtlMirror(TREE_VIEW_BEFORE);
expect(findings).toHaveLength(1);
expect(findings[0].family).toBe('inset-inline');
expect(findings[0].neutral).toBe('inset-inline-start: auto');
});
it('collapses a multi-line value — the real tree-view calc() spans four lines', () => {
const css = `
[data-x]:dir(rtl) {
inset-inline-start: auto;
inset-inline-end: calc(
var(--depth, 0) * var(--indent) +
var(--indent) / 2
);
}
`;
const findings = lintRtlMirror(css);
expect(findings).toHaveLength(1);
expect(findings[0].payload).toBe(
'inset-inline-end: calc( var(--depth, 0) * var(--indent) + var(--indent) / 2 )'
);
});
it('passes the fix — deleting the rule, since the base already mirrored', () => {
const css = `
[data-feed-thread] {
padding-inline-start: var(--_feed-thread-indent);
border-inline-start: var(--border-width-medium) solid var(--_feed-palette-element);
}
`;
expect(lintRtlMirror(css)).toEqual([]);
});
it('does NOT flag a value changed without being relocated', () => {
// Asymmetric by design is real; what betrays the double flip is the PAIR.
const css = `
[data-x]:dir(rtl) {
padding-inline-start: 0;
}
`;
expect(lintRtlMirror(css)).toEqual([]);
});
it('does NOT flag two faces that both carry a value', () => {
// An author describing two different edges, not cancelling a mirror.
const css = `
[data-x]:dir(rtl) {
margin-inline-start: var(--a);
margin-inline-end: var(--b);
}
`;
expect(lintRtlMirror(css)).toEqual([]);
});
it('does NOT flag two faces that are both off — that is a reset', () => {
const css = `
[data-x]:dir(rtl) {
inset-inline-start: auto;
inset-inline-end: auto;
}
`;
expect(lintRtlMirror(css)).toEqual([]);
});
it('does NOT flag the pair outside a :dir() block', () => {
// Without the direction branch there is no mirror to cancel.
const css = `
[data-x] {
border-inline-start: 0;
border-inline-end: 1px solid red;
}
`;
expect(lintRtlMirror(css)).toEqual([]);
});
it("judges a child on its OWN prelude, not the parent's :dir()", () => {
// The same call RTL-1 makes about anchors.
const css = `
[data-x]:dir(rtl) {
color: red;
& [data-y] {
border-inline-start: 0;
border-inline-end: 1px solid red;
}
}
`;
expect(lintRtlMirror(css)).toEqual([]);
});
it('flags :dir(ltr) too — the shape is just as suspect', () => {
const css = `
[data-x]:dir(ltr) {
margin-inline-end: 0;
margin-inline-start: var(--gap);
}
`;
expect(lintRtlMirror(css)).toHaveLength(1);
});
it('honours the rtl-mirror escape hatch, same line or the line above', () => {
const sameLine = `
[data-x]:dir(rtl) {
border-inline-start: 0;
border-inline-end: 1px solid red; /* rtl-mirror: the design breaks the mirror here */
}
`;
const lineAbove = `
[data-x]:dir(rtl) {
border-inline-start: 0;
/* rtl-mirror: the design breaks the mirror here */
border-inline-end: 1px solid red;
}
`;
expect(lintRtlMirror(sameLine)).toEqual([]);
expect(lintRtlMirror(lineAbove)).toEqual([]);
});
it("does not accept RTL-1's marker — it would declare the wrong thing", () => {
const css = `
[data-x]:dir(rtl) {
border-inline-start: 0;
border-inline-end: 1px solid red; /* rtl-physical: wrong marker */
}
`;
expect(lintRtlMirror(css)).toHaveLength(1);
});
it('does not read commented-out code', () => {
const css = `
[data-x]:dir(rtl) {
border-inline-start: 0;
/* border-inline-end: 1px solid red; */
}
`;
expect(lintRtlMirror(css)).toEqual([]);
});
});

Powered by TurnKey Linux.