@adcops/autocore-react 3.5.7 → 3.5.11

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 (95) hide show
  1. package/dist/components/forms/useEditDraft.d.ts +49 -0
  2. package/dist/components/forms/useEditDraft.d.ts.map +1 -0
  3. package/dist/components/forms/useEditDraft.js +1 -0
  4. package/dist/components/index.d.ts +6 -0
  5. package/dist/components/index.d.ts.map +1 -1
  6. package/dist/components/index.js +1 -1
  7. package/dist/components/tis/TestDataView.d.ts +8 -0
  8. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  9. package/dist/components/tis/TestDataView.js +1 -1
  10. package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
  11. package/dist/components/tis/TestRawDataView.js +1 -1
  12. package/dist/components/tis/chartViews.d.ts +57 -0
  13. package/dist/components/tis/chartViews.d.ts.map +1 -0
  14. package/dist/components/tis/chartViews.js +1 -0
  15. package/dist/components/tis-editor/TisConfigEditor.css +11 -0
  16. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
  17. package/dist/components/tis-editor/TisConfigEditor.js +1 -1
  18. package/dist/components/tis-editor/editor/AuthoringContext.d.ts +37 -0
  19. package/dist/components/tis-editor/editor/AuthoringContext.d.ts.map +1 -0
  20. package/dist/components/tis-editor/editor/AuthoringContext.js +1 -0
  21. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +11 -0
  22. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -1
  23. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -1
  24. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
  25. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
  26. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -1
  27. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -1
  28. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +7 -0
  29. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  30. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  31. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
  32. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
  33. package/dist/components/tis-editor/types.d.ts +6 -0
  34. package/dist/components/tis-editor/types.d.ts.map +1 -1
  35. package/dist/components/varpick/VariableInput.d.ts +41 -0
  36. package/dist/components/varpick/VariableInput.d.ts.map +1 -0
  37. package/dist/components/varpick/VariableInput.js +1 -0
  38. package/dist/components/varpick/VariablePicker.d.ts +45 -0
  39. package/dist/components/varpick/VariablePicker.d.ts.map +1 -0
  40. package/dist/components/varpick/VariablePicker.js +1 -0
  41. package/dist/components/varpick/index.d.ts +16 -0
  42. package/dist/components/varpick/index.d.ts.map +1 -0
  43. package/dist/components/varpick/index.js +1 -0
  44. package/dist/components/varpick/tisCatalogs.d.ts +85 -0
  45. package/dist/components/varpick/tisCatalogs.d.ts.map +1 -0
  46. package/dist/components/varpick/tisCatalogs.js +1 -0
  47. package/dist/components/varpick/varpick.css +167 -0
  48. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  49. package/dist/core/AutoCoreTagContext.js +1 -1
  50. package/dist/core/AutoCoreTagTypes.d.ts +13 -0
  51. package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
  52. package/dist/hooks/index.d.ts +4 -0
  53. package/dist/hooks/index.d.ts.map +1 -1
  54. package/dist/hooks/index.js +1 -1
  55. package/dist/hooks/useGmVariables.d.ts +57 -0
  56. package/dist/hooks/useGmVariables.d.ts.map +1 -0
  57. package/dist/hooks/useGmVariables.js +1 -0
  58. package/dist/hooks/useSequenceProducers.d.ts +38 -0
  59. package/dist/hooks/useSequenceProducers.d.ts.map +1 -0
  60. package/dist/hooks/useSequenceProducers.js +1 -0
  61. package/package.json +7 -2
  62. package/src/components/forms/useEditDraft.ts +0 -0
  63. package/src/components/index.ts +20 -0
  64. package/src/components/tis/TestDataView.tsx +16 -10
  65. package/src/components/tis/TestRawDataView.tsx +6 -7
  66. package/src/components/tis/chartViews.ts +104 -0
  67. package/src/components/tis-editor/TisConfigEditor.css +11 -0
  68. package/src/components/tis-editor/TisConfigEditor.tsx +31 -0
  69. package/src/components/tis-editor/editor/AuthoringContext.tsx +43 -0
  70. package/src/components/tis-editor/editor/ChartViewDialog.tsx +48 -17
  71. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +1 -0
  72. package/src/components/tis-editor/editor/RawDataEditor.tsx +7 -2
  73. package/src/components/tis-editor/editor/TestFieldDialog.tsx +90 -23
  74. package/src/components/tis-editor/editor/ViewsEditor.tsx +56 -5
  75. package/src/components/tis-editor/types.ts +6 -0
  76. package/src/components/varpick/VariableInput.tsx +126 -0
  77. package/src/components/varpick/VariablePicker.tsx +168 -0
  78. package/src/components/varpick/index.ts +22 -0
  79. package/src/components/varpick/tisCatalogs.ts +235 -0
  80. package/src/components/varpick/varpick.css +167 -0
  81. package/src/core/AutoCoreTagContext.tsx +3 -1
  82. package/src/core/AutoCoreTagTypes.ts +14 -0
  83. package/src/hooks/index.ts +11 -0
  84. package/src/hooks/useGmVariables.ts +142 -0
  85. package/src/hooks/useSequenceProducers.ts +104 -0
  86. package/tools/tests/dist/TestFieldDialog-C4mKVh7q.js +32817 -0
  87. package/tools/tests/dist/autocore-react.css +257 -0
  88. package/tools/tests/dist/tis-editor.entry.js +9295 -0
  89. package/tools/tests/dist/varpick.entry.js +113 -0
  90. package/tools/tests/live-server.probe.mjs +172 -0
  91. package/tools/tests/tis-editor.entry.tsx +46 -0
  92. package/tools/tests/tis-editor.test.mjs +133 -0
  93. package/tools/tests/varpick.entry.tsx +81 -0
  94. package/tools/tests/varpick.test.mjs +208 -0
  95. package/tools/tests/vite.config.mjs +35 -0
