hazo_ui 3.2.0 → 3.3.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/CHANGE_LOG.md CHANGED
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v3.3.0 — 2026-06-11
9
+
10
+ **New:** Required `doc` field on `Case` + per-case documentation accordion in `AutoTestRunner`.
11
+
12
+ Every test case registered via `registerScenario` must now declare a `doc: CaseDoc` with four
13
+ required string fields: `description`, `inputs`, `expectedOutputs`, and `caveats`. The field is
14
+ enforced at the type level — a case without `doc` fails `tsc`. All 20 existing scenario files
15
+ across the hazo workspace have been backfilled in this release.
16
+
17
+ The `AutoTestRunner` renders a `▸/▾` caret toggle next to each case name. Clicking it expands a
18
+ doc panel showing the four fields as labeled sections (Description / Inputs / Expected outputs /
19
+ Caveats). Failed-case error output and "Copy prompt" behaviour are unchanged.
20
+
21
+ **Exports added:** `CaseDoc` is now part of the public `hazo_ui/test-harness` surface.
22
+
23
+ ```ts
24
+ import { registerScenario, type CaseDoc } from "hazo_ui/test-harness";
25
+
26
+ registerScenario("my-pkg", {
27
+ name: "My Package",
28
+ pkg: "my-pkg",
29
+ cases: [
30
+ {
31
+ name: "creates a record",
32
+ doc: {
33
+ description: "Verifies that createRecord() inserts a row and returns the new ID.",
34
+ inputs: "A mock DB adapter seeded with an empty table; payload { title: 'hello' }.",
35
+ expectedOutputs: "Resolved string ID, table row count increases to 1.",
36
+ caveats: "None",
37
+ },
38
+ run: async () => { /* ... */ },
39
+ },
40
+ ],
41
+ });
42
+ ```
43
+
8
44
  ## v3.2.0 — 2026-05-31
9
45
 
10
46
  **New:** `MarkdownEditor` — a generic, SSR-safe Markdown/MDX editor.
