tempestweb 0.5.2__tar.gz → 0.5.3__tar.gz

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 (116) hide show
  1. {tempestweb-0.5.2 → tempestweb-0.5.3}/CHANGELOG.md +22 -0
  2. {tempestweb-0.5.2 → tempestweb-0.5.3}/PKG-INFO +7 -1
  3. {tempestweb-0.5.2 → tempestweb-0.5.3}/README.md +6 -0
  4. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/dom.js +74 -6
  5. {tempestweb-0.5.2 → tempestweb-0.5.3}/pyproject.toml +1 -1
  6. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/dom.test.js +25 -6
  7. {tempestweb-0.5.2 → tempestweb-0.5.3}/.gitignore +0 -0
  8. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/constants.js +0 -0
  9. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/events.js +0 -0
  10. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/livereload.js +0 -0
  11. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/audio.js +0 -0
  12. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/camera.js +0 -0
  13. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/clipboard.js +0 -0
  14. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/file.js +0 -0
  15. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/geolocation.js +0 -0
  16. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/http.js +0 -0
  17. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/index.js +0 -0
  18. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/install.js +0 -0
  19. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/notifications.js +0 -0
  20. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/onnx.js +0 -0
  21. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/share.js +0 -0
  22. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/native/storage.js +0 -0
  23. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/offline/store.js +0 -0
  24. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/offline/sync.js +0 -0
  25. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/push/web-push-client.js +0 -0
  26. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/pwa/install-prompt.js +0 -0
  27. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/pwa/manifest.js +0 -0
  28. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/router.js +0 -0
  29. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/style.js +0 -0
  30. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/sw/register.js +0 -0
  31. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/sw/sw.js +0 -0
  32. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/tempestweb.js +0 -0
  33. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/transport-sse.js +0 -0
  34. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/transport-wasm.js +0 -0
  35. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/transport-ws.js +0 -0
  36. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/transport.js +0 -0
  37. {tempestweb-0.5.2 → tempestweb-0.5.3}/client/virtualize.js +0 -0
  38. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/__init__.py +0 -0
  39. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/_core/__init__.py +0 -0
  40. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/__init__.py +0 -0
  41. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/commands/__init__.py +0 -0
  42. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/commands/build.py +0 -0
  43. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/commands/dev.py +0 -0
  44. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/commands/new.py +0 -0
  45. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/commands/run.py +0 -0
  46. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/commands/sync.py +0 -0
  47. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/config.py +0 -0
  48. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/loader.py +0 -0
  49. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/main.py +0 -0
  50. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/cli/scaffold.py +0 -0
  51. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/components/__init__.py +0 -0
  52. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/components/fields.py +0 -0
  53. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/components/forms.py +0 -0
  54. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/core/__init__.py +0 -0
  55. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/core/constants.py +0 -0
  56. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/devserver/__init__.py +0 -0
  57. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/devserver/http.py +0 -0
  58. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/devserver/reload.py +0 -0
  59. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/devserver/watcher.py +0 -0
  60. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/__init__.py +0 -0
  61. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/audio.py +0 -0
  62. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/bridges.py +0 -0
  63. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/camera.py +0 -0
  64. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/clipboard.py +0 -0
  65. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/dispatch.py +0 -0
  66. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/file.py +0 -0
  67. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/geolocation.py +0 -0
  68. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/http.py +0 -0
  69. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/install.py +0 -0
  70. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/notifications.py +0 -0
  71. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/onnx.py +0 -0
  72. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/share.py +0 -0
  73. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/native/storage.py +0 -0
  74. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/observability/__init__.py +0 -0
  75. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/observability/auth.py +0 -0
  76. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/observability/error_boundary.py +0 -0
  77. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/observability/feature_flags.py +0 -0
  78. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/observability/logger.py +0 -0
  79. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/observability/telemetry.py +0 -0
  80. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/pwa/__init__.py +0 -0
  81. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/pwa/icons.py +0 -0
  82. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/pwa/manifest.py +0 -0
  83. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/pwa/pyodide_vendor.py +0 -0
  84. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/runtime/__init__.py +0 -0
  85. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/runtime/events.py +0 -0
  86. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/runtime/serialize.py +0 -0
  87. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/runtime/session.py +0 -0
  88. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/runtime/wasm.py +0 -0
  89. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/runtime/wasm_main.py +0 -0
  90. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/server/__init__.py +0 -0
  91. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/server/app.py +0 -0
  92. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/server/webpush.py +0 -0
  93. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/transports/__init__.py +0 -0
  94. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/transports/base.py +0 -0
  95. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/transports/sse.py +0 -0
  96. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/transports/wasm.py +0 -0
  97. {tempestweb-0.5.2 → tempestweb-0.5.3}/tempestweb/transports/websocket.py +0 -0
  98. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/events.test.js +0 -0
  99. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/mount.test.js +0 -0
  100. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/native-onnx-file.test.js +0 -0
  101. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/native.test.js +0 -0
  102. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/offline-store.test.js +0 -0
  103. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/offline-sync.test.js +0 -0
  104. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/pwa-install-prompt.test.js +0 -0
  105. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/pwa-manifest.test.js +0 -0
  106. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/router.test.js +0 -0
  107. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/setup.js +0 -0
  108. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/smoke.test.js +0 -0
  109. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/style.test.js +0 -0
  110. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/sw-register.test.js +0 -0
  111. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/sw-strategies.test.js +0 -0
  112. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/transport-sse.test.js +0 -0
  113. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/transport-wasm.test.js +0 -0
  114. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/transport-ws.test.js +0 -0
  115. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/virtualize.test.js +0 -0
  116. {tempestweb-0.5.2 → tempestweb-0.5.3}/tests/client/web-push-client.test.js +0 -0
