@obinexusltd/obix-component-loading 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OBINexus Computing — Nnamdi Michael Okpala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @obinexusltd/obix-component-loading
2
+
3
+ **The `ObixLoading` feedback component** — a `role="status"` spinner with an accessible label and an optional full-area `overlay`; starts hidden.
4
+
5
+ Split out of `@obinexusltd/obix-component-feedback` as an independent package.
6
+
7
+ ```bash
8
+ npm install @obinexusltd/obix-component-loading
9
+ ```
10
+
11
+ > **Zero dependencies.** Data-Oriented: `createX(config)` returns
12
+ > `{ name, state, actions, render }`. Actions are pure
13
+ > `(state, …args) => newState`; `render(state)` is deterministic, HTML-escaped
14
+ > markup (an empty string while hidden). `renderX(config, overrides?)` renders
15
+ > in one call.
16
+
17
+ ## API
18
+
19
+ ```ts
20
+ import { createLoading, renderLoading } from "@obinexusltd/obix-component-loading";
21
+
22
+ const el = createLoading({ /* see docs/02-usage.md */ });
23
+ el.render(el.state);
24
+ renderLoading({ /* config */ }); // one call
25
+ ```
26
+
27
+ **Actions** — `show(state)` · `hide(state)` · `setLabel(state, label)`
28
+
29
+ ## Documentation
30
+
31
+ | # | Guide |
32
+ |---|-------|
33
+ | 01 | [Overview](docs/01-overview.md) |
34
+ | 02 | [Usage & API](docs/02-usage.md) |
35
+ | 03 | [Accessibility](docs/03-accessibility.md) |
36
+ | 04 | [State & rendered HTML](docs/04-state-and-html.md) |
37
+
38
+ ## Related
39
+
40
+ Part of the OBIX feedback set: `@obinexusltd/obix-component-``toast` · `alert` · `progress` · `loading`.
41
+
42
+ ## License
43
+
44
+ MIT — OBINexus Computing
@@ -0,0 +1,5 @@
1
+ import type { DOPComponent, LoadingConfig, LoadingState } from "./types.js";
2
+ export type { Action, DOPComponent, LoadingConfig, LoadingState } from "./types.js";
3
+ export declare function createLoading(config?: LoadingConfig): DOPComponent<LoadingState>;
4
+ export declare function renderLoading(config?: LoadingConfig, overrides?: Partial<LoadingState>): string;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5E,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAsBpF,wBAAgB,aAAa,CAAC,MAAM,GAAE,aAAkB,GAAG,YAAY,CAAC,YAAY,CAAC,CAkBpF;AAGD,wBAAgB,aAAa,CAC3B,MAAM,GAAE,aAAkB,EAC1B,SAAS,GAAE,OAAO,CAAC,YAAY,CAAM,GACpC,MAAM,CAGR"}
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ function esc(value) {
2
+ return String(value ?? "")
3
+ .replace(/&/g, "&amp;")
4
+ .replace(/</g, "&lt;")
5
+ .replace(/>/g, "&gt;")
6
+ .replace(/"/g, "&quot;");
7
+ }
8
+ function renderLoadingHtml(state) {
9
+ if (!state.visible)
10
+ return "";
11
+ return (`<div class="obix-loading${state.overlay ? " obix-loading--overlay" : ""}"` +
12
+ ` role="status" aria-live="polite" aria-busy="${state.visible}"` +
13
+ ` aria-label="${esc(state.label)}">` +
14
+ `<div class="obix-loading__spinner" aria-hidden="true"></div>` +
15
+ `<span class="obix-loading__label">${esc(state.label)}</span>` +
16
+ `</div>`);
17
+ }
18
+ export function createLoading(config = {}) {
19
+ if (!config || typeof config !== "object") {
20
+ throw new TypeError("[obix-component-loading] createLoading: config must be an object");
21
+ }
22
+ const state = {
23
+ visible: false,
24
+ label: config.label ?? "Loading…",
25
+ overlay: config.overlay ?? false,
26
+ };
27
+ const actions = {
28
+ show: (s) => ({ ...s, visible: true }),
29
+ hide: (s) => ({ ...s, visible: false }),
30
+ setLabel: (s, label) => ({ ...s, label: String(label) }),
31
+ };
32
+ return { name: "ObixLoading", state, actions, render: renderLoadingHtml };
33
+ }
34
+ export function renderLoading(config = {}, overrides = {}) {
35
+ const loading = createLoading(config);
36
+ return loading.render({ ...loading.state, ...overrides });
37
+ }
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAmB;IAC5C,IAAI,CAAC,KAAK,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IAC9B,OAAO,CACL,2BAA2B,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,EAAE,GAAG;QAC3E,gDAAgD,KAAK,CAAC,OAAO,GAAG;QAChE,gBAAgB,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI;QACpC,8DAA8D;QAC9D,qCAAqC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS;QAC9D,QAAQ,CACT,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,SAAwB,EAAE;IACtD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC1C,MAAM,IAAI,SAAS,CAAC,kEAAkE,CAAC,CAAC;IAC1F,CAAC;IAED,MAAM,KAAK,GAAiB;QAC1B,OAAO,EAAE,KAAK;QACd,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,UAAU;QACjC,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,KAAK;KACjC,CAAC;IAEF,MAAM,OAAO,GAAG;QACd,IAAI,EAAE,CAAC,CAAe,EAAgB,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAClE,IAAI,EAAE,CAAC,CAAe,EAAgB,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACnE,QAAQ,EAAE,CAAC,CAAe,EAAE,KAAc,EAAgB,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;KAC9F,CAAC;IAEF,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;AAC5E,CAAC;AAGD,MAAM,UAAU,aAAa,CAC3B,SAAwB,EAAE,EAC1B,YAAmC,EAAE;IAErC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,17 @@
1
+ export type Action<S> = (state: S, ...args: any[]) => S;
2
+ export interface DOPComponent<S> {
3
+ name: string;
4
+ state: S;
5
+ actions: Record<string, Action<S>>;
6
+ render: (state: S) => string;
7
+ }
8
+ export interface LoadingConfig {
9
+ label?: string;
10
+ overlay?: boolean;
11
+ }
12
+ export interface LoadingState {
13
+ visible: boolean;
14
+ label: string;
15
+ overlay: boolean;
16
+ }
17
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAExD,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAE5B,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAE3B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ # ObixLoading — Overview
2
+
3
+ `@obinexusltd/obix-component-loading` is one feedback component from the OBIX component set, packaged
4
+ on its own. It is **Data-Oriented** and has **zero runtime dependencies**.
5
+
6
+ ## The shape
7
+
8
+ ```ts
9
+ const loading = createLoading(config);
10
+ loading.name // "ObixLoading"
11
+ loading.state // the full component state (see docs/04)
12
+ loading.actions // pure (state, …args) => state transitions
13
+ loading.render // (state) => string — deterministic, HTML-escaped
14
+ ```
15
+
16
+ - **No classes, no lifecycle, no DOM.** Data plus pure functions. You own
17
+ the timers, event wiring, and where the HTML string goes.
18
+ - **Deterministic render.** `render(state)` returns the same markup for the
19
+ same state, and an **empty string** while the component is hidden.
20
+ - **Pure actions.** `actions.foo(state, …)` returns a *new* state.
21
+
22
+ ## When to use it
23
+
24
+ Use `@obinexusltd/obix-component-loading` when you want just this one piece of feedback UI without
25
+ pulling the whole feedback bundle.
26
+
@@ -0,0 +1,46 @@
1
+ # ObixLoading — Usage & API
2
+
3
+ ```ts
4
+ import { createLoading, renderLoading } from "@obinexusltd/obix-component-loading";
5
+ ```
6
+
7
+ ## `createLoading(config)`
8
+
9
+ Returns a `DOPComponent`: `{ name, state, actions, render }`. Throws
10
+ `TypeError` when a required field is missing or the wrong type.
11
+
12
+ | Field | Type | Default | Notes |
13
+ |-------|------|---------|-------|
14
+ | `label` | `string` | `"Loading…"` | `aria-label` + visible label text |
15
+ | `overlay` | `boolean` | `false` | Add `obix-loading--overlay` to cover the container |
16
+
17
+ The full `Config` / `State` interfaces are in
18
+ [`src/types.ts`](../src/types.ts), which ships in the package.
19
+
20
+ ## `renderLoading(config, overrides?)`
21
+
22
+ One-call render — `createLoading(config).render({ ...state, ...overrides })`.
23
+ Pass `{ visible: true }` as an override to render a normally-hidden state for
24
+ snapshots / SSR:
25
+
26
+ ```ts
27
+ renderLoading(config, { visible: true });
28
+ ```
29
+
30
+ ## Actions
31
+
32
+ `show(state)` · `hide(state)` · `setLabel(state, label)` — state starts `visible: false`.
33
+
34
+ Every action is `(state, …args) => newState` and **pure**. Thread state through
35
+ them (directly or via your store) and re-render:
36
+
37
+ ```ts
38
+ const l = createLoading({ label: "Loading dashboard…", overlay: true });
39
+ mount.innerHTML = l.render(l.state); // "" — hidden by default
40
+ mount.innerHTML = l.render(l.actions.show(l.state)); // <div role="status" aria-busy="true">
41
+ // … when the fetch resolves:
42
+ mount.innerHTML = l.render(l.actions.hide(l.state)); // ""
43
+ ```
44
+
45
+ `render(state)` returns `""` whenever the component is not visible — assigning
46
+ it to `innerHTML` cleanly removes the element.
@@ -0,0 +1,17 @@
1
+ # ObixLoading — Accessibility
2
+
3
+ The rendered HTML carries its ARIA inline — there is no separate a11y layer.
4
+
5
+ - Live-region semantics are built in: `role` / `aria-live` / `aria-atomic`
6
+ are set from state (e.g. assertive for errors, polite for status).
7
+ - All interpolated text (messages, labels) is HTML-escaped.
8
+ - Dismiss controls are real `<button type="button">` with an `aria-label`.
9
+
10
+ ## Your responsibilities
11
+
12
+ - Insert the rendered string into a container that already exists in the DOM
13
+ so the live region is announced when content changes.
14
+ - Drive timers yourself (e.g. call `dismiss` after `state.duration` ms).
15
+ - Manage focus: for a dismissible alert, return focus to the triggering
16
+ element (`returnFocusId` is carried in state for you to use).
17
+ - Render one instance per container; give toasts a unique `id`.
@@ -0,0 +1,22 @@
1
+ # ObixLoading — State & rendered HTML
2
+
3
+ ## State
4
+
5
+ The full state object is defined by the `State` interface in
6
+ [`src/types.ts`](../src/types.ts), built from `config` by
7
+ `createLoading`. Plain JSON-serialisable data.
8
+
9
+ ## Rendered HTML
10
+
11
+ `render(state)` returns a single element as a string, or **`""`** when the
12
+ component is not visible. Attribute order is fixed; only state-driven
13
+ attributes/branches change between renders.
14
+
15
+ Class names follow the `obix-*` convention
16
+ (`obix-loading`, `obix-loading--<modifier>`).
17
+ No CSS ships with the package.
18
+
19
+ ## Testing
20
+
21
+ `test/*.test.mjs` (Node’s built-in runner) asserts the rendered string and
22
+ action purity. Run `npm run build && npm test`.
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@obinexusltd/obix-component-loading",
3
+ "version": "0.1.0",
4
+ "description": "OBIX ObixLoading — a role=\"status\" spinner with an accessible label and an optional full-area overlay; starts hidden. Data-Oriented, zero dependencies.",
5
+ "license": "MIT",
6
+ "author": "OBINexus Computing — Nnamdi Michael Okpala <okpalan@protonmail.com>",
7
+ "type": "module",
8
+ "private": false,
9
+ "sideEffects": false,
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./src": "./src/index.ts",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "src",
22
+ "dist",
23
+ "docs",
24
+ "test",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "directories": {
29
+ "lib": "dist",
30
+ "doc": "docs",
31
+ "test": "test"
32
+ },
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "test": "node --test \"test/*.test.mjs\"",
36
+ "prepublishOnly": "npm run build"
37
+ },
38
+ "keywords": [
39
+ "obix",
40
+ "component",
41
+ "dop",
42
+ "accessibility",
43
+ "loading",
44
+ "spinner",
45
+ "busy",
46
+ "feedback"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "engines": {
52
+ "node": ">=20.11.0"
53
+ },
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/obinexusmk2/obix-component-loading.git"
57
+ }
58
+ }
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @obinexusltd/obix-component-loading
3
+ *
4
+ * The ObixLoading feedback component — a `role="status"` spinner with an
5
+ * accessible label and an optional full-area `overlay`. Starts hidden; call
6
+ * `show` to display it.
7
+ *
8
+ * Data-Oriented: `createLoading(config)` returns
9
+ * `{ name, state, actions, render }`. Actions are pure
10
+ * `(state, …args) => LoadingState`; `render(state)` is deterministic,
11
+ * HTML-escaped markup, or `""` while hidden. Zero dependencies.
12
+ *
13
+ * Split out of `@obinexusltd/obix-component-feedback`.
14
+ */
15
+ import type { DOPComponent, LoadingConfig, LoadingState } from "./types.js";
16
+
17
+ export type { Action, DOPComponent, LoadingConfig, LoadingState } from "./types.js";
18
+
19
+ function esc(value: unknown): string {
20
+ return String(value ?? "")
21
+ .replace(/&/g, "&amp;")
22
+ .replace(/</g, "&lt;")
23
+ .replace(/>/g, "&gt;")
24
+ .replace(/"/g, "&quot;");
25
+ }
26
+
27
+ function renderLoadingHtml(state: LoadingState): string {
28
+ if (!state.visible) return "";
29
+ return (
30
+ `<div class="obix-loading${state.overlay ? " obix-loading--overlay" : ""}"` +
31
+ ` role="status" aria-live="polite" aria-busy="${state.visible}"` +
32
+ ` aria-label="${esc(state.label)}">` +
33
+ `<div class="obix-loading__spinner" aria-hidden="true"></div>` +
34
+ `<span class="obix-loading__label">${esc(state.label)}</span>` +
35
+ `</div>`
36
+ );
37
+ }
38
+
39
+ export function createLoading(config: LoadingConfig = {}): DOPComponent<LoadingState> {
40
+ if (!config || typeof config !== "object") {
41
+ throw new TypeError("[obix-component-loading] createLoading: config must be an object");
42
+ }
43
+
44
+ const state: LoadingState = {
45
+ visible: false,
46
+ label: config.label ?? "Loading…",
47
+ overlay: config.overlay ?? false,
48
+ };
49
+
50
+ const actions = {
51
+ show: (s: LoadingState): LoadingState => ({ ...s, visible: true }),
52
+ hide: (s: LoadingState): LoadingState => ({ ...s, visible: false }),
53
+ setLabel: (s: LoadingState, label: unknown): LoadingState => ({ ...s, label: String(label) }),
54
+ };
55
+
56
+ return { name: "ObixLoading", state, actions, render: renderLoadingHtml };
57
+ }
58
+
59
+ /** Render a loading indicator's HTML in one call, optionally with state overrides. */
60
+ export function renderLoading(
61
+ config: LoadingConfig = {},
62
+ overrides: Partial<LoadingState> = {},
63
+ ): string {
64
+ const loading = createLoading(config);
65
+ return loading.render({ ...loading.state, ...overrides });
66
+ }
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @obinexusltd/obix-component-loading — types.
3
+ * Zero-dependency Data-Oriented component: { name, state, actions, render }.
4
+ */
5
+
6
+ /* eslint-disable @typescript-eslint/no-explicit-any */
7
+
8
+ export type Action<S> = (state: S, ...args: any[]) => S;
9
+
10
+ export interface DOPComponent<S> {
11
+ name: string;
12
+ state: S;
13
+ actions: Record<string, Action<S>>;
14
+ render: (state: S) => string;
15
+ }
16
+
17
+ export interface LoadingConfig {
18
+ /** Accessible label. Default `"Loading…"`. */
19
+ label?: string;
20
+ /** Cover the containing area with an overlay. Default `false`. */
21
+ overlay?: boolean;
22
+ }
23
+
24
+ export interface LoadingState {
25
+ /** Starts `false` — call `show` to display. */
26
+ visible: boolean;
27
+ label: string;
28
+ overlay: boolean;
29
+ }
@@ -0,0 +1,40 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createLoading, renderLoading } from "../dist/index.js";
4
+
5
+ test("factory returns a DOP component shape", () => {
6
+ const el = createLoading({ label: "Loading" });
7
+ assert.equal(el.name, "ObixLoading");
8
+ assert.equal(typeof el.render, "function");
9
+ assert.equal(typeof el.actions, "object");
10
+ assert.equal(typeof el.render(el.state), "string");
11
+ });
12
+
13
+ test("config is optional; a non-object arg throws", () => {
14
+ assert.doesNotThrow(() => createLoading());
15
+ assert.doesNotThrow(() => createLoading({}));
16
+ assert.throws(() => createLoading(42), TypeError);
17
+ });
18
+
19
+ test("starts hidden -> render is an empty string until shown", () => {
20
+ const el = createLoading({ label: "Please wait" });
21
+ assert.equal(el.render(el.state), "");
22
+ const shown = el.actions.show(el.state);
23
+ assert.match(el.render(shown), /role="status"/);
24
+ assert.match(el.render(shown), /aria-busy="true"/);
25
+ });
26
+
27
+ test("render is deterministic and escapes the label", () => {
28
+ const a = renderLoading({ label: "A & B <x>" }, { visible: true });
29
+ const b = renderLoading({ label: "A & B <x>" }, { visible: true });
30
+ assert.equal(a, b);
31
+ assert.match(a, /A &amp; B &lt;x&gt;/);
32
+ assert.doesNotMatch(a, /<x>/);
33
+ });
34
+
35
+ test("actions are pure - input state is untouched", () => {
36
+ const el = createLoading({ label: "x" });
37
+ const before = JSON.stringify(el.state);
38
+ for (const k of Object.keys(el.actions)) el.actions[k](el.state, "y");
39
+ assert.equal(JSON.stringify(el.state), before);
40
+ });