105 lines
6.8 KiB
JavaScript
105 lines
6.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import test from "node:test";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { RangeControl } from "../packages/ui-react/dist/index.js";
|
|
|
|
test("RangeControl keeps native drag semantics and exposes one persistent exact-value editor", () => {
|
|
const markup = renderToStaticMarkup(createElement(RangeControl, {
|
|
label: "Размер головки",
|
|
value: 20,
|
|
min: 1,
|
|
max: 32,
|
|
step: 1,
|
|
formatValue: (value) => `${value} px`,
|
|
onChange: () => {},
|
|
}));
|
|
|
|
assert.match(markup, /type="range"/);
|
|
assert.match(markup, /aria-valuetext="20 px"/);
|
|
assert.match(markup, /class="nodedc-range__editor"/);
|
|
assert.match(markup, /aria-label="Размер головки: точное значение"/);
|
|
assert.doesNotMatch(markup, /nodedc-range__value-hit/);
|
|
});
|
|
|
|
test("vertical scale keeps endpoints outside and the label/value centered at every fill level", () => {
|
|
for (const [value, progress] of [[-1.2, 0], [25.15, 50], [51.5, 100]]) {
|
|
const markup = renderToStaticMarkup(createElement(RangeControl, {
|
|
label: "Срез", orientation: "vertical", value, min: -1.2, max: 51.5, step: "any",
|
|
formatValue: (n) => `${n.toFixed(1).replace('.', ',')} м`,
|
|
formatLimit: (n) => n.toFixed(1).replace('.', ','), onChange: () => {},
|
|
}));
|
|
const renderedProgress = Number(markup.match(/--nodedc-range-progress:([\d.]+)%/)?.[1]);
|
|
assert.ok(Math.abs(renderedProgress - progress) < 1e-9);
|
|
assert.match(markup, /aria-label="Срез" aria-orientation="vertical"/);
|
|
assert.match(markup, /min="-1.2" max="51.5" step="any"/);
|
|
assert.equal((markup.match(/nodedc-range-scale__limit--max">51,5<\/span>/g) ?? []).length, 1);
|
|
assert.equal((markup.match(/nodedc-range-scale__limit--min">-1,2<\/span>/g) ?? []).length, 1);
|
|
assert.match(markup, /<\/span><\/span><\/div><span class="nodedc-range-scale__limit/);
|
|
assert.equal((markup.match(/nodedc-range__label">Срез · /g) ?? []).length, 2);
|
|
assert.match(markup, /nodedc-range__fill-text" aria-hidden="true"/);
|
|
assert.doesNotMatch(markup, /nodedc-range__value|nodedc-range__editor|formatLimit=/);
|
|
}
|
|
});
|
|
|
|
test("vertical scale mirrors only endpoint labels for the opposite scene edge", () => {
|
|
const markup = renderToStaticMarkup(createElement(RangeControl, {
|
|
label: "Срез", orientation: "vertical", limitSide: "left", value: 25.15, min: -1.2, max: 51.5,
|
|
step: "any", formatLimit: (n) => n.toFixed(1).replace('.', ','), onChange: () => {},
|
|
}));
|
|
assert.match(markup, /class="nodedc-range-scale" data-limit-side="left"/);
|
|
assert.match(markup, /nodedc-range-scale__limit--max">51,5<\/span>/);
|
|
assert.match(markup, /nodedc-range-scale__limit--min">-1,2<\/span>/);
|
|
});
|
|
|
|
test("vertical contrast clips both glyph layers and visible focus has no perimeter", async () => {
|
|
const styles = await readFile(new URL("../packages/ui-core/styles.css", import.meta.url), "utf8");
|
|
assert.match(styles, /\.nodedc-range\[data-orientation="vertical"\] \.nodedc-range__fill-text \{ clip-path: inset\(calc\(100% - var\(--nodedc-range-progress\)\) 0 0 round var\(--nodedc-radius-circle\)\)/);
|
|
assert.match(styles, /\.nodedc-range-scale \{[^}]*width: calc\(var\(--nodedc-control-height\) \* 0\.4\)/);
|
|
assert.match(styles, /\.nodedc-range-scale__limit \{[^}]*left: calc\(100% \+ 0\.4rem\);[^}]*color: var\(--nodedc-text-primary\);[^}]*font-size: calc\(var\(--nodedc-font-size-xs\) \* 0\.9\)/);
|
|
assert.match(styles, /\.nodedc-range-scale\[data-limit-side="left"\] \.nodedc-range-scale__limit \{[^}]*right: calc\(100% \+ 0\.4rem\);[^}]*left: auto;[^}]*text-align: right/);
|
|
assert.match(styles, /\.nodedc-range\[data-orientation="vertical"\] \.nodedc-range__label \{[^}]*font-size: calc\(var\(--nodedc-font-size-sm\) \* 0\.9\)/);
|
|
assert.match(styles, /\.nodedc-range\[data-orientation="vertical"\] \{[^}]*border: 0; outline: 0; box-shadow: none;/);
|
|
assert.match(styles, /\.nodedc-range\[data-orientation="vertical"\]:has\(input:focus-visible\) \.nodedc-range__label \{ text-decoration: underline; text-decoration-thickness: 2px;/);
|
|
assert.doesNotMatch(styles, /\.nodedc-range\[data-orientation="vertical"\]:has\(input:focus-visible\) \{/);
|
|
assert.doesNotMatch(styles, /\.nodedc-range\[data-orientation="vertical"\]:focus-within/);
|
|
});
|
|
|
|
test("exact editing is a stable native caret field without remount, forced selection or a dark editor box", async () => {
|
|
const [component, styles, windowComponent] = await Promise.all([
|
|
readFile(new URL("../packages/ui-react/src/RangeControl.tsx", import.meta.url), "utf8"),
|
|
readFile(new URL("../packages/ui-core/styles.css", import.meta.url), "utf8"),
|
|
readFile(new URL("../packages/ui-react/src/Window.tsx", import.meta.url), "utf8"),
|
|
]);
|
|
|
|
assert.match(component, /exactValueBounds\?: \{/);
|
|
assert.match(component, /exactValueBounds\?\.min \?\? Number\.NEGATIVE_INFINITY/);
|
|
assert.match(component, /exactValueBounds\?\.max \?\? Number\.POSITIVE_INFINITY/);
|
|
assert.match(component, /normalizeEditedRangeValue\(parsed, min, max, step, exactValueBounds\)/);
|
|
assert.match(component, /value=\{draft\}/);
|
|
assert.match(component, /data-contrast=\{editorContrast\}/);
|
|
assert.match(component, /fillRight >= textRight \? "fill" : "base"/);
|
|
assert.match(component, /new ResizeObserver\(updateContrast\)/);
|
|
const editorSource = component.match(/className="nodedc-range__editor"[\s\S]*?onBlur=\{\(\) => \{[\s\S]*?\n \}\}\n \/>/)?.[0] ?? "";
|
|
assert.notEqual(editorSource, "");
|
|
assert.doesNotMatch(editorSource, /onPointerDown=/);
|
|
assert.match(component, /event\.key === "Enter"/);
|
|
assert.match(component, /event\.key === "Escape"/);
|
|
assert.match(component, /event\.stopPropagation\(\)/);
|
|
assert.match(component, /cancelNextBlurRef/);
|
|
assert.match(component, /event\.currentTarget\.blur\(\)/);
|
|
assert.match(component, /onBlur=\{\(\) => \{/);
|
|
assert.doesNotMatch(component, /\.select\(\)/);
|
|
assert.doesNotMatch(component, /setSelectionRange/);
|
|
assert.doesNotMatch(component, /\{editing \? \(\s*<input/);
|
|
assert.match(styles, /\.nodedc-range input\[type="range"\][\s\S]*?z-index: 4/);
|
|
assert.match(styles, /\.nodedc-range__editor[\s\S]*?z-index: 5[\s\S]*?background: transparent[\s\S]*?color: transparent[\s\S]*?caret-color: transparent[\s\S]*?box-shadow: none/);
|
|
assert.match(styles, /\.nodedc-range__editor\[data-active\][\s\S]*?caret-color: currentColor/);
|
|
assert.match(styles, /\.nodedc-range__editor\[data-active\]\[data-contrast="fill"\][\s\S]*?rgb\(var\(--nodedc-on-accent-rgb\)\)/);
|
|
assert.match(windowComponent, /const onCloseRef = useRef\(onClose\)/);
|
|
assert.match(windowComponent, /onCloseRef\.current = onClose/);
|
|
assert.match(windowComponent, /onCloseRef\.current\(\)/);
|
|
assert.doesNotMatch(windowComponent, /\[closeOnEscape, onClose, open, shouldLockBodyScroll, shouldTrapFocus\]/);
|
|
});
|