@skyramp/skyramp 1.3.15 → 1.3.17
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/package.json +1 -1
- package/src/classes/GoJSDiagram.d.ts +66 -0
- package/src/classes/GoJSDiagram.js +395 -0
- package/src/classes/SmartPlaywright.js +29 -4
- package/src/index.d.ts +1 -0
- package/src/index.js +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
export interface AddNodeOptions {
|
|
4
|
+
/** Key to assign to the new node in the GoJS model. */
|
|
5
|
+
key: string;
|
|
6
|
+
/** GoJS node category — used to look up the palette template. */
|
|
7
|
+
category: string;
|
|
8
|
+
/** CSS selector for the div that hosts the target GoJS diagram. */
|
|
9
|
+
diagramSelector: string;
|
|
10
|
+
/** CSS selector for the palette div to copy app-specific properties from. Optional. */
|
|
11
|
+
paletteSelector?: string;
|
|
12
|
+
/** Key of the pre-existing diagram node to use as placement anchor. */
|
|
13
|
+
anchorKey?: string;
|
|
14
|
+
/** X offset from the anchor node's document-space location. */
|
|
15
|
+
anchorOffsetX?: number;
|
|
16
|
+
/** Y offset from the anchor node's document-space location. */
|
|
17
|
+
anchorOffsetY?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NodeRef {
|
|
21
|
+
panelSelector: string;
|
|
22
|
+
key: string | number;
|
|
23
|
+
port?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PaletteSource {
|
|
27
|
+
panelSelector: string;
|
|
28
|
+
category: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DocTarget {
|
|
32
|
+
panelSelector: string;
|
|
33
|
+
docX: number;
|
|
34
|
+
docY: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NodeSource {
|
|
38
|
+
panelSelector: string;
|
|
39
|
+
key: string | number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export declare class GoJSDiagram {
|
|
43
|
+
constructor(page: Page, framePath?: string[]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add a new node to a GoJS diagram at a position relative to an anchor node.
|
|
47
|
+
* App-specific properties are copied from the palette template for the given
|
|
48
|
+
* category, skipping GoJS internals and loc to avoid palette mutation.
|
|
49
|
+
*/
|
|
50
|
+
addNode(opts: AddNodeOptions): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a directed link between two GoJS nodes via the model API.
|
|
54
|
+
*/
|
|
55
|
+
linkNodes(source: NodeRef, target: NodeRef): Promise<void>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Drag a node from a GoJS Palette into a GoJS Diagram.
|
|
59
|
+
*/
|
|
60
|
+
dragFromPalette(source: PaletteSource, target: DocTarget): Promise<void>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Move an existing node within a GoJS Diagram.
|
|
64
|
+
*/
|
|
65
|
+
moveNode(source: NodeSource, target: DocTarget): Promise<void>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Skyramp Corporation.
|
|
3
|
+
*
|
|
4
|
+
* GoJSDiagram — a Playwright helper for driving GoJS canvases.
|
|
5
|
+
*
|
|
6
|
+
* GoJS renders entirely into a <canvas> element so there are no DOM nodes for
|
|
7
|
+
* individual diagram items. This class handles the three-step coordinate
|
|
8
|
+
* resolution that is required for any reliable interaction:
|
|
9
|
+
*
|
|
10
|
+
* 1. Ask the GoJS model for the viewport-space offset of the element
|
|
11
|
+
* (via go.Diagram.fromDiv().transformDocToView() or findNodeForKey()).
|
|
12
|
+
* 2. Ask Playwright for the page-level bounding box of the <canvas> element
|
|
13
|
+
* (via locator.boundingBox() which correctly traverses the iframe chain).
|
|
14
|
+
* 3. Add the two together → final page coordinate for page.mouse.*.
|
|
15
|
+
*
|
|
16
|
+
* The class is generic: it locates GoJS containers via the standard
|
|
17
|
+
* go.Diagram.fromDiv() API and does NOT rely on application-specific CSS IDs.
|
|
18
|
+
*
|
|
19
|
+
* Usage (TypeScript):
|
|
20
|
+
*
|
|
21
|
+
* import { GoJSDiagram } from '@skyramp/skyramp';
|
|
22
|
+
*
|
|
23
|
+
* const gojs = new GoJSDiagram(page, ['iframe[title="main menu"]', 'iframe[title="iframe process"]']);
|
|
24
|
+
*
|
|
25
|
+
* // Drag a node from a palette to a diagram
|
|
26
|
+
* await gojs.dragFromPalette(
|
|
27
|
+
* { panelSelector: '#myPalette', category: 'Task' },
|
|
28
|
+
* { panelSelector: '#myDiagram', docX: 350, docY: 285 },
|
|
29
|
+
* );
|
|
30
|
+
*
|
|
31
|
+
* // Move an existing node within a diagram
|
|
32
|
+
* await gojs.moveNode(
|
|
33
|
+
* { panelSelector: '#myDiagram', key: '3' },
|
|
34
|
+
* { panelSelector: '#myDiagram', docX: 250, docY: 300 },
|
|
35
|
+
* );
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/* global document, window */
|
|
39
|
+
|
|
40
|
+
const GOJS_DRAG_STEPS = 50; // Minimum steps for GoJS to register a drag
|
|
41
|
+
|
|
42
|
+
class GoJSDiagram {
|
|
43
|
+
/**
|
|
44
|
+
* @param {import('@playwright/test').Page} page The Playwright Page object.
|
|
45
|
+
* @param {string[]} framePath Ordered list of iframe selectors to reach the
|
|
46
|
+
* frame that contains the GoJS diagram (e.g. ['iframe[title="menu"]',
|
|
47
|
+
* 'iframe[title="process"]']). Empty array means the diagram is in the
|
|
48
|
+
* top-level page.
|
|
49
|
+
*/
|
|
50
|
+
constructor(page, framePath = []) {
|
|
51
|
+
this._page = page;
|
|
52
|
+
this._framePath = framePath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns a FrameLocator scoped to the innermost frame in framePath,
|
|
57
|
+
* or the page itself when framePath is empty.
|
|
58
|
+
*
|
|
59
|
+
* @returns {import('@playwright/test').Page | import('@playwright/test').FrameLocator}
|
|
60
|
+
*/
|
|
61
|
+
_frameLocator() {
|
|
62
|
+
let scope = this._page;
|
|
63
|
+
for (const sel of this._framePath) {
|
|
64
|
+
scope = scope.frameLocator(sel);
|
|
65
|
+
}
|
|
66
|
+
return scope;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the page-level {x, y} of a point expressed in GoJS document
|
|
71
|
+
* coordinates within the specified panel container.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} panelSelector CSS selector for the div that hosts the GoJS
|
|
74
|
+
* diagram or palette (e.g. '#myPalette', '[data-testid="diagram"]').
|
|
75
|
+
* @param {number} docX X coordinate in GoJS document space.
|
|
76
|
+
* @param {number} docY Y coordinate in GoJS document space.
|
|
77
|
+
* @returns {Promise<{x: number, y: number}>}
|
|
78
|
+
*/
|
|
79
|
+
async _resolveDocPoint(panelSelector, docX, docY) {
|
|
80
|
+
const fl = this._frameLocator();
|
|
81
|
+
const canvasLocator = fl.locator(`${panelSelector} canvas`).first();
|
|
82
|
+
|
|
83
|
+
// Page-level bounding box of the canvas element
|
|
84
|
+
const box = await canvasLocator.boundingBox();
|
|
85
|
+
if (!box) {
|
|
86
|
+
throw new Error(`GoJSDiagram: canvas not found for panel "${panelSelector}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Viewport-space point inside the canvas, obtained via GoJS API in the frame
|
|
90
|
+
const vpPt = await this._evaluateInFrame(([sel, dx, dy]) => {
|
|
91
|
+
const container = document.querySelector(sel);
|
|
92
|
+
if (!container) return null;
|
|
93
|
+
const diagram = window.go?.Diagram?.fromDiv?.(container);
|
|
94
|
+
if (!diagram) return null;
|
|
95
|
+
const vp = diagram.transformDocToView(new window.go.Point(dx, dy));
|
|
96
|
+
return { x: vp.x, y: vp.y };
|
|
97
|
+
}, [panelSelector, docX, docY]);
|
|
98
|
+
|
|
99
|
+
if (!vpPt) {
|
|
100
|
+
throw new Error(`GoJSDiagram: could not resolve document point via GoJS for panel "${panelSelector}"`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { x: box.x + vpPt.x, y: box.y + vpPt.y };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the page-level {x, y} of a palette node identified by category.
|
|
108
|
+
* Picks the first node in the palette whose data.category matches.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} panelSelector CSS selector for the palette container div.
|
|
111
|
+
* @param {string} category The GoJS node category to look for.
|
|
112
|
+
* @returns {Promise<{x: number, y: number}>}
|
|
113
|
+
*/
|
|
114
|
+
async _resolvePaletteNode(panelSelector, category) {
|
|
115
|
+
const fl = this._frameLocator();
|
|
116
|
+
const canvasLocator = fl.locator(`${panelSelector} canvas`).first();
|
|
117
|
+
|
|
118
|
+
const box = await canvasLocator.boundingBox();
|
|
119
|
+
if (!box) {
|
|
120
|
+
throw new Error(`GoJSDiagram: palette canvas not found for panel "${panelSelector}"`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const vpPt = await this._evaluateInFrame(([sel, cat]) => {
|
|
124
|
+
const container = document.querySelector(sel);
|
|
125
|
+
if (!container) return null;
|
|
126
|
+
const diagram = window.go?.Diagram?.fromDiv?.(container);
|
|
127
|
+
if (!diagram) return null;
|
|
128
|
+
let found = null;
|
|
129
|
+
diagram.nodes.each(n => {
|
|
130
|
+
if (!found && n.data?.category === cat) found = n;
|
|
131
|
+
});
|
|
132
|
+
if (!found) return null;
|
|
133
|
+
const center = found.getDocumentPoint(window.go.Spot.Center);
|
|
134
|
+
const vp = diagram.transformDocToView(center);
|
|
135
|
+
return { x: vp.x, y: vp.y };
|
|
136
|
+
}, [panelSelector, category]);
|
|
137
|
+
|
|
138
|
+
if (!vpPt) {
|
|
139
|
+
throw new Error(`GoJSDiagram: no node with category "${category}" found in palette "${panelSelector}"`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { x: box.x + vpPt.x, y: box.y + vpPt.y };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the page-level {x, y} of a diagram node identified by key.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} panelSelector CSS selector for the diagram container div.
|
|
149
|
+
* @param {string|number} key The GoJS node key.
|
|
150
|
+
* @returns {Promise<{x: number, y: number}>}
|
|
151
|
+
*/
|
|
152
|
+
async _resolveDiagramNode(panelSelector, key) {
|
|
153
|
+
const fl = this._frameLocator();
|
|
154
|
+
const canvasLocator = fl.locator(`${panelSelector} canvas`).first();
|
|
155
|
+
|
|
156
|
+
const box = await canvasLocator.boundingBox();
|
|
157
|
+
if (!box) {
|
|
158
|
+
throw new Error(`GoJSDiagram: diagram canvas not found for panel "${panelSelector}"`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const vpPt = await this._evaluateInFrame(([sel, k]) => {
|
|
162
|
+
const container = document.querySelector(sel);
|
|
163
|
+
if (!container) return null;
|
|
164
|
+
const diagram = window.go?.Diagram?.fromDiv?.(container);
|
|
165
|
+
if (!diagram) return null;
|
|
166
|
+
const node = diagram.findNodeForKey(k);
|
|
167
|
+
if (!node) return null;
|
|
168
|
+
const center = node.getDocumentPoint(window.go.Spot.Center);
|
|
169
|
+
const vp = diagram.transformDocToView(center);
|
|
170
|
+
return { x: vp.x, y: vp.y };
|
|
171
|
+
}, [panelSelector, key]);
|
|
172
|
+
|
|
173
|
+
if (!vpPt) {
|
|
174
|
+
throw new Error(`GoJSDiagram: no node with key "${key}" found in diagram "${panelSelector}"`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { x: box.x + vpPt.x, y: box.y + vpPt.y };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Execute a function inside the correct frame context.
|
|
182
|
+
* Uses page.evaluate() when there are no iframes, otherwise uses
|
|
183
|
+
* page.frames().find() to reach the correct nested frame.
|
|
184
|
+
*
|
|
185
|
+
* @param {Function} fn Function to evaluate in the frame.
|
|
186
|
+
* @param {...any} args Arguments forwarded to fn.
|
|
187
|
+
* @returns {Promise<any>}
|
|
188
|
+
*/
|
|
189
|
+
async _evaluateInFrame(fn, ...args) {
|
|
190
|
+
if (this._framePath.length === 0) {
|
|
191
|
+
return this._page.evaluate(fn, ...args);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Locate the target frame by walking the frame tree
|
|
195
|
+
const targetUrl = await this._frameLocator()
|
|
196
|
+
.locator('html')
|
|
197
|
+
.evaluate(el => el.ownerDocument.defaultView?.location?.href ?? '');
|
|
198
|
+
|
|
199
|
+
const frame = this._page.frames().find(f => f.url() === targetUrl);
|
|
200
|
+
if (!frame) {
|
|
201
|
+
throw new Error(`GoJSDiagram: could not find frame for path [${this._framePath.join(', ')}]`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return frame.evaluate(fn, args[0]);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Execute the mouse drag sequence that GoJS responds to.
|
|
209
|
+
* Requires ≥50 intermediate mousemove steps.
|
|
210
|
+
*
|
|
211
|
+
* @param {{x: number, y: number}} src Page-level source coordinates.
|
|
212
|
+
* @param {{x: number, y: number}} dest Page-level destination coordinates.
|
|
213
|
+
*/
|
|
214
|
+
async _drag(src, dest) {
|
|
215
|
+
// Move to source first (hover to let GoJS know where we start)
|
|
216
|
+
await this._page.mouse.move(src.x, src.y);
|
|
217
|
+
await this._page.waitForTimeout(200);
|
|
218
|
+
await this._page.mouse.down();
|
|
219
|
+
await this._page.waitForTimeout(600);
|
|
220
|
+
await this._page.mouse.move(dest.x, dest.y, { steps: GOJS_DRAG_STEPS });
|
|
221
|
+
await this._page.waitForTimeout(800);
|
|
222
|
+
await this._page.mouse.up();
|
|
223
|
+
await this._page.waitForTimeout(500);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Drag a node from a GoJS Palette into a GoJS Diagram.
|
|
228
|
+
*
|
|
229
|
+
* @param {{ panelSelector: string, category: string }} source
|
|
230
|
+
* @param {{ panelSelector: string, docX: number, docY: number }} target
|
|
231
|
+
*/
|
|
232
|
+
async dragFromPalette(source, target) {
|
|
233
|
+
const src = await this._resolvePaletteNode(source.panelSelector, source.category);
|
|
234
|
+
const dest = await this._resolveDocPoint(target.panelSelector, target.docX, target.docY);
|
|
235
|
+
await this._drag(src, dest);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Move an existing node within a GoJS Diagram.
|
|
240
|
+
*
|
|
241
|
+
* @param {{ panelSelector: string, key: string|number }} source
|
|
242
|
+
* @param {{ panelSelector: string, docX: number, docY: number }} target
|
|
243
|
+
*/
|
|
244
|
+
async moveNode(source, target) {
|
|
245
|
+
const src = await this._resolveDiagramNode(source.panelSelector, source.key);
|
|
246
|
+
const dest = await this._resolveDocPoint(target.panelSelector, target.docX, target.docY);
|
|
247
|
+
await this._drag(src, dest);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Create a directed link between two GoJS nodes via the model API.
|
|
252
|
+
*
|
|
253
|
+
* This method uses diagram.model.addLinkData() which bypasses the invisible
|
|
254
|
+
* overlay div that intercepts all coordinate-based pointer events on the GoJS
|
|
255
|
+
* canvas. Raw mouse drag approaches (including _drag()) are unreliable for
|
|
256
|
+
* link creation because the port hit areas are small and position-dependent.
|
|
257
|
+
*
|
|
258
|
+
* Usage (TypeScript):
|
|
259
|
+
*
|
|
260
|
+
* await gojs.linkNodes(
|
|
261
|
+
* { panelSelector: '#myDiagram', key: 'TaskA' },
|
|
262
|
+
* { panelSelector: '#myDiagram', key: 'TaskB' },
|
|
263
|
+
* );
|
|
264
|
+
*
|
|
265
|
+
* @param {{ panelSelector: string, key: string|number, port?: string }} source
|
|
266
|
+
* @param {{ panelSelector: string, key: string|number, port?: string }} target
|
|
267
|
+
*/
|
|
268
|
+
async linkNodes(source, target) {
|
|
269
|
+
await this._evaluateInFrame(([sel, fromKey, toKey, fromPort, toPort]) => {
|
|
270
|
+
const container = document.querySelector(sel);
|
|
271
|
+
if (!container) return;
|
|
272
|
+
const diagram = window.go?.Diagram?.fromDiv?.(container);
|
|
273
|
+
if (!diagram) return;
|
|
274
|
+
const linkData = { from: fromKey, to: toKey };
|
|
275
|
+
if (fromPort) linkData.fromPort = fromPort;
|
|
276
|
+
if (toPort) linkData.toPort = toPort;
|
|
277
|
+
const txName = `link ${fromKey} -> ${toKey}`;
|
|
278
|
+
diagram.startTransaction(txName);
|
|
279
|
+
diagram.model.addLinkData(linkData);
|
|
280
|
+
diagram.commitTransaction(txName);
|
|
281
|
+
}, [source.panelSelector, source.key, target.key, source.port ?? '', target.port ?? '']);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Add a new node to a GoJS diagram, placing it at a position relative to an
|
|
286
|
+
* existing anchor node. App-specific properties are copied from the palette
|
|
287
|
+
* template for the given category, skipping GoJS internal fields and the loc
|
|
288
|
+
* property to prevent palette mutation from corrupting subsequent placements.
|
|
289
|
+
*
|
|
290
|
+
* Usage (TypeScript):
|
|
291
|
+
*
|
|
292
|
+
* await _canvas.addNode({
|
|
293
|
+
* key: 'Conditional-0001',
|
|
294
|
+
* category: 'Conditional',
|
|
295
|
+
* diagramSelector: '#divCenter',
|
|
296
|
+
* paletteSelector: '#divLeft',
|
|
297
|
+
* anchorKey: 'InputTask-0001',
|
|
298
|
+
* anchorOffsetX: -9,
|
|
299
|
+
* anchorOffsetY: 137,
|
|
300
|
+
* });
|
|
301
|
+
*
|
|
302
|
+
* @param {{
|
|
303
|
+
* key: string,
|
|
304
|
+
* category: string,
|
|
305
|
+
* diagramSelector: string,
|
|
306
|
+
* paletteSelector?: string,
|
|
307
|
+
* anchorKey?: string,
|
|
308
|
+
* anchorOffsetX?: number,
|
|
309
|
+
* anchorOffsetY?: number,
|
|
310
|
+
* }} opts
|
|
311
|
+
*/
|
|
312
|
+
async addNode(opts) {
|
|
313
|
+
const {
|
|
314
|
+
key,
|
|
315
|
+
category,
|
|
316
|
+
diagramSelector,
|
|
317
|
+
paletteSelector,
|
|
318
|
+
anchorKey,
|
|
319
|
+
anchorOffsetX = 0,
|
|
320
|
+
anchorOffsetY = 0,
|
|
321
|
+
} = opts;
|
|
322
|
+
|
|
323
|
+
// Phase 1: Add the node via GoJS model API (no selection here).
|
|
324
|
+
await this._evaluateInFrame(([diagSel, palSel, nodeKey, nodeCat, anKey, offX, offY]) => {
|
|
325
|
+
const go = window.go;
|
|
326
|
+
if (!go) throw new Error('GoJSDiagram: GoJS not found on window');
|
|
327
|
+
const diagramDiv = document.querySelector(diagSel);
|
|
328
|
+
if (!diagramDiv) throw new Error(`GoJSDiagram: panel not found for "${diagSel}"`);
|
|
329
|
+
const diagram = go.Diagram.fromDiv(diagramDiv);
|
|
330
|
+
if (!diagram) throw new Error(`GoJSDiagram: no GoJS diagram found for "${diagSel}"`);
|
|
331
|
+
|
|
332
|
+
const nodeData = { key: nodeKey, category: nodeCat };
|
|
333
|
+
|
|
334
|
+
// Copy app-specific properties from the palette template, skipping GoJS
|
|
335
|
+
// internals and loc to prevent palette mutation across multiple addNode calls.
|
|
336
|
+
if (palSel) {
|
|
337
|
+
const paletteDiv = document.querySelector(palSel);
|
|
338
|
+
const palette = paletteDiv ? go.Diagram.fromDiv(paletteDiv) : null;
|
|
339
|
+
if (palette) {
|
|
340
|
+
const skip = new Set(['__gohashid', '__gohash', 'loc', 'key']);
|
|
341
|
+
palette.nodes.each((n) => {
|
|
342
|
+
if (n.data?.category === nodeCat) {
|
|
343
|
+
for (const prop in n.data) {
|
|
344
|
+
if (!skip.has(prop)) nodeData[prop] = n.data[prop];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const txName = `add ${nodeCat} node`;
|
|
352
|
+
diagram.startTransaction(txName);
|
|
353
|
+
try {
|
|
354
|
+
diagram.model.addNodeData(nodeData);
|
|
355
|
+
const node = diagram.findNodeForKey(nodeKey);
|
|
356
|
+
if (node && anKey) {
|
|
357
|
+
const anchor = diagram.findNodeForKey(anKey);
|
|
358
|
+
if (!anchor) throw new Error(`GoJSDiagram: anchor node "${anKey}" not found`);
|
|
359
|
+
node.location = new go.Point(anchor.location.x + offX, anchor.location.y + offY);
|
|
360
|
+
}
|
|
361
|
+
} finally {
|
|
362
|
+
diagram.commitTransaction(txName);
|
|
363
|
+
}
|
|
364
|
+
}, [diagramSelector, paletteSelector ?? '', key, category, anchorKey ?? '', anchorOffsetX, anchorOffsetY]);
|
|
365
|
+
|
|
366
|
+
// Phase 2: Wait for GoJS to finish its layout pass after the transaction.
|
|
367
|
+
await this._page.waitForTimeout(300);
|
|
368
|
+
|
|
369
|
+
// Phase 3: Select via GoJS API (fires ChangedSelection for apps that listen to it).
|
|
370
|
+
// Returns debug info so callers can log selection state without cross-origin console issues.
|
|
371
|
+
await this._evaluateInFrame(([diagSel, nodeKey]) => {
|
|
372
|
+
const go = window.go;
|
|
373
|
+
if (!go) return { error: 'no go' };
|
|
374
|
+
const diagramDiv = document.querySelector(diagSel);
|
|
375
|
+
if (!diagramDiv) return { error: 'no div' };
|
|
376
|
+
const diagram = go.Diagram.fromDiv(diagramDiv);
|
|
377
|
+
if (!diagram) return { error: 'no diagram' };
|
|
378
|
+
const node = diagram.findNodeForKey(nodeKey);
|
|
379
|
+
if (!node) return { error: 'node not found', key: nodeKey };
|
|
380
|
+
diagram.select(node);
|
|
381
|
+
return { selected: node.isSelected, selectionCount: diagram.selection.count };
|
|
382
|
+
}, [diagramSelector, key]);
|
|
383
|
+
|
|
384
|
+
// Phase 4: Click at the node's page-level coordinates using Playwright's mouse.
|
|
385
|
+
// This fires the full browser event chain (including any overlay div handlers)
|
|
386
|
+
// for apps that rely on click events rather than GoJS's ChangedSelection.
|
|
387
|
+
// _resolveDiagramNode() computes page-level coords by combining the canvas
|
|
388
|
+
// bounding box (via Playwright) with GoJS transformDocToView() (via frame.evaluate).
|
|
389
|
+
const coords = await this._resolveDiagramNode(diagramSelector, key);
|
|
390
|
+
await this._page.mouse.click(coords.x, coords.y);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = GoJSDiagram;
|
|
395
|
+
|
|
@@ -189,6 +189,11 @@ async function retryWithLLM(skyrampLocator, error) {
|
|
|
189
189
|
throw error
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
const consecutiveCount = skyrampLocator._skyrampPage.incrementConsecutiveLLMCount();
|
|
193
|
+
if (consecutiveCount >= 3) {
|
|
194
|
+
throw new Error(`LLM-based locator retry limit exceeded (${consecutiveCount} consecutive attempts). Please add "data-testid" attributes for more stable locators.`);
|
|
195
|
+
}
|
|
196
|
+
|
|
192
197
|
let locatorStr = skyrampLocator._locator.toString();
|
|
193
198
|
|
|
194
199
|
if (!shouldAttemptImprovement(errorMessage, errorType)) {
|
|
@@ -241,7 +246,8 @@ async function retryWithLLM(skyrampLocator, error) {
|
|
|
241
246
|
|
|
242
247
|
if (locatorCount == 1) {
|
|
243
248
|
const func = newLocator[skyrampLocator.execFname];
|
|
244
|
-
|
|
249
|
+
try {
|
|
250
|
+
const result = await func.call(newLocator, skyrampLocator.execParam, skyrampLocator.execArgs);
|
|
245
251
|
console.log(`✅ SUCCESS! Used selector: ${newLocator} instead of ${skyrampLocator._locator}`);
|
|
246
252
|
console.log(` ${parseErrorStack(error.stack)}`);
|
|
247
253
|
|
|
@@ -249,10 +255,10 @@ async function retryWithLLM(skyrampLocator, error) {
|
|
|
249
255
|
skyrampLocator.locatorCount = 1;
|
|
250
256
|
skyrampLocator._skyrampPage.addLLMChoices(skyrampLocator._locator, newLocator, error.stack);
|
|
251
257
|
return result;
|
|
252
|
-
}
|
|
258
|
+
} catch (innerError) {
|
|
253
259
|
// if it fails, move to the next one
|
|
254
|
-
debug(`retrying with LLM failed at ${skyrampLocator._locator} replaced by {newLocator}`,
|
|
255
|
-
}
|
|
260
|
+
debug(`retrying with LLM failed at ${skyrampLocator._locator} replaced by ${newLocator}`, innerError.name);
|
|
261
|
+
}
|
|
256
262
|
}
|
|
257
263
|
} catch {
|
|
258
264
|
continue
|
|
@@ -400,6 +406,7 @@ class SkyrampPlaywrightLocator {
|
|
|
400
406
|
// if it fails, there could be potentially a hydration issue we can retry after a little wait time
|
|
401
407
|
try {
|
|
402
408
|
return await this.execute().then(result => {
|
|
409
|
+
this._skyrampPage.resetConsecutiveLLMCount();
|
|
403
410
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
404
411
|
});
|
|
405
412
|
} catch (error) {
|
|
@@ -412,6 +419,7 @@ class SkyrampPlaywrightLocator {
|
|
|
412
419
|
|
|
413
420
|
// Is this really necessary?
|
|
414
421
|
await this.execute(true).then(result => {
|
|
422
|
+
this._skyrampPage.resetConsecutiveLLMCount();
|
|
415
423
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
416
424
|
}).catch(() => {
|
|
417
425
|
debug(` failed second time and execute previous locator ${this._previousLocator._locator} again`);
|
|
@@ -458,6 +466,7 @@ class SkyrampPlaywrightLocator {
|
|
|
458
466
|
|
|
459
467
|
// this will likely fail, but we try to get error message generated by playwright
|
|
460
468
|
return this.execute().then(result => {
|
|
469
|
+
this._skyrampPage.resetConsecutiveLLMCount();
|
|
461
470
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
462
471
|
}).catch(error => {
|
|
463
472
|
return this._retryWithLLM(error, this.newMultiLocatorErrorMsg());
|
|
@@ -482,6 +491,7 @@ class SkyrampPlaywrightLocator {
|
|
|
482
491
|
try {
|
|
483
492
|
// then execute the current one
|
|
484
493
|
return await this.execute().then(result => {
|
|
494
|
+
this._skyrampPage.resetConsecutiveLLMCount();
|
|
485
495
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
486
496
|
});
|
|
487
497
|
} catch (error) {
|
|
@@ -490,6 +500,7 @@ class SkyrampPlaywrightLocator {
|
|
|
490
500
|
// wait for some time and re execute
|
|
491
501
|
await this.wait(defaultWaitForTimeout);
|
|
492
502
|
return this.execute(true).then(result => {
|
|
503
|
+
this._skyrampPage.resetConsecutiveLLMCount();
|
|
493
504
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
494
505
|
}).catch(newError => {
|
|
495
506
|
return this._retryWithLLM(newError, this.newPrevHydrationErrorMsg());
|
|
@@ -517,6 +528,7 @@ class SkyrampPlaywrightLocator {
|
|
|
517
528
|
|
|
518
529
|
try {
|
|
519
530
|
return await this.execute().then(result => {
|
|
531
|
+
this._skyrampPage.resetConsecutiveLLMCount();
|
|
520
532
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
521
533
|
});
|
|
522
534
|
} catch (error) {
|
|
@@ -524,6 +536,7 @@ class SkyrampPlaywrightLocator {
|
|
|
524
536
|
debug(`${this._locator} failed at first try. attempting again with some timeout`);
|
|
525
537
|
await this.wait(defaultWaitForTimeout);
|
|
526
538
|
return this.execute(true).then(result=> {
|
|
539
|
+
this._skyrampPage.resetConsecutiveLLMCount();
|
|
527
540
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
528
541
|
}).catch(newError => {
|
|
529
542
|
if (newError.name == "TimeoutError") {
|
|
@@ -1265,6 +1278,18 @@ class SkyrampPlaywrightPage {
|
|
|
1265
1278
|
return result;
|
|
1266
1279
|
}
|
|
1267
1280
|
|
|
1281
|
+
incrementConsecutiveLLMCount() {
|
|
1282
|
+
if (this.consecutiveLLMCount == undefined) {
|
|
1283
|
+
this.consecutiveLLMCount = 0;
|
|
1284
|
+
}
|
|
1285
|
+
this.consecutiveLLMCount++;
|
|
1286
|
+
return this.consecutiveLLMCount;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
resetConsecutiveLLMCount() {
|
|
1290
|
+
this.consecutiveLLMCount = 0;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1268
1293
|
addLLMChoices(originalLocator, newLocator, stack) {
|
|
1269
1294
|
if (this.llmChoices == undefined) {
|
|
1270
1295
|
this.llmChoices = []
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ const MockV2 = require('./classes/MockV2');
|
|
|
20
20
|
const { getValue, getResponseValue, checkSchema, iterate, pushToolEvent, getBaseUrl } = require('./utils');
|
|
21
21
|
const { checkStatusCode, checkRequestPayload } = require('./function');
|
|
22
22
|
const { newSkyrampPlaywrightPage, expect } = require('./classes/SmartPlaywright');
|
|
23
|
+
const GoJSDiagram = require('./classes/GoJSDiagram');
|
|
23
24
|
const {
|
|
24
25
|
workspaceConfigSchema,
|
|
25
26
|
serviceSchema,
|
|
@@ -60,6 +61,7 @@ module.exports = {
|
|
|
60
61
|
pushToolEvent,
|
|
61
62
|
getBaseUrl,
|
|
62
63
|
newSkyrampPlaywrightPage,
|
|
64
|
+
GoJSDiagram,
|
|
63
65
|
expect,
|
|
64
66
|
workspaceConfigSchema,
|
|
65
67
|
serviceSchema,
|