|
| 1 | +## Test Cases |
| 2 | + |
| 3 | +Most findings are internal cleanups; some are structurally verifiable by unit test, |
| 4 | +others (B5 packaging, B7 comment) are verified by build/inspection and carry no test. |
| 5 | +The central net-new coverage is **B6** (`EditableChartStore`), which also acts as the |
| 6 | +regression net for **B4** (data round-trips through the store before the Plotly cast). |
| 7 | + |
| 8 | +Test files: |
| 9 | + |
| 10 | +- `packages/shared/charts/src/model/stores/__tests__/EditableChart.store.spec.ts` (new — B6) |
| 11 | +- `packages/pluggableWidgets/custom-chart-web/src/hooks/__tests__/useCustomChart.spec.ts` (new — B1/B2/B3) |
| 12 | +- `packages/pluggableWidgets/chart-playground-web/src/helpers/__tests__/prettifyJson.spec.ts` (new — B9) |
| 13 | + |
| 14 | +### Reproduction Tests |
| 15 | + |
| 16 | +- **B3 — playgroundData carries real layout/config options** (unit) |
| 17 | + - **Given**: `useCustomChart` rendered with a `CustomChartContainerProps` whose adapter |
| 18 | + parses a non-empty `layoutStatic` / `configurationOptions` (e.g. `{ "title": "X" }` / |
| 19 | + `{ "displaylogo": true }`). |
| 20 | + - **When**: the hook returns `playgroundData`. |
| 21 | + - **Then**: `playgroundData.layoutOptions` equals the adapter's `layout` (contains |
| 22 | + `title: "X"`) and `playgroundData.configOptions` equals the adapter's `config` |
| 23 | + (contains `displaylogo: true`) — **not** `{}`. |
| 24 | + |
| 25 | +- **B6 — store reset() loads props into layout/config/data** (unit) |
| 26 | + - **Given**: a fresh `EditableChartStore` wired to a `ComputedAtom` whose props are |
| 27 | + `{ layout: { a: 1 }, config: { b: 2 }, data: [{ x: [1] }] }`. |
| 28 | + - **When**: `setup()` autorun runs (or `reset` is called directly). |
| 29 | + - **Then**: `store.layout`, `store.config`, `store.data` equal those inputs. |
| 30 | + |
| 31 | +### Edge Cases |
| 32 | + |
| 33 | +- **B6 — setDataAt updates a valid index with valid JSON** (unit) |
| 34 | + - **Given**: store with `data = [{ x: [1] }, { x: [2] }]`. |
| 35 | + - **When**: `setDataAt(1, '{"x":[9]}')`. |
| 36 | + - **Then**: `store.data[1]` deep-equals `{ x: [9] }`; index 0 unchanged; `data` is a new |
| 37 | + array reference (observable.ref replaced, not mutated). |
| 38 | + |
| 39 | +- **B6 — setDataAt ignores out-of-range index** (unit) |
| 40 | + - **Given**: store with `data` of length 2. |
| 41 | + - **When**: `setDataAt(5, '{"x":[9]}')` and `setDataAt(-1, '{"x":[9]}')`. |
| 42 | + - **Then**: `store.data` is unchanged (no throw). |
| 43 | + |
| 44 | +- **B6 — setDataAt swallows invalid JSON and warns** (unit) |
| 45 | + - **Given**: store with a valid `data` array; `console.warn` spied. |
| 46 | + - **When**: `setDataAt(0, "{ not json ")`. |
| 47 | + - **Then**: `store.data` unchanged, no throw, `console.warn` called once. |
| 48 | + |
| 49 | +- **B6 — setDataAt rejects non-object JSON (array / primitive)** (unit) |
| 50 | + - **Given**: store with valid `data`. |
| 51 | + - **When**: `setDataAt(0, "[1,2,3]")` and `setDataAt(0, "42")`. |
| 52 | + - **Then**: `store.data` unchanged (guard requires a non-array object). |
| 53 | + |
| 54 | +- **B6 — setLayout / setConfig ignore null, replace on object** (unit) |
| 55 | + - **Given**: store with `layout = { a: 1 }`. |
| 56 | + - **When**: `setLayout(null as any)` then `setLayout({ c: 3 })`. |
| 57 | + - **Then**: after null, `layout` unchanged; after object, `layout` deep-equals `{ c: 3 }` |
| 58 | + and is a fresh reference. Same for `setConfig`. |
| 59 | + |
| 60 | +- **B6 — JSON getters serialize current state** (unit) |
| 61 | + - **Given**: store with `layout = { a: 1 }`, `config = { b: 2 }`, `data = [{ x: [1] }]`. |
| 62 | + - **When**: read `layoutJson`, `configJson`, `dataJson`. |
| 63 | + - **Then**: `layoutJson === '{"a":1}'`, `configJson === '{"b":2}'`, |
| 64 | + `dataJson` deep-equals `['{"x":[1]}']`. |
| 65 | + |
| 66 | +- **B9 — shared prettifyJson formats valid and flags invalid** (unit) |
| 67 | + - **Given**: the extracted `prettifyJson` helper. |
| 68 | + - **When**: called with `'{"a":1}'` and with `"{ bad"`. |
| 69 | + - **Then**: valid input returns 2-space-indented JSON (`'{\n "a": 1\n}'`); invalid input |
| 70 | + returns `'{ "error": "invalid JSON" }'`. |
| 71 | + |
| 72 | +### Regression Tests |
| 73 | + |
| 74 | +- **B1 — playgroundData is a plain reactive object, no stale caching** (unit) |
| 75 | + - **Given**: `useCustomChart` rendered inside an `observer` (mirrors `CustomChart.tsx`). |
| 76 | + - **When**: the store's `data` changes (e.g. via `setDataAt`) and the component re-renders. |
| 77 | + - **Then**: `playgroundData.plotData` reflects the new `store.data` (reactivity preserved |
| 78 | + after removing the `computed(...).get()` wrapper). `playgroundData.type === "editor.data.v2"`. |
| 79 | + |
| 80 | +- **B2 — hook return shape has no containerStyle** (unit) |
| 81 | + - **Given**: `useCustomChart` rendered. |
| 82 | + - **When**: inspect the returned object keys. |
| 83 | + - **Then**: keys are exactly `{ playgroundData, ref }`; `containerStyle` absent. (Compile-time |
| 84 | + guarantee via `UseCustomChartReturn`; asserted at runtime as regression guard.) |
| 85 | + |
| 86 | +- **B4 — store data round-trips into Plotly Data via the boundary helper** (unit) |
| 87 | + - **Given**: `store.data = [{ type: "bar", x: [1], y: [2] }]`. |
| 88 | + - **When**: mapped through `toPlotlyData(store.data)`. |
| 89 | + - **Then**: output deep-equals the input traces (identity mapping, correctly typed as |
| 90 | + `Data[]`) — no data dropped or reshaped. |
| 91 | + |
| 92 | +- **B10 — onViewSelectChange keeps a stable identity across renders** (unit) |
| 93 | + - **Given**: `useV2EditorController` (and `useComposedEditorController`) rendered. |
| 94 | + - **When**: the component re-renders without `store`/`key` changing. |
| 95 | + - **Then**: the `onViewSelectChange` reference is unchanged between renders (memoized like |
| 96 | + `onEditorChange`). |
| 97 | + |
| 98 | +- **B11 — single source of truth for active key in V2 controller** (unit) |
| 99 | + - **Given**: `useV2EditorController` rendered. |
| 100 | + - **When**: `onViewSelectChange` selects `"config"`, then trace index `0`. |
| 101 | + - **Then**: `viewSelectValue` and the editor code both follow the selected key with no |
| 102 | + desync; changing the key updates the displayed code (the MobX reaction still fires from |
| 103 | + the single retained key representation). |
| 104 | + |
| 105 | +## Notes |
| 106 | + |
| 107 | +- **B5** (`@types/jest` → devDependencies, drop from `tsconfig.build.json`) and **B7** |
| 108 | + (document the `set-state-in-effect` suppression): no unit test — verified by |
| 109 | + `pnpm --filter @mendix/shared-charts build` succeeding and code inspection. Track in tasks.md. |
| 110 | +- **B8** (silent `catch {}` in the editor controllers): **out of scope**, deferred to WC-3348. |
| 111 | +- **B10/B11**: if a memoization/identity assertion proves brittle in RTL, fall back to |
| 112 | + asserting observable behavior (no desync, correct code shown) rather than reference equality. |
| 113 | +- The `EditableChartStore` needs a `SetupComponentHost` + `ComputedAtom` test harness; check |
| 114 | + `@mendix/widget-plugin-mobx-kit` for an existing test helper before hand-rolling one. |
0 commit comments