@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.
- package/dist/index.d.ts +137 -0
- package/dist/index.js +261 -0
- package/package.json +37 -0
- package/readme.md +74 -0
package/dist/index.d.ts
ADDED
|
@@ -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.
|