@@ -4,6 +4,28 @@ All notable changes to **tempestweb** are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/); this project adheres to semantic
5
5
  versioning.
6
6
 
7
+ ## [0.5.3] — 2026-06-13
8
+
9
+ ### Fixed
10
+
11
+ - **`Checkbox` now renders its label as visible text on the web.** The DOM
12
+ renderer mapped `Checkbox` to a bare `<input type=checkbox>` and put its
13
+ `label` on `aria-label` only, so labelled checkboxes (todo items, settings
14
+ toggles) showed as empty boxes. A `Checkbox` now renders as a `<label>`
15
+ wrapping the real `<input>` plus a caption text node: the box and its caption
16
+ lay out as one tidy row (the wrapper also gives the input its accessible name
17
+ natively). The `<label>` is the keyed, path-addressed element; the nested
18
+ input carries `checked` and fires `change`, which bubbles to the label for
19
+ event delegation. An explicit widget `Style` still wins.
20
+
21
+ ### Examples
22
+
23
+ - **Fixed three examples that passed `children=` to `Container`.** `Container`
24
+ holds a single `child`, not a `children` list (that is `Column`/`Row`/`Stack`).
25
+ Pydantic silently dropped the unknown kwarg, so the container rendered empty:
26
+ `list_demo` lost its row text (1000-item list showed blank rows), `gesture_demo`
27
+ lost its "swipe or tap me" hint, and `anim_demo` carried a no-op `children=[]`.
28
+
7
29
  ## [0.5.2] — 2026-06-13
8
30
 
9
31
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tempestweb
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Build web apps in typed Python — one tree, a DOM renderer, two execution modes (WASM + server).
5
5
  Author-email: Mauricio Benjamin <mauricio.benjamin@reloverelations.com>
6
6
  License: MIT
@@ -60,6 +60,12 @@ demos (stopwatch, forms, data table, kanban, chat, theming, i18n, canvas, native
60
60
  capabilities, observability, PWA/WebPush, and a server-mode walkthrough), each
61
61
  running unchanged in both modes.
62
62
 
