@luna_ui/luna 0.4.0 → 0.5.3

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.
Files changed (131) hide show
  1. package/dist/cli.mjs +1 -1
  2. package/dist/{index-CDWzWF-h.d.ts → index-vO066aMd.d.ts} +17 -6
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/jsx-dev-runtime.js +1 -1
  6. package/dist/jsx-runtime.d.ts +1 -1
  7. package/dist/jsx-runtime.js +1 -1
  8. package/dist/src-DviLMStS.js +1 -0
  9. package/package.json +1 -1
  10. package/src/index.ts +26 -8
  11. package/tests/__screenshots__/{preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png → apg.test.ts/APG-Components---Accessibility-Tests-Button-Pattern-disabled-button-has-aria-disabled-1.png} +0 -0
  12. package/tests/apg.test.ts +466 -0
  13. package/tests/debounced.test.ts +165 -0
  14. package/tests/dom.test.ts +3 -2
  15. package/tests/issue-11-show-null-to-truthy.test.ts +176 -0
  16. package/tests/solidjs-api.test.ts +5 -4
  17. package/dist/src-DEjrAhrg.js +0 -1
  18. package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
  19. package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
  20. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
  21. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
  22. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
  23. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
  24. package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
  25. package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
  26. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
  27. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
  28. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
  29. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
  30. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
  31. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-empty-to-non-empty-transition-in-SVG-1.png +0 -0
  32. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
  33. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
  34. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
  35. package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
  36. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
  37. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
  38. package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
  39. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-renders-correctly-without-show--initial-items--1.png +0 -0
  40. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-with-context-renders-correctly-without-show-1.png +0 -0
  41. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-nested-components-with-context--forEach--and-show-1.png +0 -0
  42. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-inherit-context-from-Owner--fixed--1.png +0 -0
  43. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-work-together--context-uses-default--1.png +0 -0
  44. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
  45. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
  46. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
  47. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
  48. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
  49. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
  50. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
  51. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
  52. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
  53. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
  54. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
  55. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-works-with-For-loop-items--component-body-style--1.png +0 -0
  56. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
  57. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
  58. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
  59. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
  60. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
  61. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
  62. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
  63. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
  64. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
  65. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
  66. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
  67. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
  68. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
  69. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
  70. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
  71. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
  72. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
  73. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
  74. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
  75. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
  76. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
  77. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
  78. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
  79. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
  80. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
  81. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
  82. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
  83. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
  84. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
  85. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
  86. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
  87. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
  88. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
  89. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
  90. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
  91. package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
  92. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
  93. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
  94. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
  95. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
  96. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
  97. package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
  98. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
  99. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
  100. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
  101. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
  102. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
  103. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
  104. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
  105. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
  106. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
  107. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
  108. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
  109. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
  110. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
  111. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
  112. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
  113. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
  114. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
  115. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
  116. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
  117. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
  118. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
  119. package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
  120. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
  121. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
  122. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
  123. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
  124. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
  125. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
  126. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
  127. package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
  128. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
  129. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
  130. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
  131. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