@@ -0,0 +1,113 @@
1
+ import { Q as __toESM, X as require_client, Z as require_react, a as configSourceCatalog, d as require_jsx_runtime, i as chartFieldCatalog, n as AuthoringContext, o as cycleSourceCatalog, r as boundCatalog, t as TestFieldDialog } from "./TestFieldDialog-C4mKVh7q.js";
2
+ //#region src/hooks/useSequenceProducers.ts
3
+ var import_client = require_client();
4
+ var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
5
+ /** Every `store_value` name in a method, `cycle` bodies included. */
6
+ function collectStoredNames(method) {
7
+ const out = [];
8
+ const walk = (steps) => {
9
+ if (!Array.isArray(steps)) return;
10
+ for (const s of steps) {
11
+ if (!s || typeof s !== "object") continue;
12
+ if (s.type === "cycle") walk(s.body);
13
+ if (s.type === "store_value" && typeof s.name === "string" && s.name.trim() !== "") {
14
+ if (!out.includes(s.name)) out.push(s.name);
15
+ }
16
+ }
17
+ };
18
+ walk(method?.steps);
19
+ return out;
20
+ }
21
+ //#endregion
22
+ //#region tools/tests/varpick.entry.tsx
23
+ /**
24
+ * Fixture for `tools/tests/varpick.test.mjs`.
25
+ *
26
+ * Mounts a real `TestFieldDialog` under a real `AuthoringContext`, so the test
27
+ * drives the whole chain the way an author does: open the picker, search,
28
+ * select, Insert, and check what landed in the field. The pure catalog builders
29
+ * are exported alongside so the same run can assert on the lists themselves.
30
+ */
31
+ var import_jsx_runtime = require_jsx_runtime();
32
+ var VARIABLES = [
33
+ {
34
+ fqdn: "gm.press_position",
35
+ name: "press_position",
36
+ type: "f64",
37
+ description: "Press position, mm — datum-corrected by the motion engine",
38
+ quantity: "position",
39
+ ux: true,
40
+ nonvolatile: false,
41
+ link: "ethercat.motion.z.position",
42
+ writable: false,
43
+ is_signal: false
44
+ },
45
+ {
46
+ fqdn: "gm.press_preload_n",
47
+ name: "press_preload_n",
48
+ type: "f64",
49
+ description: "Seating (preload) force. NEGATIVE = compression.",
50
+ quantity: "force",
51
+ ux: true,
52
+ nonvolatile: true,
53
+ writable: true,
54
+ is_signal: false
55
+ },
56
+ {
57
+ fqdn: "gm.press_force_min_n",
58
+ name: "press_force_min_n",
59
+ type: "f64",
60
+ description: "Signed lower force bound for the fitted load cell.",
61
+ quantity: "force",
62
+ ux: true,
63
+ nonvolatile: false,
64
+ writable: true,
65
+ is_signal: false
66
+ }
67
+ ];
68
+ var PRODUCED = [
69
+ "preload_load",
70
+ "test_load",
71
+ "deflection"
72
+ ];
73
+ var saved = null;
74
+ var currentArray = "cycle_fields";
75
+ var setArray = () => {};
76
+ var Harness = () => {
77
+ const [arrayKey, setArrayKey] = (0, import_react.useState)("cycle_fields");
78
+ setArray = setArrayKey;
79
+ currentArray = arrayKey;
80
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthoringContext.Provider, {
81
+ value: {
82
+ variables: VARIABLES,
83
+ producedNames: PRODUCED
84
+ },
85
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TestFieldDialog, {
86
+ visible: true,
87
+ initial: {
88
+ name: "preload_load",
89
+ type: "f32"
90
+ },
91
+ siblingNames: ["preload_load", "test_load"],
92
+ isCycleField: arrayKey === "cycle_fields",
93
+ onCancel: () => {},
94
+ onSave: (f) => {
95
+ saved = f;
96
+ }
97
+ })
98
+ });
99
+ };
100
+ window.__mount = () => (0, import_client.createRoot)(document.getElementById("root")).render(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Harness, {}));
101
+ window.__saved = () => saved;
102
+ window.__setArray = (a) => setArray(a);
103
+ window.__currentArray = () => currentArray;
104
+ window.__catalogs = {
105
+ boundCatalog,
106
+ chartFieldCatalog,
107
+ configSourceCatalog,
108
+ cycleSourceCatalog,
109
+ collectStoredNames,
110
+ VARIABLES,
111
+ PRODUCED
112
+ };
113
+ //#endregion
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Live-server probe for the variable picker — `npm run probe:live`.
3
+ *
4
+ * NOT part of `npm test`: it needs a running `autocore_server`, so it is opt-in.
5
+ * What it covers that the jsdom tests cannot is the SEAM — the real
6
+ * `gm.get_variables` payload against the `GmVariable` type the client declares,
7
+ * and that payload fed through the published catalog builders. A field that the
8
+ * server renames or stops sending breaks the picker silently otherwise: the
9
+ * list just comes back empty or undescribed.
10
+ *
11
+ * ## Running it without hardware
12
+ *
13
+ * The debug binary reads `config.ubuntu.ini` from its CWD, so point a scratch
14
+ * copy at any project and run it there:
15
+ *
16
+ * mkdir -p /tmp/acsrv/projects && cd /tmp/acsrv
17
+ * cp -r <project> projects/ # a COPY: the server writes GNV + datastore
18
+ * cat > config.ubuntu.ini <<EOF
19
+ * [general]
20
+ * projects_directory=/tmp/acsrv/projects
21
+ * project_name=<project>
22
+ * port=8099
23
+ * ipc_port=9199
24
+ * disable_ads=1
25
+ * [modules]
26
+ * EOF
27
+ * /home/user/repos/autocore-server/target/debug/autocore_server
28
+ *
29
+ * Leave `[modules]` empty. Without hardware the ethercat/motion modules never
30
+ * connect and the control program is held — which does not matter here:
31
+ * `gm.get_variables` is served by the SERVER itself (gm_servelet), and `tis.*`
32
+ * is an always-on SYSTEM module that needs the datastore, not a machine.
33
+ *
34
+ * `tis`/`ams` are not declared in project.json — the server launches them
35
+ * itself, looking in `<server-exe-dir>/modules/`, then
36
+ * `../autocore-<name>/target/release/`, then PATH. A debug tree has neither, so
37
+ * symlink them once or the TIS half of this probe times out:
38
+ *
39
+ * mkdir -p autocore-server/target/debug/modules
40
+ * ln -s ../../../../autocore-tis/target/debug/autocore-tis \
41
+ * autocore-server/target/debug/modules/autocore-tis
42
+ * ln -s ../../../../autocore-ams/target/debug/autocore-ams \
43
+ * autocore-server/target/debug/modules/autocore-ams
44
+ *
45
+ * What still cannot be reached on a bench is `motion.*` — the motion manager
46
+ * starves its own request path retrying `configure_axes` against an ethercat
47
+ * that has no NIC. So the sequence editor's axis ENVELOPE (v_max/a_max/travel)
48
+ * is the one thing here that only hardware can confirm.
49
+ *
50
+ * Expects the server on 127.0.0.1:8080; edit the URL below for another port.
51
+ */
52
+
53
+ import WebSocket from 'ws';
54
+ import { gmCatalog, searchCatalog, allEntries } from '@adcops/autocore-varpick';
55
+
56
+ const ws = new WebSocket('ws://127.0.0.1:8080/ws/');
57
+ let id = 1; const pending = new Map();
58
+ const invoke = (topic, mt, data) => new Promise((res, rej) => {
59
+ const transaction_id = id++;
60
+ pending.set(transaction_id, res);
61
+ ws.send(JSON.stringify({ transaction_id, timecode: Date.now(), topic, message_type: mt, data, success: true, error_message: '' }));
62
+ setTimeout(() => { if (pending.delete(transaction_id)) rej(new Error(`timeout ${topic}`)); }, 8000);
63
+ });
64
+ ws.on('message', b => { let m; try { m = JSON.parse(b.toString()); } catch { return; }
65
+ const r = pending.get(m.transaction_id); if (r) { pending.delete(m.transaction_id); r(m); } });
66
+
67
+ let pass = 0;
68
+ const ok = (n, c, d = '') => { if (c) { pass++; console.log(`PASS ${n}`); }
69
+ else { console.error(`FAIL ${n} ${d}`); process.exitCode = 1; } };
70
+ const eq = (n, g, w) => ok(n, JSON.stringify(g) === JSON.stringify(w), `got ${JSON.stringify(g)} want ${JSON.stringify(w)}`);
71
+
72
+ ws.on('open', async () => {
73
+ try {
74
+ const resp = await invoke('gm.get_variables', 10, {});
75
+ ok('gm.get_variables succeeds', resp.success === true, resp.error_message);
76
+ const vars = resp.data.variables;
77
+ ok('returns the whole table', vars.length > 100, `n=${vars.length}`);
78
+
79
+ // -- the shape the TS GmVariable type declares --
80
+ const required = ['fqdn', 'name', 'type', 'description', 'ux', 'nonvolatile', 'writable', 'is_signal'];
81
+ const missing = vars.filter(v => required.some(k => v[k] === undefined));
82
+ eq('every row carries every non-optional field', missing.map(v => v.name), []);
83
+ ok('fqdn is the gm.-prefixed spelling', vars.every(v => v.fqdn === `gm.${v.name.toLowerCase()}`));
84
+ ok('sorted by name', vars.map(v => v.name).join() === vars.map(v => v.name).sort().join());
85
+
86
+ const by = n => vars.find(v => v.name === n);
87
+
88
+ // -- quantity survived the VariableConfig round-trip (it was NOT in the struct) --
89
+ eq('press_position.quantity', by('press_position')?.quantity, 'position');
90
+ eq('press_preload_n.quantity', by('press_preload_n')?.quantity, 'force');
91
+ // Exactly the 20 project.json declares — the point is that NONE are lost
92
+ // in the typed round-trip, which is what `quantity` used to do.
93
+ eq('every declared quantity survives', vars.filter(v => v.quantity).length, 20);
94
+
95
+ // -- writable is derived from link, which is what filters the config picker --
96
+ eq('a linked variable is not writable', by('press_position')?.writable, false);
97
+ eq('...and reports its producer', by('press_position')?.link, 'ethercat.motion.z.position');
98
+ eq('an unlinked setting is writable', by('press_preload_n')?.writable, true);
99
+ eq('signals are flagged', by('tick')?.is_signal, true);
100
+
101
+ // -- descriptions actually arrive: the whole point --
102
+ ok('press_position describes itself', /datum-corrected/.test(by('press_position')?.description ?? ''),
103
+ by('press_position')?.description);
104
+ const described = vars.filter(v => (v.description ?? '').trim()).length;
105
+ ok('nearly everything is described', described / vars.length > 0.9,
106
+ `${described}/${vars.length}`);
107
+
108
+ // -- the LIVE payload through the PUBLISHED catalog builders --
109
+ const unitFor = q => ({ position: 'mm', force: 'N' })[q];
110
+ const cfg = gmCatalog(vars, { spelling: 'fqdn', writableOnly: true, unitFor });
111
+ const bound = gmCatalog(vars, { spelling: 'token', unitFor });
112
+ ok('config picker drops hardware-owned variables',
113
+ !allEntries(cfg).some(e => e.name === 'press_position'));
114
+ ok('config picker keeps the operator settings',
115
+ allEntries(cfg).some(e => e.name === 'press_preload_n'));
116
+ eq('bound picker spells a token',
117
+ allEntries(bound).find(e => e.name === 'press_force_min_n')?.insert, '${gm.press_force_min_n}');
118
+ eq('units come through', allEntries(bound).find(e => e.name === 'press_position')?.unit, 'mm');
119
+ eq('groups present', bound.map(g => g.id), ['measured', 'settings', 'program', 'signals']);
120
+
121
+ // -- search over real data: find a thing by what it MEANS --
122
+ const hits = q => allEntries(searchCatalog(bound, q)).map(e => e.name);
123
+ ok('"EL3356" finds the load cell', hits('EL3356').includes('zforce_load'), hits('EL3356').join());
124
+ ok('"preload" finds the seating force', hits('preload').includes('press_preload_n'));
125
+ ok('a nonsense query finds nothing', hits('zzzznope').length === 0);
126
+
127
+ // ── the TIS half: both producers of a cycle column ──────────────────
128
+ //
129
+ // This is the pairing the cycle-field Source picker exists to show, and
130
+ // neither side checks the other at run time: `tis.add_cycle` writes
131
+ // `cycle_data` verbatim, so a declared column nothing stores is blank
132
+ // forever and a stored name with no column is recorded invisibly.
133
+ const tis = await invoke('tis.show_config', 10, {});
134
+ ok('tis.show_config succeeds', tis.success === true, tis.error_message);
135
+ const method = tis.data?.test_methods?.golf_compression;
136
+ ok('the method is served', !!method);
137
+
138
+ const declared = (method?.cycle_fields ?? []).map(f => f.name);
139
+ eq('cycle columns as declared', declared,
140
+ ['preload_load', 'preload_position', 'test_load', 'test_position', 'deflection']);
141
+ ok('none of them declares a GM source',
142
+ (method?.cycle_fields ?? []).every(f => !f.source),
143
+ 'on this machine the sequence fills them by name');
144
+
145
+ const seq = await invoke('datastore.methods/golf_compression.seq.json', 2, {});
146
+ ok('the sequence reads back', seq.success === true);
147
+ const steps = seq.data?.steps ?? [];
148
+ const produced = steps.filter(s => s.type === 'store_value').map(s => s.name);
149
+
150
+ // The contract the picker makes visible, asserted here against live data.
151
+ eq('every declared column has a producer', declared.filter(d => !produced.includes(d)), []);
152
+ eq('every produced name has a column', produced.filter(pn => !declared.includes(pn)), []);
153
+
154
+ // -- the rename actually reached the shipped method --
155
+ const triggers = steps.filter(s => s.type === 'scope_point').map(s => s.trigger.source);
156
+ eq('scope points read the renamed signal', [...new Set(triggers)], ['load']);
157
+ const exprs = steps.flatMap(s => (s.capture ?? []).map(c => c.expr));
158
+ ok('captures use the renamed signals', exprs.every(e => e === 'load' || e === 'position'),
159
+ exprs.join());
160
+ const retract = steps.find(s => s.kind === 'move_to_position');
161
+ eq('retract uses the renamed start position', retract?.params?.position, 'start_position');
162
+
163
+ // -- a live bound token resolves to a real variable --
164
+ const tl = (method?.config_fields ?? []).find(f => f.name === 'target_load');
165
+ eq('target_load keeps its token bound', tl?.min, '${gm.press_force_min_n}');
166
+ ok('...and that variable exists', vars.some(v => v.fqdn === 'gm.press_force_min_n'));
167
+
168
+ console.log(`\nlive server: ${pass} checks passed`);
169
+ } catch (e) { console.error('ERROR', e); process.exitCode = 1; }
170
+ ws.close(); process.exit(process.exitCode ?? 0);
171
+ });
172
+ ws.on('error', e => { console.error('WS ERROR', e.message); process.exit(1); });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Fixture for `tools/tests/tis-editor.test.mjs`.
3
+ *
4
+ * Mounts the two TIS-editor sections whose dialogs seed a draft from a record
5
+ * — the chart-view list and a field array — under a parent that can be made
6
+ * to re-render on demand. That re-render is the whole point: on a machine the
7
+ * editor sits under providers that re-render with live tag traffic, and the
8
+ * dialogs used to throw away the operator's typing every time one arrived.
9
+ */
10
+
11
+ import * as React from 'react';
12
+ import { useState } from 'react';
13
+ import { createRoot } from 'react-dom/client';
14
+ import { ViewsEditor } from '../../src/components/tis-editor/editor/ViewsEditor';
15
+ import { orderedViews } from '../../src/components/tis/chartViews';
16
+ import { FieldArrayEditor } from '../../src/components/tis-editor/editor/FieldArrayEditor';
17
+
18
+ const METHOD: any = {
19
+ label: 'golf', cycle_fields: [{ name: 'peak_load' }, { name: 'deflection' }],
20
+ views: {
21
+ load_vs_deflection: { title: 'Peak Load vs Deflection (per cycle)', type: 'cycle_scatter', x: { field: 'deflection', label: 'Deflection' }, y: [{ field: 'peak_load', label: 'Peak Load', y_axis: 'left' }] },
22
+ peak_load_by_cycle: { title: 'Peak Load by Cycle', type: 'cycle_scatter', x: { field: 'cycle_index', label: 'Cycle' }, y: [{ field: 'peak_load', label: 'Peak Load', y_axis: 'left' }] },
23
+ },
24
+ };
25
+
26
+ let forceParentRender: () => void = () => {};
27
+ let current: any = METHOD;
28
+
29
+ const Harness: React.FC = () => {
30
+ const [, setTick] = useState(0);
31
+ const [method, setMethod] = useState<any>(METHOD);
32
+ forceParentRender = () => setTick(t => t + 1);
33
+ current = method;
34
+ const onChange = (m: any) => { current = m; setMethod(m); };
35
+ return (
36
+ <>
37
+ <ViewsEditor method={method} onChange={onChange} />
38
+ <FieldArrayEditor arrayKey="cycle_fields" method={method} onChange={onChange} />
39
+ </>
40
+ );
41
+ };
42
+
43
+ (window as any).__mount = () => createRoot(document.getElementById('root')!).render(<Harness />);
44
+ (window as any).__forceParentRender = () => forceParentRender();
45
+ (window as any).__method = () => current;
46
+ (window as any).__order = () => orderedViews(current.views).map((v: any) => v.name);
@@ -0,0 +1,133 @@
1
+ /**
2
+ * TIS-editor regression test — run with `npm run test:tis-editor`.
3
+ *
4
+ * Covers two things that have no other guard:
5
+ *
6
+ * 1. An open dialog keeps the operator's edits across re-renders of the tree
7
+ * above it. The editors live under `AutoCoreTagProvider`/`TisProvider`,
8
+ * whose consumers re-render on live tag and broadcast traffic; a draft
9
+ * reseeded on prop identity is wiped a frame after every keystroke, which
10
+ * is what made the chart-view dialog read-only on a running machine
11
+ * (gen4_ac, 2026-08-29).
12
+ * 2. Chart views display in the author's `order`, and Move up / Move down
13
+ * renumbers it — the server hands `views` back id-sorted, so the order is
14
+ * carried per view rather than implied by the map.
15
+ *
16
+ * No test framework: jsdom + `react`'s `act`, driving the real components.
17
+ */
18
+
19
+ import { JSDOM } from 'jsdom';
20
+
21
+ const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', {
22
+ url: 'http://localhost/', pretendToBeVisual: true,
23
+ });
24
+ global.window = dom.window;
25
+ global.document = dom.window.document;
26
+ Object.defineProperty(global, "navigator", { value: dom.window.navigator, configurable: true });
27
+ global.HTMLElement = dom.window.HTMLElement;
28
+ global.Element = dom.window.Element;
29
+ global.Node = dom.window.Node;
30
+ global.Event = dom.window.Event;
31
+ global.KeyboardEvent = dom.window.KeyboardEvent;
32
+ global.MouseEvent = dom.window.MouseEvent;
33
+ global.getComputedStyle = dom.window.getComputedStyle;
34
+ global.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 0);
35
+ global.cancelAnimationFrame = (id) => clearTimeout(id);
36
+ global.IS_REACT_ACT_ENVIRONMENT = true;
37
+ dom.window.IS_REACT_ACT_ENVIRONMENT = true;
38
+ global.matchMedia = dom.window.matchMedia ?? (() => ({ matches: false, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){} }));
39
+ dom.window.confirm = () => true;
40
+
41
+ await import('./dist/tis-editor.entry.js');
42
+ const { act } = await import('react');
43
+
44
+ const tick = async () => { await act(async () => { await new Promise(r => setTimeout(r, 20)); }); };
45
+
46
+ await act(async () => { window.__mount(); });
47
+ await tick();
48
+
49
+ const q = (sel) => [...document.querySelectorAll(sel)];
50
+ const rowIds = () => [...q('tbody')[0].querySelectorAll('tr')].map(tr => tr.querySelector('td')?.textContent);
51
+ const rowButtons = (row) => [...q('tbody')[0].querySelectorAll('tr')[row].querySelectorAll('button')];
52
+ const byLabel = (row, label) => rowButtons(row).find(b => b.getAttribute('aria-label') === label);
53
+ const dialog = () => q('.p-dialog').at(-1);
54
+ const inputs = () => [...dialog().querySelectorAll('input')];
55
+ const setValue = (el, v) => {
56
+ const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value');
57
+ desc.set.call(el, v);
58
+ el.dispatchEvent(new dom.window.Event('input', { bubbles: true }));
59
+ };
60
+ const check = (name, got, want) => {
61
+ const ok = JSON.stringify(got) === JSON.stringify(want);
62
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name}: ${JSON.stringify(got)}${ok ? '' : ' want ' + JSON.stringify(want)}`);
63
+ if (!ok) process.exitCode = 1;
64
+ };
65
+
66
+ check('initial row order (id order, no `order` keys)', rowIds(), ['load_vs_deflection', 'peak_load_by_cycle']);
67
+
68
+ // ---- 1. edit an existing view, then force a parent re-render mid-edit
69
+ await act(async () => { byLabel(1, 'Edit').click(); });
70
+ await tick();
71
+ const titleInput = inputs()[1];
72
+ check('dialog seeded from the edited view', titleInput.value, 'Peak Load by Cycle');
73
+
74
+ await act(async () => { setValue(titleInput, 'Deflection by Cycle'); });
75
+ await tick();
76
+ check('typing lands', inputs()[1].value, 'Deflection by Cycle');
77
+
78
+ for (let i = 0; i < 5; i++) { await act(async () => { window.__forceParentRender(); }); await tick(); }
79
+ check('survives 5 parent re-renders', inputs()[1].value, 'Deflection by Cycle');
80
+
81
+ // also edit the Y series field, then re-render again
82
+ const yField = inputs().find(el => el.value === 'peak_load' && el.placeholder === 'field');
83
+ await act(async () => { setValue(yField, 'deflection'); });
84
+ await tick();
85
+ await act(async () => { window.__forceParentRender(); });
86
+ await tick();
87
+ check('series edit survives too', inputs().find(el => el.placeholder === 'field').value, 'deflection');
88
+
89
+ // Save
90
+ await act(async () => { [...dialog().querySelectorAll('button')].find(b => b.textContent.includes('Save')).click(); });
91
+ await tick();
92
+ const saved = window.__method().views.peak_load_by_cycle;
93
+ check('saved title', saved.title, 'Deflection by Cycle');
94
+ check('saved series field', saved.y[0].field, 'deflection');
95
+
96
+ // ---- 2. reorder
97
+ await act(async () => { byLabel(1, 'Move up').click(); });
98
+ await tick();
99
+ check('row order after Move up', rowIds(), ['peak_load_by_cycle', 'load_vs_deflection']);
100
+ check('order keys are dense', Object.fromEntries(Object.entries(window.__method().views).map(([k, v]) => [k, v.order])), { peak_load_by_cycle: 0, load_vs_deflection: 1 });
101
+ check('runtime picker agrees', window.__order(), ['peak_load_by_cycle', 'load_vs_deflection']);
102
+
103
+ // Move-up on the first row is disabled; move the first row down again
104
+ check('first row Move up disabled', byLabel(0, 'Move up').disabled, true);
105
+ check('last row Move down disabled', byLabel(1, 'Move down').disabled, true);
106
+ await act(async () => { byLabel(0, 'Move down').click(); });
107
+ await tick();
108
+ check('row order after Move down', rowIds(), ['load_vs_deflection', 'peak_load_by_cycle']);
109
+
110
+ // ---- 3. a new view lands last, not first
111
+ await act(async () => { [...document.querySelectorAll('button')].find(b => b.textContent.includes('Add view')).click(); });
112
+ await tick();
113
+ await act(async () => { setValue(inputs()[0], 'aaa_new_view'); });
114
+ await tick();
115
+ await act(async () => { window.__forceParentRender(); });
116
+ await tick();
117
+ check('new-view id survives a re-render', inputs()[0].value, 'aaa_new_view');
118
+ await act(async () => { [...dialog().querySelectorAll('button')].find(b => b.textContent.includes('Save')).click(); });
119
+ await tick();
120
+ check('new view sorts last despite its id', rowIds(), ['load_vs_deflection', 'peak_load_by_cycle', 'aaa_new_view']);
121
+
122
+ // ---- 4. the field dialog (same reseed class) — second DataTable on the page
123
+ const fieldRows = () => [...q('tbody')[1].querySelectorAll('tr')];
124
+ const fieldBtn = (row, label) => [...fieldRows()[row].querySelectorAll('button')].find(b => b.getAttribute('aria-label') === label);
125
+ check('field rows', fieldRows().map(tr => tr.querySelector('td')?.textContent), ['peak_load', 'deflection']);
126
+ await act(async () => { fieldBtn(0, 'Edit').click(); });
127
+ await tick();
128
+ const nameInput = inputs()[0];
129
+ check('field dialog seeded', nameInput.value, 'peak_load');
130
+ await act(async () => { setValue(nameInput, 'peak_load_n'); });
131
+ await tick();
132
+ for (let i = 0; i < 5; i++) { await act(async () => { window.__forceParentRender(); }); await tick(); }
133
+ check('field edit survives parent re-renders', inputs()[0].value, 'peak_load_n');
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Fixture for `tools/tests/varpick.test.mjs`.
3
+ *
4
+ * Mounts a real `TestFieldDialog` under a real `AuthoringContext`, so the test
5
+ * drives the whole chain the way an author does: open the picker, search,
6
+ * select, Insert, and check what landed in the field. The pure catalog builders
7
+ * are exported alongside so the same run can assert on the lists themselves.
8
+ */
9
+
10
+ import * as React from 'react';
11
+ import { useState } from 'react';
12
+ import { createRoot } from 'react-dom/client';
13
+ import { TestFieldDialog } from '../../src/components/tis-editor/editor/TestFieldDialog';
14
+ import { AuthoringContext } from '../../src/components/tis-editor/editor/AuthoringContext';
15
+ import {
16
+ boundCatalog,
17
+ chartFieldCatalog,
18
+ configSourceCatalog,
19
+ cycleSourceCatalog,
20
+ } from '../../src/components/varpick';
21
+ import { collectStoredNames } from '../../src/hooks/useSequenceProducers';
22
+ import type { GmVariable } from '@adcops/autocore-varpick';
23
+
24
+ const VARIABLES: GmVariable[] = [
25
+ {
26
+ fqdn: 'gm.press_position', name: 'press_position', type: 'f64',
27
+ description: 'Press position, mm — datum-corrected by the motion engine',
28
+ quantity: 'position', ux: true, nonvolatile: false,
29
+ link: 'ethercat.motion.z.position', writable: false, is_signal: false,
30
+ },
31
+ {
32
+ fqdn: 'gm.press_preload_n', name: 'press_preload_n', type: 'f64',
33
+ description: 'Seating (preload) force. NEGATIVE = compression.',
34
+ quantity: 'force', ux: true, nonvolatile: true, writable: true, is_signal: false,
35
+ },
36
+ {
37
+ fqdn: 'gm.press_force_min_n', name: 'press_force_min_n', type: 'f64',
38
+ description: 'Signed lower force bound for the fitted load cell.',
39
+ quantity: 'force', ux: true, nonvolatile: false, writable: true, is_signal: false,
40
+ },
41
+ ];
42
+
43
+ const PRODUCED = ['preload_load', 'test_load', 'deflection'];
44
+
45
+ let saved: any = null;
46
+ let currentArray: 'cycle_fields' | 'config_fields' = 'cycle_fields';
47
+ let setArray: (a: 'cycle_fields' | 'config_fields') => void = () => {};
48
+
49
+ const Harness: React.FC = () => {
50
+ const [arrayKey, setArrayKey] = useState<'cycle_fields' | 'config_fields'>('cycle_fields');
51
+ setArray = setArrayKey;
52
+ currentArray = arrayKey;
53
+ return (
54
+ <AuthoringContext.Provider value={{ variables: VARIABLES, producedNames: PRODUCED }}>
55
+ <TestFieldDialog
56
+ visible
57
+ initial={{ name: 'preload_load', type: 'f32' }}
58
+ siblingNames={['preload_load', 'test_load']}
59
+ isCycleField={arrayKey === 'cycle_fields'}
60
+ onCancel={() => {}}
61
+ onSave={(f) => { saved = f; }}
62
+ />
63
+ </AuthoringContext.Provider>
64
+ );
65
+ };
66
+
67
+ (window as any).__mount = () => createRoot(document.getElementById('root')!).render(<Harness />);
68
+ (window as any).__saved = () => saved;
69
+ (window as any).__setArray = (a: 'cycle_fields' | 'config_fields') => setArray(a);
70
+ (window as any).__currentArray = () => currentArray;
71
+
72
+ // Pure builders, for the assertions that need no DOM.
73
+ (window as any).__catalogs = {
74
+ boundCatalog,
75
+ chartFieldCatalog,
76
+ configSourceCatalog,
77
+ cycleSourceCatalog,
78
+ collectStoredNames,
79
+ VARIABLES,
80
+ PRODUCED,
81
+ };