package/README.md CHANGED
@@ -3920,13 +3920,18 @@ export default function RootLayout({ children }) {
3920
3920
  // test-app/scenarios/my_feature.ts
3921
3921
  import { registerScenario, assertEqual } from 'hazo_ui/test-harness';
3922
3922
 
3923
- registerScenario({
3924
- id: 'my_feature',
3923
+ registerScenario('my_feature', {
3925
3924
  name: 'My Feature',
3926
3925
  pkg: 'my_pkg',
3927
3926
  cases: [
3928
3927
  {
3929
3928
  name: 'returns correct value',
3929
+ doc: {
3930
+ description: 'Verifies that myFunction adds two numbers correctly.',
3931
+ inputs: 'myFunction(1, 2)',
3932
+ expectedOutputs: 'Returns 3.',
3933
+ caveats: 'None',
3934
+ },
3930
3935
  run: async () => {
3931
3936
  const result = myFunction(1, 2);
3932
3937
  assertEqual(result, 3, 'should add two numbers');
@@ -3934,6 +3939,12 @@ registerScenario({
3934
3939
  },
3935
3940
  {
3936
3941
  name: 'throws on invalid input',
3942
+ doc: {
3943
+ description: 'Verifies that myFunction rejects null as the first argument.',
3944
+ inputs: 'myFunction(null, 2)',
3945
+ expectedOutputs: 'Throws an error containing "invalid input".',
3946
+ caveats: 'None',
3947
+ },
3937
3948
  run: async () => {
3938
3949
  await assertThrows(() => myFunction(null, 2), 'invalid input');
3939
3950
  },
@@ -3954,6 +3965,22 @@ export default function MyFeaturePage() {
3954
3965
  }
3955
3966
  ```
3956
3967
 
3968
+ ### Case documentation (`doc` — required)
3969
+
3970
+ Every case **must** include a `doc: CaseDoc` field. This is enforced at the TypeScript level — omitting it is a compile error. The `AutoTestRunner` surfaces it as a `▸/▾` per-case accordion so reviewers can read what each test verifies without opening the source file.
3971
+
3972
+ ```ts
3973
+ import type { CaseDoc } from 'hazo_ui/test-harness';
3974
+
3975
+ // All four fields are required strings:
3976
+ const doc: CaseDoc = {
3977
+ description: 'What this test verifies, in plain language.',
3978
+ inputs: 'Inputs / preconditions fed in (URL, payload, fixture, state).',
3979
+ expectedOutputs: 'What is asserted on success.',
3980
+ caveats: 'None', // use "None" when nothing notable applies
3981
+ };
3982
+ ```
3983
+
3957
3984
  ### Copying failures as a Claude prompt
3958
3985
 
3959
3986
  `CopyAllFailuresButton` copies all failed cases as a structured prompt with 8 sections (what-went-wrong, expected/actual/diff, test code, code under test, error chain, context, ring buffer). Place it in your sidebar or test page header.
@@ -258,8 +258,9 @@ import {
258
258
  // Import celebration (v2.18.0)
259
259
  import { CelebrationProvider, celebrate, CELEBRATION_GRADIENT } from 'hazo_ui';
260
260
 
261
- // Import test harness (v3.0.0) — test-app only, never in production bundles
262
- import { AutoTestProvider, AutoTestRunner, SidebarLayout, AppSidebar, registerScenario, assertEqual } from 'hazo_ui/test-harness';
261
+ // Import test harness (v3.3.0) — test-app only, never in production bundles
262
+ // Note: every Case must include a `doc: CaseDoc` field (required since v3.3.0)
263
+ import { AutoTestProvider, AutoTestRunner, SidebarLayout, AppSidebar, registerScenario, assertEqual, type CaseDoc } from 'hazo_ui/test-harness';
263
264
  ```
264
265
 
265
266
  **Toaster setup**: Mount `<HazoUiToaster />` once at the root of your app (e.g., in `layout.tsx`) so `successToast()` / `errorToast()` calls have somewhere to render.
@@ -4,7 +4,7 @@ var React = require('react');
4
4
  var clsx = require('clsx');
5
5
  var tailwindMerge = require('tailwind-merge');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
- var hazo_core = require('hazo_core');
7
+ var client = require('hazo_core/client');
8
8
 
9
9
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
10
 
@@ -152,6 +152,7 @@ function build_initial_state() {
152
152
  status: "pending",
153
153
  cases: scenario.cases.map((c) => ({
154
154
  name: c.name,
155
+ doc: c.doc,
155
156
  status: "pending"
156
157
  }))
157
158
  });
@@ -681,7 +682,7 @@ ${ctx_lines.join("\n")}`);
681
682
 
682
683
  `;
683
684
  try {
684
- const hazo_logs = await hazo_core.optional_import("hazo_logs");
685
+ const hazo_logs = await client.optional_import("hazo_logs");
685
686
  const get_ring = hazo_logs?.["getRingBuffer"];
686
687
  if (!hazo_logs || typeof get_ring !== "function") {
687
688
  ring_section += "ring buffer not available (hazo_logs >= 2.0.0 required)";
@@ -860,6 +861,63 @@ function CopySinglePromptButton({
860
861
  }
861
862
  );
862
863
  }
864
+ function CaseRow({
865
+ c,
866
+ scenario_id,
867
+ pkg
868
+ }) {
869
+ const [expanded, set_expanded] = React.useState(false);
870
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-2", children: [
871
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
872
+ c.doc && /* @__PURE__ */ jsxRuntime.jsx(
873
+ "button",
874
+ {
875
+ onClick: () => set_expanded((v) => !v),
876
+ className: "text-gray-400 hover:text-gray-600 text-xs font-mono w-3 shrink-0",
877
+ "aria-label": expanded ? "Collapse doc" : "Expand doc",
878
+ children: expanded ? "\u25BE" : "\u25B8"
879
+ }
880
+ ),
881
+ !c.doc && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-3 shrink-0" }),
882
+ /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { status: c.status }),
883
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
884
+ "flex-1",
885
+ c.status === "failed" ? "text-red-700" : "text-gray-700"
886
+ ), children: c.name }),
887
+ c.durationMs != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-400", children: [
888
+ c.durationMs,
889
+ "ms"
890
+ ] }),
891
+ c.status === "failed" && c.error && /* @__PURE__ */ jsxRuntime.jsx(
892
+ CopySinglePromptButton,
893
+ {
894
+ scenario_id,
895
+ case_result: c,
896
+ pkg
897
+ }
898
+ )
899
+ ] }),
900
+ expanded && c.doc && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 ml-8 text-xs rounded border border-gray-100 bg-gray-50 divide-y divide-gray-100", children: [
901
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2", children: [
902
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-gray-600", children: "Description" }),
903
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-gray-700", children: c.doc.description })
904
+ ] }),
905
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2", children: [
906
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-gray-600", children: "Inputs" }),
907
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-gray-700", children: c.doc.inputs })
908
+ ] }),
909
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2", children: [
910
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-gray-600", children: "Expected outputs" }),
911
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-gray-700", children: c.doc.expectedOutputs })
912
+ ] }),
913
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2", children: [
914
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-semibold text-gray-600", children: "Caveats" }),
915
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-gray-700", children: c.doc.caveats })
916
+ ] })
917
+ ] }),
918
+ c.status === "failed" && c.error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 ml-8 text-xs text-red-600 bg-red-50 rounded px-2 py-1 font-mono", children: c.error.message })
919
+ ] });
920
+ }
863
921
  function ScenarioRow({
864
922
  scenario,
865
923
  pkg
@@ -901,28 +959,7 @@ function ScenarioRow({
901
959
  }
902
960
  )
903
961
  ] }),
904
- expanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y divide-gray-100", children: scenario.cases.map((c, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-2", children: [
905
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
906
- /* @__PURE__ */ jsxRuntime.jsx(StatusIcon, { status: c.status }),
907
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn(
908
- "flex-1",
909
- c.status === "failed" ? "text-red-700" : "text-gray-700"
910
- ), children: c.name }),
911
- c.durationMs != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-gray-400", children: [
912
- c.durationMs,
913
- "ms"
914
- ] }),
915
- c.status === "failed" && c.error && /* @__PURE__ */ jsxRuntime.jsx(
916
- CopySinglePromptButton,
917
- {
918
- scenario_id: scenario.id,
919
- case_result: c,
920
- pkg
921
- }
922
- )
923
- ] }),
924
- c.status === "failed" && c.error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 ml-5 text-xs text-red-600 bg-red-50 rounded px-2 py-1 font-mono", children: c.error.message })
925
- ] }, i)) })
962
+ expanded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y divide-gray-100", children: scenario.cases.map((c, i) => /* @__PURE__ */ jsxRuntime.jsx(CaseRow, { c, scenario_id: scenario.id, pkg }, i)) })
926
963
  ] });
927
964
  }
928
965
  function AutoTestRunner() {