@jay-framework/runtime-automation 0.11.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.
@@ -0,0 +1,137 @@
1
+ import * as _jay_framework_runtime from '@jay-framework/runtime';
2
+ import { JayComponent } from '@jay-framework/runtime';
3
+ import { TrackByMap } from '@jay-framework/view-state-merge';
4
+
5
+ /**
6
+ * Coordinate path identifying an element.
7
+ * For forEach items: ['trackByValue', 'refName']
8
+ * For nested: ['parentTrackBy', 'childTrackBy', 'refName']
9
+ */
10
+ type Coordinate = string[];
11
+ /**
12
+ * Represents an interactive element on the page.
13
+ */
14
+ interface Interaction {
15
+ /** Ref name from jay-html */
16
+ refName: string;
17
+ /** Full coordinate path (for forEach items) */
18
+ coordinate: Coordinate;
19
+ /** The actual DOM element - can be used to read/set values or call click() */
20
+ element: HTMLElement;
21
+ /** HTML element type (e.g., "HTMLButtonElement") */
22
+ elementType: string;
23
+ /** Events this element can handle (e.g., ["click", "input"]) */
24
+ supportedEvents: string[];
25
+ /** For collection items: the item's ViewState */
26
+ itemContext?: object;
27
+ /** Human-readable description (from contract if available) */
28
+ description?: string;
29
+ }
30
+ /**
31
+ * Current page state exposed for automation.
32
+ */
33
+ interface PageState {
34
+ /** Current ViewState of the component (includes headless component data under their keys) */
35
+ viewState: object;
36
+ /** All available interactions with their DOM elements */
37
+ interactions: Interaction[];
38
+ /** Custom events the component can emit */
39
+ customEvents: Array<{
40
+ name: string;
41
+ }>;
42
+ }
43
+ /**
44
+ * Automation API attached to wrapped components.
45
+ */
46
+ interface AutomationAPI {
47
+ /** Get current page state and available interactions */
48
+ getPageState(): PageState;
49
+ /** Trigger an event on an element by coordinate */
50
+ triggerEvent(eventType: string, coordinate: Coordinate, eventData?: object): void;
51
+ /** Subscribe to ViewState changes - called on every ViewState update */
52
+ onStateChange(callback: (state: PageState) => void): () => void;
53
+ /** Get a specific interaction by coordinate */
54
+ getInteraction(coordinate: Coordinate): Interaction | undefined;
55
+ /** Get list of custom events the component emits */
56
+ getCustomEvents(): Array<{
57
+ name: string;
58
+ }>;
59
+ /** Subscribe to a custom component event (e.g., 'AddToCart') */
60
+ onComponentEvent(eventName: string, callback: (eventData: any) => void): () => void;
61
+ /** Cleanup - call when component is unmounted */
62
+ dispose(): void;
63
+ }
64
+ /** @deprecated Use Interaction instead */
65
+ type AIInteraction = Interaction;
66
+ /** @deprecated Use PageState instead */
67
+ type AIPageState = PageState;
68
+ /** @deprecated Use AutomationAPI instead */
69
+ type AIAgentAPI = AutomationAPI;
70
+
71
+ /**
72
+ * Options for creating an AutomationAgent with slow ViewState support.
73
+ */
74
+ interface AutomationAgentOptions {
75
+ /** The initial merged slow+fast ViewState */
76
+ initialViewState: object;
77
+ /** TrackByMap for deep merging arrays by their track-by keys */
78
+ trackByMap: TrackByMap;
79
+ }
80
+ /** Wrapper type that adds automation capabilities to a component */
81
+ type AutomationWrappedComponent<T> = T & {
82
+ automation: AutomationAPI;
83
+ };
84
+ /**
85
+ * Wraps a Jay component with automation capabilities.
86
+ * Uses addEventListener('viewStateChange', ...) to capture all state changes.
87
+ *
88
+ * @param component - The Jay component to wrap
89
+ * @param options - Optional options for slow rendering support:
90
+ * - initialViewState: Full ViewState (slow+fast merged)
91
+ * - trackByMap: Map of array paths to their track-by keys for deep merging
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const instance = MyComponent(props);
96
+ * const wrapped = wrapWithAutomation(instance);
97
+ *
98
+ * // Access automation API
99
+ * const state = wrapped.automation.getPageState();
100
+ * wrapped.automation.triggerEvent('click', ['button-ref']);
101
+ * ```
102
+ *
103
+ * @example With slow rendering
104
+ * ```typescript
105
+ * const instance = MyComponent(fastViewState);
106
+ * const fullViewState = deepMergeViewStates(slowViewState, fastViewState, trackByMap);
107
+ * const wrapped = wrapWithAutomation(instance, { initialViewState: fullViewState, trackByMap });
108
+ * ```
109
+ */
110
+ declare function wrapWithAutomation<T extends JayComponent<any, any, any>>(component: T, options?: AutomationAgentOptions): AutomationWrappedComponent<T>;
111
+
112
+ /**
113
+ * Global context for accessing the automation API.
114
+ * Available in dev mode when automation is enabled.
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * import { useContext } from '@jay-framework/runtime';
119
+ * import { AUTOMATION_CONTEXT } from '@jay-framework/runtime-automation';
120
+ *
121
+ * function MyComponent(props, refs, automation: AutomationAPI) {
122
+ * const state = automation.getPageState();
123
+ * // ...
124
+ * }
125
+ *
126
+ * export const MyComp = makeJayComponent(render, MyComponent, AUTOMATION_CONTEXT);
127
+ * ```
128
+ */
129
+ declare const AUTOMATION_CONTEXT: _jay_framework_runtime.ContextMarker<AutomationAPI>;
130
+
131
+ /**
132
+ * Collects all interactive elements from a component's refs.
133
+ * Handles nested refs (e.g., headless components) by recursively traversing the refs tree.
134
+ */
135
+ declare function collectInteractions(refs: any): Interaction[];
136
+
137
+ export { type AIAgentAPI, type AIInteraction, type AIPageState, AUTOMATION_CONTEXT, type AutomationAPI, type AutomationAgentOptions, type AutomationWrappedComponent, type Coordinate, type Interaction, type PageState, collectInteractions, wrapWithAutomation };
package/dist/index.js ADDED
@@ -0,0 +1,261 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => {
4
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
+ return value;
6
+ };
7
+ import { createJayContext } from "@jay-framework/runtime";
8
+ function deepMergeViewStates(base, overlay, trackByMap, path = "") {
9
+ if (!base && !overlay)
10
+ return {};
11
+ if (!base)
12
+ return overlay || {};
13
+ if (!overlay)
14
+ return base || {};
15
+ const result = {};
16
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(overlay)]);
17
+ for (const key of allKeys) {
18
+ const baseValue = base[key];
19
+ const overlayValue = overlay[key];
20
+ const currentPath = path ? `${path}.${key}` : key;
21
+ if (overlayValue === void 0) {
22
+ result[key] = baseValue;
23
+ } else if (baseValue === void 0) {
24
+ result[key] = overlayValue;
25
+ } else if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
26
+ const trackByField = trackByMap[currentPath];
27
+ if (trackByField) {
28
+ result[key] = mergeArraysByTrackBy(
29
+ baseValue,
30
+ overlayValue,
31
+ trackByField,
32
+ trackByMap,
33
+ currentPath
34
+ );
35
+ } else {
36
+ result[key] = overlayValue;
37
+ }
38
+ } else if (typeof baseValue === "object" && baseValue !== null && typeof overlayValue === "object" && overlayValue !== null && !Array.isArray(baseValue) && !Array.isArray(overlayValue)) {
39
+ result[key] = deepMergeViewStates(baseValue, overlayValue, trackByMap, currentPath);
40
+ } else {
41
+ result[key] = overlayValue;
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+ function mergeArraysByTrackBy(baseArray, overlayArray, trackByField, trackByMap, arrayPath) {
47
+ const baseByKey = /* @__PURE__ */ new Map();
48
+ for (const item of baseArray) {
49
+ const key = item[trackByField];
50
+ if (key !== void 0 && key !== null) {
51
+ if (baseByKey.has(key)) {
52
+ console.warn(
53
+ `Duplicate trackBy key [${key}] in base array at path [${arrayPath}]. This may cause incorrect merging.`
54
+ );
55
+ }
56
+ baseByKey.set(key, item);
57
+ }
58
+ }
59
+ const overlayByKey = /* @__PURE__ */ new Map();
60
+ for (const item of overlayArray) {
61
+ const key = item[trackByField];
62
+ if (key !== void 0 && key !== null) {
63
+ overlayByKey.set(key, item);
64
+ }
65
+ }
66
+ return baseArray.map((baseItem) => {
67
+ const key = baseItem[trackByField];
68
+ if (key === void 0 || key === null) {
69
+ return baseItem;
70
+ }
71
+ const overlayItem = overlayByKey.get(key);
72
+ if (overlayItem) {
73
+ return deepMergeViewStates(baseItem, overlayItem, trackByMap, arrayPath);
74
+ } else {
75
+ return baseItem;
76
+ }
77
+ });
78
+ }
79
+ function collectInteractions(refs) {
80
+ const interactions = [];
81
+ if (!refs)
82
+ return interactions;
83
+ collectInteractionsRecursive(refs, interactions);
84
+ return interactions;
85
+ }
86
+ function collectInteractionsRecursive(refs, interactions) {
87
+ if (!refs)
88
+ return;
89
+ for (const [refName, refImpl] of Object.entries(refs)) {
90
+ if (!refImpl)
91
+ continue;
92
+ if (refImpl.elements && refImpl.elements instanceof Set) {
93
+ for (const elem of refImpl.elements) {
94
+ if (elem.element) {
95
+ interactions.push({
96
+ refName,
97
+ coordinate: elem.coordinate || [refName],
98
+ element: elem.element,
99
+ elementType: getElementType(elem.element),
100
+ supportedEvents: getSupportedEvents(elem.element),
101
+ itemContext: elem.viewState
102
+ });
103
+ }
104
+ }
105
+ } else if (isNestedRefsObject(refImpl)) {
106
+ collectInteractionsRecursive(refImpl, interactions);
107
+ }
108
+ }
109
+ }
110
+ function isNestedRefsObject(obj) {
111
+ if (!obj || typeof obj !== "object")
112
+ return false;
113
+ if (obj.elements instanceof Set)
114
+ return false;
115
+ const proto = Object.getPrototypeOf(obj);
116
+ if (proto !== Object.prototype && proto !== null)
117
+ return false;
118
+ return true;
119
+ }
120
+ function getElementType(element) {
121
+ return element.constructor.name;
122
+ }
123
+ function getSupportedEvents(element) {
124
+ const base = ["click", "focus", "blur"];
125
+ if (element instanceof HTMLInputElement) {
126
+ return [...base, "input", "change"];
127
+ }
128
+ if (element instanceof HTMLButtonElement) {
129
+ return ["click", "focus", "blur"];
130
+ }
131
+ if (element instanceof HTMLSelectElement) {
132
+ return [...base, "change"];
133
+ }
134
+ if (element instanceof HTMLTextAreaElement) {
135
+ return [...base, "input", "change"];
136
+ }
137
+ if (element instanceof HTMLAnchorElement) {
138
+ return ["click"];
139
+ }
140
+ if (element instanceof HTMLFormElement) {
141
+ return ["submit", "reset"];
142
+ }
143
+ return base;
144
+ }
145
+ const VIEW_STATE_CHANGE = "viewStateChange";
146
+ class AutomationAgent {
147
+ constructor(component, options) {
148
+ __publicField(this, "stateListeners", /* @__PURE__ */ new Set());
149
+ __publicField(this, "cachedInteractions", null);
150
+ __publicField(this, "viewStateHandler", null);
151
+ /**
152
+ * When slow rendering is used, this holds the merged slow+fast ViewState.
153
+ * Updated on each viewStateChange event with the new fast state merged in.
154
+ */
155
+ __publicField(this, "mergedViewState");
156
+ __publicField(this, "initialSlowViewState");
157
+ __publicField(this, "trackByMap");
158
+ this.component = component;
159
+ if (options) {
160
+ this.initialSlowViewState = options.initialViewState;
161
+ this.trackByMap = options.trackByMap;
162
+ this.mergedViewState = deepMergeViewStates(
163
+ options.initialViewState,
164
+ this.component.viewState || {},
165
+ options.trackByMap
166
+ );
167
+ }
168
+ this.subscribeToUpdates();
169
+ }
170
+ subscribeToUpdates() {
171
+ this.viewStateHandler = () => {
172
+ this.cachedInteractions = null;
173
+ if (this.initialSlowViewState && this.trackByMap) {
174
+ this.mergedViewState = deepMergeViewStates(
175
+ this.initialSlowViewState,
176
+ this.component.viewState || {},
177
+ this.trackByMap
178
+ );
179
+ }
180
+ this.notifyListeners();
181
+ };
182
+ this.component.addEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
183
+ }
184
+ notifyListeners() {
185
+ if (this.stateListeners.size === 0)
186
+ return;
187
+ const state = this.getPageState();
188
+ this.stateListeners.forEach((callback) => callback(state));
189
+ }
190
+ getPageState() {
191
+ if (!this.cachedInteractions) {
192
+ this.cachedInteractions = collectInteractions(this.component.element?.refs);
193
+ }
194
+ return {
195
+ // Use merged state if available (slow+fast), otherwise component's viewState
196
+ viewState: this.mergedViewState || this.component.viewState,
197
+ interactions: this.cachedInteractions,
198
+ customEvents: this.getCustomEvents()
199
+ };
200
+ }
201
+ triggerEvent(eventType, coordinate, eventData) {
202
+ const interaction = this.getInteraction(coordinate);
203
+ if (!interaction) {
204
+ throw new Error(`No element found at coordinate: ${coordinate.join("/")}`);
205
+ }
206
+ const event = new Event(eventType, { bubbles: true });
207
+ if (eventData) {
208
+ Object.assign(event, eventData);
209
+ }
210
+ interaction.element.dispatchEvent(event);
211
+ }
212
+ 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
+ );
217
+ }
218
+ onStateChange(callback) {
219
+ this.stateListeners.add(callback);
220
+ return () => this.stateListeners.delete(callback);
221
+ }
222
+ getCustomEvents() {
223
+ const events = [];
224
+ const component = this.component;
225
+ for (const key in component) {
226
+ if (component[key]?.emit && typeof component[key].emit === "function") {
227
+ const name = key.startsWith("on") ? key.slice(2) : key;
228
+ events.push({ name });
229
+ }
230
+ }
231
+ return events;
232
+ }
233
+ onComponentEvent(eventName, callback) {
234
+ const component = this.component;
235
+ const handlerKey = eventName.startsWith("on") ? eventName : `on${eventName}`;
236
+ const handler = component[handlerKey];
237
+ if (!handler || typeof handler !== "function") {
238
+ throw new Error(`Unknown component event: ${eventName}`);
239
+ }
240
+ handler(({ event }) => callback(event));
241
+ return () => handler(void 0);
242
+ }
243
+ dispose() {
244
+ if (this.viewStateHandler) {
245
+ this.component.removeEventListener(VIEW_STATE_CHANGE, this.viewStateHandler);
246
+ this.viewStateHandler = null;
247
+ }
248
+ this.stateListeners.clear();
249
+ this.cachedInteractions = null;
250
+ }
251
+ }
252
+ function wrapWithAutomation(component, options) {
253
+ const agent = new AutomationAgent(component, options);
254
+ return Object.assign(component, { automation: agent });
255
+ }
256
+ const AUTOMATION_CONTEXT = createJayContext();
257
+ export {
258
+ AUTOMATION_CONTEXT,
259
+ collectInteractions,
260
+ wrapWithAutomation
261
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@jay-framework/runtime-automation",
3
+ "version": "0.11.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "Automation API for Jay components - enables programmatic state inspection and interaction triggering",
7
+ "main": "./dist/index.js",
8
+ "files": [
9
+ "dist",
10
+ "readme.md"
11
+ ],
12
+ "scripts": {
13
+ "build": "npm run build:js && npm run build:types",
14
+ "build:watch": "npm run build:js -- --watch & npm run build:types -- --watch",
15
+ "build:js": "vite build",
16
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
17
+ "build:check-types": "tsc",
18
+ "clean": "rimraf dist",
19
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest"
22
+ },
23
+ "dependencies": {
24
+ "@jay-framework/runtime": "^0.11.0",
25
+ "@jay-framework/view-state-merge": "^0.11.0"
26
+ },
27
+ "devDependencies": {
28
+ "@jay-framework/component": "^0.11.0",
29
+ "@jay-framework/dev-environment": "^0.11.0",
30
+ "@types/node": "^20.11.5",
31
+ "rimraf": "^5.0.5",
32
+ "tsup": "^8.0.1",
33
+ "typescript": "^5.3.3",
34
+ "vite": "^5.0.11",
35
+ "vitest": "^1.2.1"
36
+ }
37
+ }
package/readme.md ADDED
@@ -0,0 +1,74 @@
1
+ # @jay-framework/runtime-automation
2
+
3
+ Automation API for Jay components. Enables programmatic automation to:
4
+
5
+ - Read current page state (ViewState)
6
+ - Discover available interactions (refs with coordinates)
7
+ - Trigger events on elements
8
+ - Subscribe to state changes
9
+ - Listen to custom component events
10
+
11
+ **Use cases**: AI agents, test automation, accessibility tools, E2E testing.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @jay-framework/runtime-automation
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```typescript
22
+ import { wrapWithAutomation } from '@jay-framework/runtime-automation';
23
+
24
+ // Wrap your component with automation capabilities
25
+ const instance = MyComponent(props);
26
+ const wrapped = wrapWithAutomation(instance);
27
+
28
+ // Mount as usual
29
+ target.appendChild(wrapped.element.dom);
30
+
31
+ // Automation API is available on the instance
32
+ const state = wrapped.automation.getPageState();
33
+ console.log(state.viewState); // Current data
34
+ console.log(state.interactions); // Available actions
35
+
36
+ // Trigger events
37
+ wrapped.automation.triggerEvent('click', ['product-123', 'remove']);
38
+
39
+ // Subscribe to state changes
40
+ const unsubscribe = wrapped.automation.onStateChange((newState) => {
41
+ console.log('State updated:', newState.viewState);
42
+ });
43
+ ```
44
+
45
+ ## Browser Console Usage
46
+
47
+ ```javascript
48
+ // Expose to window for console access
49
+ window.app = wrapped;
50
+
51
+ // Then from console:
52
+ app.automation.getPageState();
53
+ app.automation.triggerEvent('click', ['item-1', 'removeBtn']);
54
+ ```
55
+
56
+ ## API
57
+
58
+ ### `wrapWithAutomation(component)`
59
+
60
+ Wraps a Jay component with automation capabilities.
61
+
62
+ ### `AutomationAPI`
63
+
64
+ - `getPageState()` - Get current ViewState and available interactions
65
+ - `triggerEvent(type, coordinate, data?)` - Trigger an event on an element
66
+ - `getInteraction(coordinate)` - Get a specific interaction by coordinate
67
+ - `onStateChange(callback)` - Subscribe to ViewState changes
68
+ - `getCustomEvents()` - List custom events the component emits
69
+ - `onComponentEvent(name, callback)` - Subscribe to a custom component event
70
+ - `dispose()` - Clean up listeners
71
+
72
+ ## Design
73
+
74
+ See [Design Log #76 - AI Agent Integration](../../../design-log/76%20-%20AI%20Agent%20Integration.md) for full design documentation.