63
+ Building something real? Read the **[App architecture & best practices](https://mauriciobenjamin700.github.io/tempestweb/best-practices/)**
64
+ guide ([EN](https://mauriciobenjamin700.github.io/tempestweb/en/best-practices/)) —
65
+ the ideal layered structure (routes · pages · components · styles · controllers ·
66
+ services · storages · schemas · utils · core), mirroring `tempest-fastapi-sdk`, so
67
+ your app doesn't rot into garbage code.
68
+
63
69
  ## How it works
64
70
 
65
71
  ```text
@@ -32,6 +32,12 @@ demos (stopwatch, forms, data table, kanban, chat, theming, i18n, canvas, native
32
32
  capabilities, observability, PWA/WebPush, and a server-mode walkthrough), each
33
33
  running unchanged in both modes.
34
34
 
35
+ Building something real? Read the **[App architecture & best practices](https://mauriciobenjamin700.github.io/tempestweb/best-practices/)**
36
+ guide ([EN](https://mauriciobenjamin700.github.io/tempestweb/en/best-practices/)) —
37
+ the ideal layered structure (routes · pages · components · styles · controllers ·
38
+ services · storages · schemas · utils · core), mirroring `tempest-fastapi-sdk`, so
39
+ your app doesn't rot into garbage code.
40
+
35
41
  ## How it works
36
42
 
37
43
  ```text
@@ -32,7 +32,12 @@ const TAG_BY_TYPE = Object.freeze({
32
32
  Text: "span",
33
33
  Button: "button",
34
34
  Input: "input",
35
- Checkbox: "input",
35
+ // A Checkbox renders as a <label> wrapping a real <input type=checkbox> plus
36
+ // its caption text, so the box and its label show side by side and the input
37
+ // gets its accessible name natively. The <label> is the keyed, path-addressed
38
+ // element; the input it wraps is renderer-internal (Checkbox is an IR leaf, so
39
+ // no patch path ever descends into it).
40
+ Checkbox: "label",
36
41
  Image: "img",
37
42
  });
38
43
 
@@ -89,14 +94,15 @@ function applyProps(el, props) {
89
94
  el.removeAttribute("style");
90
95
  }
91
96
  }
92
- // Text-bearing props. A Checkbox is an <input> and cannot hold text, so its
93
- // label rides as an accessible name instead of textContent.
97
+ // Text-bearing props. A Checkbox is a <label> wrapping an <input>, so its
98
+ // caption is set as a trailing text node (see setCheckboxLabel) rather than
99
+ // overwriting textContent, which would drop the nested input.
94
100
  if ("content" in props) {
95
101
  el.textContent = props.content == null ? "" : String(props.content);
96
102
  }
97
103
  if ("label" in props) {
98
104
  if (type === "Checkbox") {
99
- el.setAttribute("aria-label", props.label == null ? "" : String(props.label));
105
+ setCheckboxLabel(el, props.label == null ? "" : String(props.label));
100
106
  } else {
101
107
  el.textContent = props.label == null ? "" : String(props.label);
102
108
  }
@@ -188,6 +194,46 @@ function applyLazyProps(el, type, props) {
188
194
  el.setAttribute("data-tw-window-start", String(start ?? 0));
189
195
  }
190
196
 
197
+ /**
198
+ * Get (or lazily create) the real ``<input type=checkbox>`` nested inside a
199
+ * Checkbox's ``<label>`` wrapper. The label is the keyed, path-addressed
200
+ * element; this nested input carries the actual ``checked`` state and fires the
201
+ * native ``change`` event (which bubbles up to the keyed label for delegation).
202
+ *
203
+ * @param {HTMLElement} el The Checkbox ``<label>`` element.
204
+ * @returns {HTMLInputElement} The nested checkbox input.
205
+ */
206
+ function ensureCheckboxInput(el) {
207
+ let input = /** @type {HTMLInputElement|null} */ (el.querySelector("input"));
208
+ if (input == null) {
209
+ input = /** @type {HTMLInputElement} */ (document.createElement("input"));
210
+ input.setAttribute("type", "checkbox");
211
+ el.insertBefore(input, el.firstChild);
212
+ }
213
+ return input;
214
+ }
215
+
216
+ /**
217
+ * Set a Checkbox's visible caption, kept as a single text node after the nested
218
+ * input so the box and its label render side by side. Wrapping the input in the
219
+ * ``<label>`` also gives it its accessible name natively (no ``aria-label``).
220
+ *
221
+ * @param {HTMLElement} el The Checkbox ``<label>`` element.
222
+ * @param {string} text The caption text (``""`` clears it).
223
+ * @returns {void}
224
+ */
225
+ function setCheckboxLabel(el, text) {
226
+ const input = ensureCheckboxInput(el);
227
+ for (const node of Array.from(el.childNodes)) {
228
+ if (node !== input) {
229
+ el.removeChild(node);
230
+ }
231
+ }
232
+ if (text) {
233
+ el.appendChild(document.createTextNode(text));
234
+ }
235
+ }
236
+
191
237
  function applyControlProps(el, type, props) {
192
238
  if (type === "Input") {
193
239
  el.setAttribute("type", props.secure ? "password" : "text");
@@ -201,9 +247,29 @@ function applyControlProps(el, type, props) {
201
247
  el.setAttribute("maxlength", String(props.max_length));
202
248
  }
203
249
  } else if (type === "Checkbox") {
204
- el.setAttribute("type", "checkbox");
250
+ const input = ensureCheckboxInput(el);
205
251
  if ("checked" in props) {
206
- el.checked = Boolean(props.checked);
252
+ input.checked = Boolean(props.checked);
253
+ }
254
+ // Lay the box and caption out on one line, as a block-level row so stacked
255
+ // checkboxes each get their own line (an inline default would let adjacent
256
+ // labels flow together in a non-flex parent). Defaults only — an explicit
257
+ // Style on the widget (applied just before this) wins. Re-applied here on
258
+ // every update because a `style` patch resets the element's inline cssText.
259
+ if (!el.style.display) {
260
+ el.style.display = "flex";
261
+ }
262
+ if (!el.style.alignItems) {
263
+ el.style.alignItems = "center";
264
+ }
265
+ if (!el.style.gap) {
266
+ el.style.gap = "0.4em";
267
+ }
268
+ // Size to the box + caption. Without this the <label>, as a flex item of a
269
+ // column, collapses to the input's width and the caption overflows onto the
270
+ // next row; fit-content keeps it a tidy one-line row instead of stretching.
271
+ if (!el.style.width) {
272
+ el.style.width = "fit-content";
207
273
  }
208
274
  } else if (type === "Image") {
209
275
  if ("src" in props && props.src != null) {
@@ -263,6 +329,8 @@ function applyUpdate(root, patch) {
263
329
  for (const key of patch.unset_props ?? []) {
264
330
  if (key === "style") {
265
331
  el.removeAttribute("style");
332
+ } else if (key === "label" && el.getAttribute(TYPE_ATTR) === "Checkbox") {
333
+ setCheckboxLabel(el, "");
266
334
  } else if (key === "content" || key === "label") {
267
335
  el.textContent = "";
268
336
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tempestweb"
3
- version = "0.5.2"
3
+ version = "0.5.3"
4
4
  description = "Build web apps in typed Python — one tree, a DOM renderer, two execution modes (WASM + server)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -2,7 +2,7 @@
2
2
  import { test } from "node:test";
3
3
  import assert from "node:assert/strict";
4
4
  import { fixture, freshDom } from "./setup.js";
5
- import { applyPatches, buildElement, KEY_ATTR } from "../../client/dom.js";
5
+ import { applyPatches, buildElement, KEY_ATTR, TYPE_ATTR } from "../../client/dom.js";
6
6
 
7
7
  /** Install jsdom's `document` globally so dom.js's `document.createElement` works. */
8
8
  function withDocument() {
@@ -160,7 +160,7 @@ test("a secure Input renders type=password", () => {
160
160
  assert.equal(el.getAttribute("type"), "password");
161
161
  });
162
162
 
163
- test("Checkbox builds an <input type=checkbox> reflecting checked", () => {
163
+ test("Checkbox builds a <label> wrapping a checkbox input plus visible caption", () => {
164
164
  withDocument();
165
165
  const el = buildElement({
166
166
  type: "Checkbox",
@@ -168,10 +168,29 @@ test("Checkbox builds an <input type=checkbox> reflecting checked", () => {
168
168
  props: { checked: true, label: "Agree" },
169
169
  children: [],
170
170
  });
171
- assert.equal(el.tagName, "INPUT");
172
- assert.equal(el.getAttribute("type"), "checkbox");
173
- assert.equal(el.checked, true);
174
- assert.equal(el.getAttribute("aria-label"), "Agree");
171
+ // The keyed, path-addressed element is the <label>; the real input is nested.
172
+ assert.equal(el.tagName, "LABEL");
173
+ assert.equal(el.getAttribute(TYPE_ATTR), "Checkbox");
174
+ const input = el.querySelector("input");
175
+ assert.equal(input.getAttribute("type"), "checkbox");
176
+ assert.equal(input.checked, true);
177
+ // The caption is visible text (the <label> gives the input its name natively).
178
+ assert.equal(el.textContent.trim(), "Agree");
179
+ });
180
+
181
+ test("Checkbox Update toggles checked and relabels without dropping the input", () => {
182
+ withDocument();
183
+ const el = buildElement({
184
+ type: "Checkbox",
185
+ key: "c",
186
+ props: { checked: false, label: "Old" },
187
+ children: [],
188
+ });
189
+ applyPatches(el, [{ path: [], set_props: { checked: true, label: "New" } }]);
190
+ const input = el.querySelector("input");
191
+ assert.equal(el.querySelectorAll("input").length, 1, "exactly one nested input");
192
+ assert.equal(input.checked, true);
193
+ assert.equal(el.textContent.trim(), "New");
175
194
  });
176
195
 
177
196
  test("Image builds an <img> with src/alt", () => {
File without changes
File without changes
File without changes
File without changes
File without changes