@jay-framework/runtime-automation 0.11.0 → 0.13.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/dist/index.d.ts CHANGED
@@ -4,27 +4,43 @@ import { TrackByMap } from '@jay-framework/view-state-merge';
4
4
 
5
5
  /**
6
6
  * Coordinate path identifying an element.
7
+ * For simple elements: ['refName']
7
8
  * For forEach items: ['trackByValue', 'refName']
8
- * For nested: ['parentTrackBy', 'childTrackBy', 'refName']
9
+ * For nested forEach: ['parentTrackBy', 'childTrackBy', 'refName']
9
10
  */
10
11
  type Coordinate = string[];
11
12
  /**
12
- * Represents an interactive element on the page.
13
+ * A single interactive element instance — has the DOM element and its coordinate.
14
+ */
15
+ interface InteractionInstance {
16
+ /** Full coordinate path identifying this element */
17
+ coordinate: Coordinate;
18
+ /** The actual DOM element — can be used to read/set values or call click() */
19
+ element: HTMLElement;
20
+ /** Relevant events this element handles (e.g., ["click"] or ["input", "change"]) */
21
+ events: string[];
22
+ }
23
+ /**
24
+ * Interactions grouped by refName.
25
+ * Each ref has one or more instances (multiple when inside a forEach).
13
26
  */
14
27
  interface Interaction {
15
28
  /** Ref name from jay-html */
16
29
  refName: string;
17
- /** Full coordinate path (for forEach items) */
30
+ /** All instances of this ref (one per forEach item, or a single entry for non-forEach) */
31
+ items: InteractionInstance[];
32
+ /** Human-readable description (from contract if available) */
33
+ description?: string;
34
+ }
35
+ /**
36
+ * Internal: raw per-element data from the interaction collector.
37
+ * Not part of the public API — consumed by the grouping function.
38
+ */
39
+ interface CollectedInteraction {
40
+ refName: string;
18
41
  coordinate: Coordinate;
19
- /** The actual DOM element - can be used to read/set values or call click() */
20
42
  element: HTMLElement;
21
- /** HTML element type (e.g., "HTMLButtonElement") */
22
- elementType: string;
23
- /** Events this element can handle (e.g., ["click", "input"]) */
24
43
  supportedEvents: string[];
25
- /** For collection items: the item's ViewState */
26
- itemContext?: object;
27
- /** Human-readable description (from contract if available) */
28
44
  description?: string;
29
45
  }
30
46
  /**
@@ -33,7 +49,7 @@ interface Interaction {
33
49
  interface PageState {
34
50
  /** Current ViewState of the component (includes headless component data under their keys) */
35
51
  viewState: object;
36
- /** All available interactions with their DOM elements */
52
+ /** Available interactions grouped by ref name */
37
53
  interactions: Interaction[];
38
54
  /** Custom events the component can emit */
