hazo_ui 3.2.1 → 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 +36 -0
- package/README.md +29 -2
- package/SETUP_CHECKLIST.md +3 -2
- package/dist/test-harness/index.cjs +61 -24
- package/dist/test-harness/index.cjs.map +1 -1
- package/dist/test-harness/index.d.cts +38 -26
- package/dist/test-harness/index.d.ts +38 -26
- package/dist/test-harness/index.js +60 -23
- package/dist/test-harness/index.js.map +1 -1
- package/package.json +3 -3
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.
|
package/SETUP_CHECKLIST.md
CHANGED
|
@@ -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.
|
|
262
|
-
|
|
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
|
|
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
|
|
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.
|
|
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() {
|