@adcops/autocore-react 3.5.9 → 3.5.13

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 (76) hide show
  1. package/dist/components/index.d.ts +4 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/components/tis-editor/TisConfigEditor.css +11 -0
  5. package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
  6. package/dist/components/tis-editor/TisConfigEditor.js +1 -1
  7. package/dist/components/tis-editor/editor/AuthoringContext.d.ts +37 -0
  8. package/dist/components/tis-editor/editor/AuthoringContext.d.ts.map +1 -0
  9. package/dist/components/tis-editor/editor/AuthoringContext.js +1 -0
  10. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +11 -0
  11. package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -1
  12. package/dist/components/tis-editor/editor/ChartViewDialog.js +1 -1
  13. package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
  14. package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
  15. package/dist/components/tis-editor/editor/RawDataEditor.d.ts.map +1 -1
  16. package/dist/components/tis-editor/editor/RawDataEditor.js +1 -1
  17. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +7 -0
  18. package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
  19. package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
  20. package/dist/components/tis-editor/editor/ViewsEditor.d.ts.map +1 -1
  21. package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
  22. package/dist/components/varpick/VariableInput.d.ts +41 -0
  23. package/dist/components/varpick/VariableInput.d.ts.map +1 -0
  24. package/dist/components/varpick/VariableInput.js +1 -0
  25. package/dist/components/varpick/VariablePicker.d.ts +45 -0
  26. package/dist/components/varpick/VariablePicker.d.ts.map +1 -0
  27. package/dist/components/varpick/VariablePicker.js +1 -0
  28. package/dist/components/varpick/index.d.ts +16 -0
  29. package/dist/components/varpick/index.d.ts.map +1 -0
  30. package/dist/components/varpick/index.js +1 -0
  31. package/dist/components/varpick/tisCatalogs.d.ts +85 -0
  32. package/dist/components/varpick/tisCatalogs.d.ts.map +1 -0
  33. package/dist/components/varpick/tisCatalogs.js +1 -0
  34. package/dist/components/varpick/varpick.css +167 -0
  35. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  36. package/dist/core/AutoCoreTagContext.js +1 -1
  37. package/dist/core/AutoCoreTagTypes.d.ts +13 -0
  38. package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
  39. package/dist/hooks/index.d.ts +4 -0
  40. package/dist/hooks/index.d.ts.map +1 -1
  41. package/dist/hooks/index.js +1 -1
  42. package/dist/hooks/useGmVariables.d.ts +57 -0
  43. package/dist/hooks/useGmVariables.d.ts.map +1 -0
  44. package/dist/hooks/useGmVariables.js +1 -0
  45. package/dist/hooks/useSequenceProducers.d.ts +38 -0
  46. package/dist/hooks/useSequenceProducers.d.ts.map +1 -0
  47. package/dist/hooks/useSequenceProducers.js +1 -0
  48. package/package.json +5 -2
  49. package/src/components/index.ts +17 -0
  50. package/src/components/tis-editor/TisConfigEditor.css +11 -0
  51. package/src/components/tis-editor/TisConfigEditor.tsx +31 -0
  52. package/src/components/tis-editor/editor/AuthoringContext.tsx +43 -0
  53. package/src/components/tis-editor/editor/ChartViewDialog.tsx +24 -7
  54. package/src/components/tis-editor/editor/FieldArrayEditor.tsx +1 -0
  55. package/src/components/tis-editor/editor/RawDataEditor.tsx +7 -2
  56. package/src/components/tis-editor/editor/TestFieldDialog.tsx +66 -12
  57. package/src/components/tis-editor/editor/ViewsEditor.tsx +25 -0
  58. package/src/components/varpick/VariableInput.tsx +126 -0
  59. package/src/components/varpick/VariablePicker.tsx +168 -0
  60. package/src/components/varpick/index.ts +22 -0
  61. package/src/components/varpick/tisCatalogs.ts +235 -0
  62. package/src/components/varpick/varpick.css +167 -0
  63. package/src/core/AutoCoreTagContext.tsx +3 -1
  64. package/src/core/AutoCoreTagTypes.ts +14 -0
  65. package/src/hooks/index.ts +11 -0
  66. package/src/hooks/useGmVariables.ts +142 -0
  67. package/src/hooks/useSequenceProducers.ts +104 -0
  68. package/tools/tests/dist/TestFieldDialog-C4mKVh7q.js +32817 -0
  69. package/tools/tests/dist/autocore-react.css +257 -0
  70. package/tools/tests/dist/tis-editor.entry.js +8431 -40567
  71. package/tools/tests/dist/varpick.entry.js +113 -0
  72. package/tools/tests/live-server.probe.mjs +172 -0
  73. package/tools/tests/varpick.entry.tsx +81 -0
  74. package/tools/tests/varpick.test.mjs +208 -0
  75. package/tools/tests/vite.config.mjs +7 -2
  76. package/tools/tests/dist/tis-editor.entry.css +0 -90