@@ -0,0 +1,466 @@
1
+ import { describe, test, expect, beforeEach } from "vitest";
2
+ import {
3
+ createElement,
4
+ render,
5
+ text,
6
+ } from "../src/index";
7
+ import axe from "axe-core";
8
+
9
+ function attr(name: string, value: unknown) {
10
+ return { _0: name, _1: value };
11
+ }
12
+
13
+ const AttrValue = {
14
+ Static: (value: string) => ({ $tag: 0, _0: value }),
15
+ };
16
+
17
+ async function checkA11y(container: HTMLElement): Promise<axe.Result[]> {
18
+ const results = await axe.run(container);
19
+ return results.violations;
20
+ }
21
+
22
+ describe("APG Components - Accessibility Tests", () => {
23
+ let container: HTMLElement;
24
+
25
+ beforeEach(() => {
26
+ container = document.createElement("div");
27
+ document.body.appendChild(container);
28
+ return () => {
29
+ container.remove();
30
+ };
31
+ });
32
+
33
+ describe("Link Pattern", () => {
34
+ test("native link has no a11y violations", async () => {
35
+ const node = createElement(
36
+ "a",
37
+ [
38
+ attr("href", AttrValue.Static("https://example.com")),
39
+ ],
40
+ [text("Visit Example")]
41
+ );
42
+ render(container, node);
43
+
44
+ const violations = await checkA11y(container);
45
+ expect(violations).toHaveLength(0);
46
+ });
47
+
48
+ test("link with aria-label for icon is accessible", async () => {
49
+ const node = createElement(
50
+ "a",
51
+ [
52
+ attr("href", AttrValue.Static("/home")),
53
+ attr("aria-label", AttrValue.Static("Go to home page")),
54
+ ],
55
+ [text("🏠")]
56
+ );
57
+ render(container, node);
58
+
59
+ const violations = await checkA11y(container);
60
+ expect(violations).toHaveLength(0);
61
+ });
62
+
63
+ test("link with target=_blank should have accessible text", async () => {
64
+ const node = createElement(
65
+ "a",
66
+ [
67
+ attr("href", AttrValue.Static("https://example.com")),
68
+ attr("target", AttrValue.Static("_blank")),
69
+ ],
70
+ [text("External Link (opens in new tab)")]
71
+ );
72
+ render(container, node);
73
+
74
+ const violations = await checkA11y(container);
75
+ expect(violations).toHaveLength(0);
76
+ });
77
+
78
+ test("link-button with role=link has proper attributes", async () => {
79
+ const node = createElement(
80
+ "span",
81
+ [
82
+ attr("role", AttrValue.Static("link")),
83
+ attr("tabindex", AttrValue.Static("0")),
84
+ ],
85
+ [text("Click here")]
86
+ );
87
+ render(container, node);
88
+
89
+ const span = container.querySelector("span");
90
+ expect(span?.getAttribute("role")).toBe("link");
91
+ expect(span?.getAttribute("tabindex")).toBe("0");
92
+ });
93
+ });
94
+
95
+ describe("Button Pattern", () => {
96
+ test("native button has no a11y violations", async () => {
97
+ const node = createElement(
98
+ "button",
99
+ [attr("type", AttrValue.Static("button"))],
100
+ [text("Click me")]
101
+ );
102
+ render(container, node);
103
+
104
+ const violations = await checkA11y(container);
105
+ expect(violations).toHaveLength(0);
106
+ });
107
+
108
+ test("icon button with aria-label is accessible", async () => {
109
+ const node = createElement(
110
+ "button",
111
+ [
112
+ attr("type", AttrValue.Static("button")),
113
+ attr("aria-label", AttrValue.Static("Close dialog")),
114
+ ],
115
+ [text("×")]
116
+ );
117
+ render(container, node);
118
+
119
+ const violations = await checkA11y(container);
120
+ expect(violations).toHaveLength(0);
121
+ });
122
+
123
+ test("disabled button has aria-disabled", async () => {
124
+ const node = createElement(
125
+ "button",
126
+ [
127
+ attr("type", AttrValue.Static("button")),
128
+ attr("disabled", AttrValue.Static("true")),
129
+ attr("aria-disabled", AttrValue.Static("true")),
130
+ ],
131
+ [text("Disabled")]
132
+ );
133
+ render(container, node);
134
+
135
+ const button = container.querySelector("button");
136
+ expect(button?.getAttribute("aria-disabled")).toBe("true");
137
+
138
+ const violations = await checkA11y(container);
139
+ expect(violations).toHaveLength(0);
140
+ });
141
+
142
+ test("toggle button has aria-pressed", async () => {
143
+ const node = createElement(
144
+ "button",
145
+ [
146
+ attr("type", AttrValue.Static("button")),
147
+ attr("aria-pressed", AttrValue.Static("false")),
148
+ ],
149
+ [text("Mute")]
150
+ );
151
+ render(container, node);
152
+
153
+ const button = container.querySelector("button");
154
+ expect(button?.getAttribute("aria-pressed")).toBe("false");
155
+
156
+ const violations = await checkA11y(container);
157
+ expect(violations).toHaveLength(0);
158
+ });
159
+
160
+ test("toggle button pressed state", async () => {
161
+ const node = createElement(
162
+ "button",
163
+ [
164
+ attr("type", AttrValue.Static("button")),
165
+ attr("aria-pressed", AttrValue.Static("true")),
166
+ ],
167
+ [text("Mute")]
168
+ );
169
+ render(container, node);
170
+
171
+ const button = container.querySelector("button");
172
+ expect(button?.getAttribute("aria-pressed")).toBe("true");
173
+
174
+ const violations = await checkA11y(container);
175
+ expect(violations).toHaveLength(0);
176
+ });
177
+
178
+ test("menu button has aria-haspopup and aria-expanded", async () => {
179
+ const node = createElement(
180
+ "button",
181
+ [
182
+ attr("type", AttrValue.Static("button")),
183
+ attr("aria-haspopup", AttrValue.Static("menu")),
184
+ attr("aria-expanded", AttrValue.Static("false")),
185
+ ],
186
+ [text("Options")]
187
+ );
188
+ render(container, node);
189
+
190
+ const button = container.querySelector("button");
191
+ expect(button?.getAttribute("aria-haspopup")).toBe("menu");
192
+ expect(button?.getAttribute("aria-expanded")).toBe("false");
193
+
194
+ const violations = await checkA11y(container);
195
+ expect(violations).toHaveLength(0);
196
+ });
197
+
198
+ test("menu button expanded state", async () => {
199
+ const node = createElement(
200
+ "button",
201
+ [
202
+ attr("type", AttrValue.Static("button")),
203
+ attr("aria-haspopup", AttrValue.Static("menu")),
204
+ attr("aria-expanded", AttrValue.Static("true")),
205
+ attr("aria-controls", AttrValue.Static("menu-id")),
206
+ ],
207
+ [text("Options")]
208
+ );
209
+ render(container, node);
210
+
211
+ const button = container.querySelector("button");
212
+ expect(button?.getAttribute("aria-haspopup")).toBe("menu");
213
+ expect(button?.getAttribute("aria-expanded")).toBe("true");
214
+ expect(button?.getAttribute("aria-controls")).toBe("menu-id");
215
+ });
216
+ });
217
+
218
+ describe("Meter Pattern", () => {
219
+ test("meter with role and aria attributes is accessible", async () => {
220
+ const node = createElement(
221
+ "div",
222
+ [
223
+ attr("role", AttrValue.Static("meter")),
224
+ attr("aria-valuenow", AttrValue.Static("75")),
225
+ attr("aria-valuemin", AttrValue.Static("0")),
226
+ attr("aria-valuemax", AttrValue.Static("100")),
227
+ attr("aria-label", AttrValue.Static("Battery level")),
228
+ ],
229
+ [text("75%")]
230
+ );
231
+ render(container, node);
232
+
233
+ const meter = container.querySelector("[role='meter']");
234
+ expect(meter?.getAttribute("aria-valuenow")).toBe("75");
235
+ expect(meter?.getAttribute("aria-valuemin")).toBe("0");
236
+ expect(meter?.getAttribute("aria-valuemax")).toBe("100");
237
+
238
+ const violations = await checkA11y(container);
239
+ expect(violations).toHaveLength(0);
240
+ });
241
+
242
+ test("meter with aria-valuetext for human-readable value", async () => {
243
+ const node = createElement(
244
+ "div",
245
+ [
246
+ attr("role", AttrValue.Static("meter")),
247
+ attr("aria-valuenow", AttrValue.Static("50")),
248
+ attr("aria-valuemin", AttrValue.Static("0")),
249
+ attr("aria-valuemax", AttrValue.Static("100")),
250
+ attr("aria-valuetext", AttrValue.Static("50% (6 hours) remaining")),
251
+ attr("aria-label", AttrValue.Static("Battery")),
252
+ ],
253
+ [text("50%")]
254
+ );
255
+ render(container, node);
256
+
257
+ const meter = container.querySelector("[role='meter']");
258
+ expect(meter?.getAttribute("aria-valuetext")).toBe("50% (6 hours) remaining");
259
+
260
+ const violations = await checkA11y(container);
261
+ expect(violations).toHaveLength(0);
262
+ });
263
+
264
+ test("native meter element is accessible", async () => {
265
+ const node = createElement(
266
+ "meter",
267
+ [
268
+ attr("value", AttrValue.Static("75")),
269
+ attr("min", AttrValue.Static("0")),
270
+ attr("max", AttrValue.Static("100")),
271
+ attr("aria-label", AttrValue.Static("Disk usage")),
272
+ ],
273
+ [text("75%")]
274
+ );
275
+ render(container, node);
276
+
277
+ const meter = container.querySelector("meter");
278
+ expect(meter?.getAttribute("value")).toBe("75");
279
+
280
+ const violations = await checkA11y(container);
281
+ expect(violations).toHaveLength(0);
282
+ });
283
+ });
284
+
285
+ describe("Landmarks Pattern", () => {
286
+ test("header element creates banner landmark", async () => {
287
+ const node = createElement("header", [], [text("Site Header")]);
288
+ render(container, node);
289
+
290
+ const header = container.querySelector("header");
291
+ expect(header).not.toBeNull();
292
+
293
+ const violations = await checkA11y(container);
294
+ expect(violations).toHaveLength(0);
295
+ });
296
+
297
+ test("nav element creates navigation landmark", async () => {
298
+ const node = createElement(
299
+ "nav",
300
+ [attr("aria-label", AttrValue.Static("Main navigation"))],
301
+ [
302
+ createElement("a", [attr("href", AttrValue.Static("/"))], [text("Home")]),
303
+ ]
304
+ );
305
+ render(container, node);
306
+
307
+ const nav = container.querySelector("nav");
308
+ expect(nav?.getAttribute("aria-label")).toBe("Main navigation");
309
+
310
+ const violations = await checkA11y(container);
311
+ expect(violations).toHaveLength(0);
312
+ });
313
+
314
+ test("main element creates main landmark", async () => {
315
+ const node = createElement("main", [], [text("Main Content")]);
316
+ render(container, node);
317
+
318
+ const main = container.querySelector("main");
319
+ expect(main).not.toBeNull();
320
+
321
+ const violations = await checkA11y(container);
322
+ expect(violations).toHaveLength(0);
323
+ });
324
+
325
+ test("aside element creates complementary landmark", async () => {
326
+ const node = createElement(
327
+ "aside",
328
+ [attr("aria-label", AttrValue.Static("Related content"))],
329
+ [text("Sidebar")]
330
+ );
331
+ render(container, node);
332
+
333
+ const aside = container.querySelector("aside");
334
+ expect(aside?.getAttribute("aria-label")).toBe("Related content");
335
+
336
+ const violations = await checkA11y(container);
337
+ expect(violations).toHaveLength(0);
338
+ });
339
+
340
+ test("footer element creates contentinfo landmark", async () => {
341
+ const node = createElement("footer", [], [text("© 2024")]);
342
+ render(container, node);
343
+
344
+ const footer = container.querySelector("footer");
345
+ expect(footer).not.toBeNull();
346
+
347
+ const violations = await checkA11y(container);
348
+ expect(violations).toHaveLength(0);
349
+ });
350
+
351
+ test("search element creates search landmark", async () => {
352
+ const node = createElement(
353
+ "search",
354
+ [attr("aria-label", AttrValue.Static("Site search"))],
355
+ [
356
+ createElement(
357
+ "input",
358
+ [
359
+ attr("type", AttrValue.Static("search")),
360
+ attr("aria-label", AttrValue.Static("Search query")),
361
+ ],
362
+ []
363
+ ),
364
+ ]
365
+ );
366
+ render(container, node);
367
+
368
+ const search = container.querySelector("search");
369
+ expect(search).not.toBeNull();
370
+
371
+ const violations = await checkA11y(container);
372
+ expect(violations).toHaveLength(0);
373
+ });
374
+
375
+ test("form with role=search creates search landmark", async () => {
376
+ const node = createElement(
377
+ "form",
378
+ [
379
+ attr("role", AttrValue.Static("search")),
380
+ attr("aria-label", AttrValue.Static("Site search")),
381
+ ],
382
+ [
383
+ createElement(
384
+ "input",
385
+ [
386
+ attr("type", AttrValue.Static("search")),
387
+ attr("aria-label", AttrValue.Static("Search query")),
388
+ ],
389
+ []
390
+ ),
391
+ ]
392
+ );
393
+ render(container, node);
394
+
395
+ const form = container.querySelector("form[role='search']");
396
+ expect(form).not.toBeNull();
397
+
398
+ const violations = await checkA11y(container);
399
+ expect(violations).toHaveLength(0);
400
+ });
401
+
402
+ test("section with aria-label creates region landmark", async () => {
403
+ const node = createElement(
404
+ "section",
405
+ [attr("aria-label", AttrValue.Static("Quick Stats"))],
406
+ [text("Statistics content")]
407
+ );
408
+ render(container, node);
409
+
410
+ const section = container.querySelector("section");
411
+ expect(section?.getAttribute("aria-label")).toBe("Quick Stats");
412
+
413
+ const violations = await checkA11y(container);
414
+ expect(violations).toHaveLength(0);
415
+ });
416
+ });
417
+
418
+ describe("Keyboard Interaction", () => {
419
+ test("button responds to Enter key", () => {
420
+ let clicked = false;
421
+ const node = createElement(
422
+ "button",
423
+ [
424
+ attr("type", AttrValue.Static("button")),
425
+ attr("click", { $tag: 2, _0: () => { clicked = true; } }),
426
+ ],
427
+ [text("Click")]
428
+ );
429
+ render(container, node);
430
+
431
+ const button = container.querySelector("button");
432
+ button?.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
433
+ button?.click();
434
+ expect(clicked).toBe(true);
435
+ });
436
+
437
+ test("button is focusable", () => {
438
+ const node = createElement(
439
+ "button",
440
+ [attr("type", AttrValue.Static("button"))],
441
+ [text("Focusable")]
442
+ );
443
+ render(container, node);
444
+
445
+ const button = container.querySelector("button");
446
+ button?.focus();
447
+ expect(document.activeElement).toBe(button);
448
+ });
449
+
450
+ test("link-button with role=link is focusable via tabindex", () => {
451
+ const node = createElement(
452
+ "span",
453
+ [
454
+ attr("role", AttrValue.Static("link")),
455
+ attr("tabindex", AttrValue.Static("0")),
456
+ ],
457
+ [text("Focusable link")]
458
+ );
459
+ render(container, node);
460
+
461
+ const span = container.querySelector("span");
462
+ span?.focus();
463
+ expect(document.activeElement).toBe(span);
464
+ });
465
+ });
466
+ });
@@ -0,0 +1,165 @@
1
+ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { createSignal, debounced, createRenderEffect } from "../src/index";
3
+
4
+ describe("debounced signal", () => {
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ });
8
+
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
12
+
13
+ test("debounced returns initial value immediately", () => {
14
+ const [value, setValue] = createSignal(42);
15
+ const [debouncedValue] = debounced([value, setValue], 100);
16
+
17
+ expect(debouncedValue()).toBe(42);
18
+ });
19
+
20
+ test("debounced delays value updates", () => {
21
+ const [value, setValue] = createSignal("initial");
22
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 100);
23
+
24
+ // Update the value
25
+ setDebouncedValue("updated");
26
+
27
+ // Debounced value should still be initial (before timeout)
28
+ expect(debouncedValue()).toBe("initial");
29
+
30
+ // Advance time by less than delay
31
+ vi.advanceTimersByTime(50);
32
+ expect(debouncedValue()).toBe("initial");
33
+
34
+ // Advance time to complete the delay
35
+ vi.advanceTimersByTime(50);
36
+ expect(debouncedValue()).toBe("updated");
37
+ });
38
+
39
+ test("debounced cancels previous timer on rapid updates", () => {
40
+ const [value, setValue] = createSignal(0);
41
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 100);
42
+
43
+ // Rapid updates
44
+ setDebouncedValue(1);
45
+ vi.advanceTimersByTime(50);
46
+ setDebouncedValue(2);
47
+ vi.advanceTimersByTime(50);
48
+ setDebouncedValue(3);
49
+
50
+ // Should still be initial value
51
+ expect(debouncedValue()).toBe(0);
52
+
53
+ // Advance to complete the delay from last update
54
+ vi.advanceTimersByTime(100);
55
+ expect(debouncedValue()).toBe(3);
56
+ });
57
+
58
+ test("debounced with 0ms delay updates immediately after microtask", () => {
59
+ const [value, setValue] = createSignal("start");
60
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 0);
61
+
62
+ setDebouncedValue("end");
63
+
64
+ // Even 0ms delay needs timer to fire
65
+ vi.advanceTimersByTime(0);
66
+ expect(debouncedValue()).toBe("end");
67
+ });
68
+
69
+ test("debounced triggers reactive effects after delay", () => {
70
+ const [value, setValue] = createSignal(0);
71
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 100);
72
+
73
+ const log: number[] = [];
74
+ createRenderEffect(() => {
75
+ log.push(debouncedValue());
76
+ });
77
+
78
+ // Initial effect run
79
+ expect(log).toEqual([0]);
80
+
81
+ // Update value
82
+ setDebouncedValue(10);
83
+ expect(log).toEqual([0]); // Not yet updated
84
+
85
+ // After delay, effect should run
86
+ vi.advanceTimersByTime(100);
87
+ expect(log).toEqual([0, 10]);
88
+ });
89
+
90
+ test("debounced handles multiple sequential updates correctly", () => {
91
+ const [value, setValue] = createSignal("a");
92
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 50);
93
+
94
+ // First update
95
+ setDebouncedValue("b");
96
+ vi.advanceTimersByTime(50);
97
+ expect(debouncedValue()).toBe("b");
98
+
99
+ // Second update (after first completed)
100
+ setDebouncedValue("c");
101
+ vi.advanceTimersByTime(50);
102
+ expect(debouncedValue()).toBe("c");
103
+ });
104
+
105
+ test("debounced works with object values", () => {
106
+ const [value, setValue] = createSignal({ count: 0 });
107
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 100);
108
+
109
+ const newObj = { count: 5 };
110
+ setDebouncedValue(newObj);
111
+
112
+ expect(debouncedValue().count).toBe(0);
113
+
114
+ vi.advanceTimersByTime(100);
115
+ expect(debouncedValue().count).toBe(5);
116
+ expect(debouncedValue()).toBe(newObj); // Same reference
117
+ });
118
+
119
+ test("debounced works with array values", () => {
120
+ const [value, setValue] = createSignal<number[]>([1, 2, 3]);
121
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 100);
122
+
123
+ setDebouncedValue([4, 5, 6]);
124
+
125
+ expect(debouncedValue()).toEqual([1, 2, 3]);
126
+
127
+ vi.advanceTimersByTime(100);
128
+ expect(debouncedValue()).toEqual([4, 5, 6]);
129
+ });
130
+
131
+ test("debounced with long delay", () => {
132
+ const [value, setValue] = createSignal("original");
133
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 1000);
134
+
135
+ setDebouncedValue("delayed");
136
+
137
+ // Check at various points
138
+ vi.advanceTimersByTime(500);
139
+ expect(debouncedValue()).toBe("original");
140
+
141
+ vi.advanceTimersByTime(499);
142
+ expect(debouncedValue()).toBe("original");
143
+
144
+ vi.advanceTimersByTime(1);
145
+ expect(debouncedValue()).toBe("delayed");
146
+ });
147
+
148
+ test("debounced handles many rapid updates efficiently", () => {
149
+ const [value, setValue] = createSignal(0);
150
+ const [debouncedValue, setDebouncedValue] = debounced([value, setValue], 100);
151
+
152
+ // Simulate 100 rapid updates
153
+ for (let i = 1; i <= 100; i++) {
154
+ setDebouncedValue(i);
155
+ vi.advanceTimersByTime(10); // 10ms between each
156
+ }
157
+
158
+ // Should still be 0 because timer keeps getting reset
159
+ expect(debouncedValue()).toBe(0);
160
+
161
+ // Wait for final debounce to complete
162
+ vi.advanceTimersByTime(100);
163
+ expect(debouncedValue()).toBe(100);
164
+ });
165
+ });
package/tests/dom.test.ts CHANGED
@@ -421,12 +421,13 @@ describe("DOM API", () => {
421
421
  expect(container.querySelector("span")).toBeNull();
422
422
  });
423
423
 
424
- test("Show accepts children as function", () => {
424
+ test("Show accepts children as function with accessor (SolidJS-style)", () => {
425
425
  const [value, setValue] = createSignal<string | null>(null);
426
426
 
427
427
  const node = Show({
428
428
  when: value,
429
- children: (v: string) => createElement("span", [], [text(v)]),
429
+ // SolidJS-style: children receives accessor function, call it with ()
430
+ children: (v: () => string) => createElement("span", [], [text(v())]),
430
431
  });
431
432
 
432
433
  mount(container, node);