Keep progress inside the initiating Button/IconButton or centered within a LoadingRegion without changing layout or unmounting live content. Document ownership and completion/error behavior in the registry and living catalog. Validation: qualified NET02 DG build/typecheck/registry/loading tests; existing browser geometry, theme and error lifecycle acceptance. All eleven committed files match the qualified source artifact byte for byte.
66 lines
3.7 KiB
JavaScript
66 lines
3.7 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 { ActivityIndicator, Button, IconButton, LoadingRegion } from "../packages/ui-react/dist/index.js";
|
|
|
|
test("ActivityIndicator separates decorative and announced progress", () => {
|
|
const decorative = renderToStaticMarkup(createElement(ActivityIndicator, { size: "compact" }));
|
|
const announced = renderToStaticMarkup(createElement(ActivityIndicator, { label: "Подключаем устройство" }));
|
|
|
|
assert.match(decorative, /class="nodedc-activity-indicator"/);
|
|
assert.match(decorative, /data-size="compact"/);
|
|
assert.match(decorative, /aria-hidden="true"/);
|
|
assert.doesNotMatch(decorative, /role="status"/);
|
|
|
|
assert.match(announced, /role="status"/);
|
|
assert.match(announced, /aria-label="Подключаем устройство"/);
|
|
assert.doesNotMatch(announced, /aria-hidden/);
|
|
assert.doesNotMatch(announced, /data-size=/);
|
|
});
|
|
|
|
test("ActivityIndicator is registered, cataloged and motion-safe", async () => {
|
|
const [styles, registrySource, docs, catalog] = await Promise.all([
|
|
readFile(new URL("../packages/ui-core/styles.css", import.meta.url), "utf8"),
|
|
readFile(new URL("../registry/components.json", import.meta.url), "utf8"),
|
|
readFile(new URL("../docs/COMPONENTS.md", import.meta.url), "utf8"),
|
|
readFile(new URL("../apps/catalog/src/CatalogApp.tsx", import.meta.url), "utf8"),
|
|
]);
|
|
const registry = JSON.parse(registrySource);
|
|
const entry = registry.components.find((component) => component.id === "activity-indicator");
|
|
|
|
assert.deepEqual(entry?.variants, ["default", "compact"]);
|
|
assert.ok(entry?.exports.includes("ActivityIndicator"));
|
|
assert.match(styles, /\.nodedc-activity-indicator\s*\{[\s\S]*?animation: nodedc-activity-indicator-spin/);
|
|
assert.match(styles, /\.nodedc-activity-indicator\[data-size="compact"\]/);
|
|
assert.match(styles, /@media \(prefers-reduced-motion: reduce\) \{\s*\.nodedc-activity-indicator,[\s\S]*?animation: none/);
|
|
assert.match(docs, /## ActivityIndicator/);
|
|
assert.match(catalog, /<ActivityIndicator label="Загружаем данные"/);
|
|
assert.match(catalog, /<Button loading>/);
|
|
assert.match(catalog, /<LoadingRegion loading/);
|
|
});
|
|
|
|
|
|
test("pending actions preserve their name and content while disabling only the pending control", () => {
|
|
for (const [Component, props] of [[Button, {}], [IconButton, {label: "Обновить"}]]) {
|
|
const busy = renderToStaticMarkup(createElement(Component, {...props, loading: true, disabled: false}, "Обновить"));
|
|
const idle = renderToStaticMarkup(createElement(Component, props, "Обновить"));
|
|
assert.match(busy, /disabled=""/);
|
|
assert.match(busy, /aria-busy="true"/);
|
|
assert.match(busy, /Обновить/);
|
|
assert.equal((busy.match(/class="nodedc-activity-indicator"/g) || []).length, 1);
|
|
assert.doesNotMatch(idle, /nodedc-action-loading|disabled=""/);
|
|
}
|
|
});
|
|
|
|
test("content remains mounted throughout loading and the status disappears on completion", () => {
|
|
const view = createElement("video", {"data-source": "synthetic"});
|
|
const pending = renderToStaticMarkup(createElement(LoadingRegion, {loading:true, label:"Ожидаем изображение"}, view));
|
|
const ready = renderToStaticMarkup(createElement(LoadingRegion, {loading:false, label:"Ожидаем изображение"}, view));
|
|
assert.match(pending, /<video data-source="synthetic"/);
|
|
assert.match(ready, /<video data-source="synthetic"/);
|
|
assert.equal((pending.match(/role="status"/g) || []).length, 1);
|
|
assert.doesNotMatch(ready, /role="status"|nodedc-activity-indicator/);
|
|
});
|