@@ -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,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
+ };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Variable-picker regression test — `npm run test:varpick`.
3
+ *
4
+ * Two halves:
5
+ *
6
+ * 1. The catalogs. Each field in the method editor wants a different list,
7
+ * spelled differently, and every way of getting it wrong fails SILENTLY —
8
+ * a `source` that names nothing never binds, a bound that resolves to
9
+ * nothing is simply unlimited, a cycle column with no producer is blank on
10
+ * every row forever.
11
+ * 2. The dialog, driven end to end in jsdom: open the picker, search, select,
12
+ * Insert, and check what landed in the field. That is the part a unit test
13
+ * of the model cannot cover — it is where the spelling is chosen.
14
+ */
15
+
16
+ import { JSDOM } from 'jsdom';
17
+
18
+ const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', {
19
+ url: 'http://localhost/', pretendToBeVisual: true,
20
+ });
21
+ global.window = dom.window;
22
+ global.document = dom.window.document;
23
+ Object.defineProperty(global, 'navigator', { value: dom.window.navigator, configurable: true });
24
+ global.HTMLElement = dom.window.HTMLElement;
25
+ global.Element = dom.window.Element;
26
+ global.Node = dom.window.Node;
27
+ global.Event = dom.window.Event;
28
+ global.KeyboardEvent = dom.window.KeyboardEvent;
29
+ global.MouseEvent = dom.window.MouseEvent;
30
+ global.getComputedStyle = dom.window.getComputedStyle;
31
+ global.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 0);
32
+ global.cancelAnimationFrame = (id) => clearTimeout(id);
33
+ global.IS_REACT_ACT_ENVIRONMENT = true;
34
+ dom.window.IS_REACT_ACT_ENVIRONMENT = true;
35
+ global.matchMedia = dom.window.matchMedia ?? (() => ({
36
+ matches: false, addListener() {}, removeListener() {},
37
+ addEventListener() {}, removeEventListener() {},
38
+ }));
39
+
40
+ await import('./dist/varpick.entry.js');
41
+ const { act } = await import('react');
42
+
43
+ const tick = async () => { await act(async () => { await new Promise(r => setTimeout(r, 20)); }); };
44
+
45
+ let passed = 0;
46
+ const check = (name, got, want) => {
47
+ const g = JSON.stringify(got);
48
+ const w = JSON.stringify(want);
49
+ if (g === w) { passed++; console.log(`PASS ${name}: ${g}`); }
50
+ else { console.error(`FAIL ${name}\n got ${g}\n want ${w}`); process.exitCode = 1; }
51
+ };
52
+ const ok = (name, cond, detail = '') => {
53
+ if (cond) { passed++; console.log(`PASS ${name}`); }
54
+ else { console.error(`FAIL ${name} ${detail}`); process.exitCode = 1; }
55
+ };
56
+
57
+ // ── 1. the catalogs ─────────────────────────────────────────────────────────
58
+
59
+ const C = window.__catalogs;
60
+ const ctx = { variables: C.VARIABLES, unitFor: (q) => ({ position: 'mm', force: 'N' })[q] };
61
+ const flat = (cat) => cat.flatMap(g => g.entries);
62
+ const inserts = (cat) => flat(cat).map(e => e.insert);
63
+ const groupIds = (cat) => cat.map(g => g.id);
64
+
65
+ {
66
+ // A config field's Source is WRITTEN by the setup form, so a variable a
67
+ // module republishes every scan must not be offered: the operator's entry
68
+ // would be overwritten before anything read it.
69
+ const cat = C.configSourceCatalog(ctx);
70
+ check('config source omits hardware-owned variables', inserts(cat),
71
+ ['gm.press_preload_n', 'gm.press_force_min_n']);
72
+ ok('config source spells an FQDN', inserts(cat).every(i => i.startsWith('gm.')));
73
+
74
+ const bounds = C.boundCatalog(ctx);
75
+ check('a bound is spelled as a token', inserts(bounds).slice(0, 1), ['${gm.press_position}']);
76
+ ok('a bound may read a hardware-owned variable',
77
+ inserts(bounds).includes('${gm.press_position}'));
78
+ check('units come from the machine, not the name',
79
+ flat(bounds).find(e => e.name === 'press_position').unit, 'mm');
80
+ }
81
+
82
+ {
83
+ // The two-producer case. A cycle field on gen4_ac is filled by a
84
+ // `store_value` matched by NAME, with `source` left empty — so a picker
85
+ // that only offered FQDNs would wire a binding that never fills.
86
+ const cat = C.cycleSourceCatalog(ctx, { producedNames: C.PRODUCED, fieldName: 'preload_load' });
87
+ check('the sequence is offered first', groupIds(cat)[0], 'sequence');
88
+ ok('GM variables are still offered', groupIds(cat).includes('measured'));
89
+
90
+ const seq = cat[0].entries;
91
+ check('choosing a sequence value CLEARS the source', seq.map(e => e.insert), ['', '', '']);
92
+ check('the matching name is called out',
93
+ seq.find(e => e.name === 'preload_load').note, undefined);
94
+ ok('a mismatched name says what to do',
95
+ /Rename this field to "test_load"/.test(seq.find(e => e.name === 'test_load').note),
96
+ seq.find(e => e.name === 'test_load').note);
97
+
98
+ // No sequence read → no group, rather than an empty one claiming nothing
99
+ // produces anything.
100
+ const none = C.cycleSourceCatalog(ctx, {});
101
+ ok('an unread sequence adds no group', !groupIds(none).includes('sequence'));
102
+ }
103
+
104
+ {
105
+ const raw = C.chartFieldCatalog({
106
+ viewType: 'raw_trace', rawColumns: ['t', 'force'], cycleFields: ['peak'], resultsFields: [],
107
+ });
108
+ check('a raw_trace offers trace columns first', groupIds(raw), ['raw', 'cycle']);
109
+ const scatter = C.chartFieldCatalog({
110
+ viewType: 'cycle_scatter', rawColumns: ['t'], cycleFields: ['peak'], testFields: ['operator'],
111
+ });
112
+ check('a cycle_scatter offers cycle columns first', groupIds(scatter), ['cycle', 'raw', 'test']);
113
+ }
114
+
115
+ {
116
+ // A repeated measurement lives inside a `cycle` body, so a walk that
117
+ // stopped at the top level would miss the methods that record the most.
118
+ const method = {
119
+ steps: [
120
+ { type: 'store_value', name: 'a', expr: 'x' },
121
+ { type: 'cycle', count: '3', body: [{ type: 'store_value', name: 'b', expr: 'y' }] },
122
+ { type: 'store_value', name: 'a', expr: 'z' },
123
+ { type: 'store_value', name: '', expr: 'q' },
124
+ { type: 'push_row' },
125
+ ],
126
+ };
127
+ check('stored names include cycle bodies, deduped, blanks dropped',
128
+ C.collectStoredNames(method), ['a', 'b']);
129
+ check('a non-method reads as no names', C.collectStoredNames({ steps: null }), []);
130
+ }
131
+
132
+ // ── 2. the dialog, end to end ───────────────────────────────────────────────
133
+
134
+ await act(async () => { window.__mount(); });
135
+ await tick();
136
+
137
+ const q = (sel) => [...document.querySelectorAll(sel)];
138
+ const dialogs = () => q('.p-dialog');
139
+ const rowLabelled = (label) =>
140
+ q('.ac-formrow').find(r => r.querySelector('.ac-formrow__label')?.textContent?.startsWith(label));
141
+ const inputIn = (label) => rowLabelled(label)?.querySelector('input');
142
+ const pickerButtonIn = (label) => rowLabelled(label)?.querySelector('button');
143
+ const setValue = (el, v) => {
144
+ const desc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value');
145
+ desc.set.call(el, v);
146
+ el.dispatchEvent(new dom.window.Event('input', { bubbles: true }));
147
+ };
148
+ const click = async (el) => {
149
+ await act(async () => { el.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); });
150
+ await tick();
151
+ };
152
+ /** The picker dialog is the last one opened, over the field dialog. */
153
+ const picker = () => dialogs().at(-1);
154
+ const pickerRows = () => [...picker().querySelectorAll('.ac-varpick__row')];
155
+ const buttonLabelled = (root, text) =>
156
+ [...root.querySelectorAll('button')].find(b => b.textContent?.trim() === text);
157
+
158
+ ok('every field dialog input still renders', inputIn('Source') !== undefined);
159
+ ok('Source has a picker button',
160
+ pickerButtonIn('Source')?.getAttribute('aria-label') === 'Choose a source for this field');
161
+
162
+ // -- a cycle field picks a SEQUENCE producer, which clears Source ------------
163
+ setValue(inputIn('Source'), 'gm.stale_binding');
164
+ await tick();
165
+ check('typing still works', inputIn('Source').value, 'gm.stale_binding');
166
+
167
+ await click(pickerButtonIn('Source'));
168
+ ok('the picker opened', dialogs().length > 1, `dialogs=${dialogs().length}`);
169
+ ok('the sequence group is present',
170
+ picker().textContent.includes('Recorded by the sequence'));
171
+
172
+ const preloadRow = pickerRows().find(r => r.querySelector('code')?.textContent === 'preload_load');
173
+ ok('the matching produced name is listed', preloadRow !== undefined);
174
+ await click(preloadRow);
175
+ await click(buttonLabelled(picker(), 'Insert'));
176
+ check('picking a sequence producer clears Source', inputIn('Source').value, '');
177
+
178
+ // -- a bound picks a token --------------------------------------------------
179
+ await click(pickerButtonIn('Min'));
180
+ const searchBox = picker().querySelector('.ac-varpick__search');
181
+ setValue(searchBox, 'lower force bound');
182
+ await tick();
183
+ check('a variable is findable by what it MEANS',
184
+ pickerRows().map(r => r.querySelector('code').textContent), ['press_force_min_n']);
185
+ await click(pickerRows()[0]);
186
+ await click(buttonLabelled(picker(), 'Insert'));
187
+ check('a bound is inserted as a token', inputIn('Min').value, '${gm.press_force_min_n}');
188
+
189
+ // -- the saved field carries both -------------------------------------------
190
+ const dialogRoot = dialogs()[0];
191
+ await click(buttonLabelled(dialogRoot, 'Save'));
192
+ const saved = window.__saved();
193
+ ok('the field saved', saved !== null);
194
+ check('source stayed cleared', saved?.source, undefined);
195
+ check('min saved as a token', saved?.min, '${gm.press_force_min_n}');
196
+
197
+ // -- a config field offers only writable variables ---------------------------
198
+ await act(async () => { window.__setArray('config_fields'); });
199
+ await tick();
200
+ await click(pickerButtonIn('Source'));
201
+ ok('a config field is not offered the sequence',
202
+ !picker().textContent.includes('Recorded by the sequence'));
203
+ ok('a config field is not offered a hardware-owned variable',
204
+ !pickerRows().some(r => r.querySelector('code')?.textContent === 'press_position'),
205
+ pickerRows().map(r => r.querySelector('code')?.textContent).join(','));
206
+
207
+ console.log(`\nvarpick: ${passed} checks passed`);
208
+ if (process.exitCode) console.error('varpick: FAILURES');
@@ -17,9 +17,14 @@ export default defineConfig({
17
17
  outDir: resolve(here, 'dist'),
18
18
  emptyOutDir: true,
19
19
  lib: {
20
- entry: resolve(here, 'tis-editor.entry.tsx'),
20
+ // Named entries so each test file imports its own bundle by name;
21
+ // shared code (React, PrimeReact) is hoisted into one chunk, which
22
+ // is what keeps a single copy of React in the jsdom run.
23
+ entry: {
24
+ 'tis-editor.entry': resolve(here, 'tis-editor.entry.tsx'),
25
+ 'varpick.entry': resolve(here, 'varpick.entry.tsx'),
26
+ },
21
27
  formats: ['es'],
22
- fileName: 'tis-editor.entry',
23
28
  },
24
29
  minify: false,
25
30
  target: 'node20',