39
55
  customEvents: Array<{
@@ -44,21 +60,21 @@ interface PageState {
44
60
  * Automation API attached to wrapped components.
45
61
  */
46
62
  interface AutomationAPI {
47
- /** Get current page state and available interactions */
63
+ /** Get current page state and available interactions (grouped by ref) */
48
64
  getPageState(): PageState;
49
65
  /** Trigger an event on an element by coordinate */
50
66
  triggerEvent(eventType: string, coordinate: Coordinate, eventData?: object): void;
51
- /** Subscribe to ViewState changes - called on every ViewState update */
67
+ /** Subscribe to ViewState changes called on every ViewState update */
52
68
  onStateChange(callback: (state: PageState) => void): () => void;
53
- /** Get a specific interaction by coordinate */
54
- getInteraction(coordinate: Coordinate): Interaction | undefined;
69
+ /** Get a specific interaction instance by coordinate (returns the DOM element) */
70
+ getInteraction(coordinate: Coordinate): InteractionInstance | undefined;
55
71
  /** Get list of custom events the component emits */
56
72
  getCustomEvents(): Array<{
57
73
  name: string;
58
74
  }>;
59
75
  /** Subscribe to a custom component event (e.g., 'AddToCart') */
60
76
  onComponentEvent(eventName: string, callback: (eventData: any) => void): () => void;
61
- /** Cleanup - call when component is unmounted */
77
+ /** Cleanup call when component is unmounted */
62
78
  dispose(): void;
63
79
  }
64
80
  /** @deprecated Use Interaction instead */
@@ -132,6 +148,11 @@ declare const AUTOMATION_CONTEXT: _jay_framework_runtime.ContextMarker<Automatio
132
148
  * Collects all interactive elements from a component's refs.
133
149
  * Handles nested refs (e.g., headless components) by recursively traversing the refs tree.
134
150
  */
135
- declare function collectInteractions(refs: any): Interaction[];
151
+ declare function collectInteractions(refs: any): CollectedInteraction[];
152
+
153
+ /**
154
+ * Groups raw collected interactions by refName and filters events to relevant ones.
155
+ */
156
+ declare function groupInteractions(raw: CollectedInteraction[]): Interaction[];
136
157
 
137
- export { type AIAgentAPI, type AIInteraction, type AIPageState, AUTOMATION_CONTEXT, type AutomationAPI, type AutomationAgentOptions, type AutomationWrappedComponent, type Coordinate, type Interaction, type PageState, collectInteractions, wrapWithAutomation };
158
+ export { type AIAgentAPI, type AIInteraction, type AIPageState, AUTOMATION_CONTEXT, type AutomationAPI, type AutomationAgentOptions, type AutomationWrappedComponent, type Coordinate, type Interaction, type InteractionInstance, type PageState, collectInteractions, groupInteractions, wrapWithAutomation };
package/dist/index.js CHANGED
@@ -91,14 +91,13 @@ function collectInteractionsRecursive(refs, interactions) {
91
91
  continue;
92
92
  if (refImpl.elements && refImpl.elements instanceof Set) {
93
93
  for (const elem of refImpl.elements) {
94
- if (elem.element) {
94
+ if (elem.element && !isDisabled(elem.element)) {
95
95
  interactions.push({
96
96
  refName,
97
97
  coordinate: elem.coordinate || [refName],
98
98
  element: elem.element,
99
- elementType: getElementType(elem.element),
100
99
  supportedEvents: getSupportedEvents(elem.element),
101
- itemContext: elem.viewState
100
+ description: elem.description
102
101
  });
103
102
  }
104
103
  }
@@ -117,8 +116,12 @@ function isNestedRefsObject(obj) {
117
116
  return false;
118
117
  return true;
119
118
  }
120
- function getElementType(element) {
121
- return element.constructor.name;
119
+ function isDisabled(element) {
120
+ if ("disabled" in element && element.disabled) {
121
+ return true;
122
+ }
123
+ const fieldset = element.closest?.("fieldset:disabled");
124
+ return !!fieldset;
122
125
  }
123
126
  function getSupportedEvents(element) {
124
127
  const base = ["click", "focus", "blur"];
@@ -142,11 +145,45 @@ function getSupportedEvents(element) {
142
145
  }
143
146
  return base;
144
147
  }
148
+ function groupInteractions(raw) {
149
+ const byRef = /* @__PURE__ */ new Map();
150
+ for (const item of raw) {
151
+ let group = byRef.get(item.refName);
152
+ if (!group) {
153
+ group = [];
154
+ byRef.set(item.refName, group);
155
+ }
156
+ group.push(item);
157
+ }
158
+ return Array.from(byRef.entries()).map(([refName, items]) => ({
159
+ refName,
160
+ description: items[0].description,
161
+ items: items.map(toInstance)
162
+ }));
163
+ }
164
+ function toInstance(raw) {
165
+ return {
166
+ coordinate: raw.coordinate,
167
+ element: raw.element,
168
+ events: relevantEvents(raw)
169
+ };
170
+ }
171
+ function relevantEvents(raw) {
172
+ const el = raw.element;
173
+ if (el instanceof HTMLButtonElement || el instanceof HTMLAnchorElement) {
174
+ return ["click"];
175
+ }
176
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
177
+ return raw.supportedEvents.filter((e) => e === "input" || e === "change");
178
+ }
179
+ return raw.supportedEvents;
180
+ }
145
181
  const VIEW_STATE_CHANGE = "viewStateChange";
146
182
  class AutomationAgent {
147
183
  constructor(component, options) {
148
184
  __publicField(this, "stateListeners", /* @__PURE__ */ new Set());
149
- __publicField(this, "cachedInteractions", null);
185
+ __publicField(this, "cachedRaw", null);
186
+ __publicField(this, "cachedGrouped", null);
150
187
  __publicField(this, "viewStateHandler", null);
151
188
  /**
152
189
  * When slow rendering is used, this holds the merged slow+fast ViewState.
@@ -169,7 +206,8 @@ class AutomationAgent {
169
206
  }
170
207
  subscribeToUpdates() {
171
208
  this.viewStateHandler = () => {
172
- this.cachedInteractions = null;
209
+ this.cachedRaw = null;
210
+ this.cachedGrouped = null;
173
211
  if (this.initialSlowViewState && this.trackByMap) {
174
212
  this.mergedViewState = deepMergeViewStates(
175
213
  this.initialSlowViewState,
@@ -187,33 +225,43 @@ class AutomationAgent {
187
225
  const state = this.getPageState();
188
226
  this.stateListeners.forEach((callback) => callback(state));
189
227
  }
190
- getPageState() {
191
- if (!this.cachedInteractions) {
192
- this.cachedInteractions = collectInteractions(this.component.element?.refs);
228
+ getGrouped() {
229
+ if (!this.cachedGrouped) {
230
+ if (!this.cachedRaw) {
231
+ this.cachedRaw = collectInteractions(this.component.element?.refs);
232
+ }
233
+ this.cachedGrouped = groupInteractions(this.cachedRaw);
193
234
  }
235
+ return this.cachedGrouped;
236
+ }
237
+ getPageState() {
194
238
  return {
195
239
  // Use merged state if available (slow+fast), otherwise component's viewState
196
240
  viewState: this.mergedViewState || this.component.viewState,
197
- interactions: this.cachedInteractions,
241
+ interactions: this.getGrouped(),
198
242
  customEvents: this.getCustomEvents()
199
243
  };
200
244
  }
201
245
  triggerEvent(eventType, coordinate, eventData) {
202
- const interaction = this.getInteraction(coordinate);
203
- if (!interaction) {
246
+ const instance = this.getInteraction(coordinate);
247
+ if (!instance) {
204
248
  throw new Error(`No element found at coordinate: ${coordinate.join("/")}`);
205
249
  }
206
250
  const event = new Event(eventType, { bubbles: true });
207
251
  if (eventData) {
208
252
  Object.assign(event, eventData);
209
253
  }
210
- interaction.element.dispatchEvent(event);
254
+ instance.element.dispatchEvent(event);
211
255
  }
212
256
  getInteraction(coordinate) {
213
- const state = this.getPageState();
214
- return state.interactions.find(
215
- (i) => i.coordinate.length === coordinate.length && i.coordinate.every((c, idx) => c === coordinate[idx])
216
- );
257
+ for (const group of this.getGrouped()) {
258
+ for (const instance of group.items) {
259
+ if (instance.coordinate.length === coordinate.length && instance.coordinate.every((c, idx) => c === coordinate[idx])) {
260
+ return instance;
261
+ }
262
+ }
263
+ }
264
+ return void 0;
217
265
  }
218
266
  onStateChange(callback) {
219
267
  this.stateListeners.add(callback);
@@ -246,7 +294,8 @@ class AutomationAgent {
246
294
  this.viewStateHandler = null;
247
295
  }
248
296
  this.stateListeners.clear();
249
- this.cachedInteractions = null;
297
+ this.cachedRaw = null;
298
+ this.cachedGrouped = null;
250
299
  }
251
300
  }
252
301
  function wrapWithAutomation(component, options) {
@@ -257,5 +306,6 @@ const AUTOMATION_CONTEXT = createJayContext();
257
306
  export {
258
307
  AUTOMATION_CONTEXT,
259
308
  collectInteractions,
309
+ groupInteractions,
260
310
  wrapWithAutomation
261
311
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/runtime-automation",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Automation API for Jay components - enables programmatic state inspection and interaction triggering",
@@ -21,12 +21,12 @@
21
21
  "test:watch": "vitest"
22
22
  },
23
23
  "dependencies": {
24
- "@jay-framework/runtime": "^0.11.0",
25
- "@jay-framework/view-state-merge": "^0.11.0"
24
+ "@jay-framework/runtime": "^0.13.0",
25
+ "@jay-framework/view-state-merge": "^0.13.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@jay-framework/component": "^0.11.0",
29
- "@jay-framework/dev-environment": "^0.11.0",
28
+ "@jay-framework/component": "^0.13.0",
29
+ "@jay-framework/dev-environment": "^0.13.0",
30
30
  "@types/node": "^20.11.5",
31
31
  "rimraf": "^5.0.5",
32
32
  "tsup": "^8.0.1",