@sudobility/testomniac_runner_service 0.1.39 → 0.1.40

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 (52) hide show
  1. package/CLAUDE.md +85 -331
  2. package/README.md +4 -8
  3. package/dist/analyzer/page-analyzer.d.ts +11 -5
  4. package/dist/analyzer/page-analyzer.d.ts.map +1 -1
  5. package/dist/analyzer/page-analyzer.js +122 -35
  6. package/dist/analyzer/page-analyzer.js.map +1 -1
  7. package/dist/api/client.d.ts +2 -4
  8. package/dist/api/client.d.ts.map +1 -1
  9. package/dist/api/client.js +3 -17
  10. package/dist/api/client.js.map +1 -1
  11. package/dist/index.d.ts +0 -4
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +0 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/orchestrator/runner.d.ts +1 -2
  16. package/dist/orchestrator/runner.d.ts.map +1 -1
  17. package/dist/orchestrator/runner.js +7 -12
  18. package/dist/orchestrator/runner.js.map +1 -1
  19. package/dist/orchestrator/test-case-executor.d.ts.map +1 -1
  20. package/dist/orchestrator/test-case-executor.js +39 -9
  21. package/dist/orchestrator/test-case-executor.js.map +1 -1
  22. package/dist/orchestrator/types.d.ts +0 -15
  23. package/dist/orchestrator/types.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/dist/orchestrator/decomposition.d.ts +0 -10
  26. package/dist/orchestrator/decomposition.d.ts.map +0 -1
  27. package/dist/orchestrator/decomposition.js +0 -658
  28. package/dist/orchestrator/decomposition.js.map +0 -1
  29. package/dist/orchestrator/direct-navigation.d.ts +0 -4
  30. package/dist/orchestrator/direct-navigation.d.ts.map +0 -1
  31. package/dist/orchestrator/direct-navigation.js +0 -47
  32. package/dist/orchestrator/direct-navigation.js.map +0 -1
  33. package/dist/orchestrator/discovery.d.ts +0 -10
  34. package/dist/orchestrator/discovery.d.ts.map +0 -1
  35. package/dist/orchestrator/discovery.js +0 -77
  36. package/dist/orchestrator/discovery.js.map +0 -1
  37. package/dist/orchestrator/expertise.d.ts +0 -5
  38. package/dist/orchestrator/expertise.d.ts.map +0 -1
  39. package/dist/orchestrator/expertise.js +0 -168
  40. package/dist/orchestrator/expertise.js.map +0 -1
  41. package/dist/orchestrator/orchestrator.d.ts +0 -5
  42. package/dist/orchestrator/orchestrator.d.ts.map +0 -1
  43. package/dist/orchestrator/orchestrator.js +0 -172
  44. package/dist/orchestrator/orchestrator.js.map +0 -1
  45. package/dist/orchestrator/page-capture.d.ts +0 -22
  46. package/dist/orchestrator/page-capture.d.ts.map +0 -1
  47. package/dist/orchestrator/page-capture.js +0 -112
  48. package/dist/orchestrator/page-capture.js.map +0 -1
  49. package/dist/orchestrator/test-execution.d.ts +0 -5
  50. package/dist/orchestrator/test-execution.d.ts.map +0 -1
  51. package/dist/orchestrator/test-execution.js +0 -254
  52. package/dist/orchestrator/test-execution.js.map +0 -1
@@ -1,658 +0,0 @@
1
- import { extractActionableItems } from "../extractors";
2
- import { computeDecomposedHashes } from "../browser/page-utils";
3
- import { detectScaffoldRegions, } from "../scanner/component-detector";
4
- import { getBody, getContentBody, getFixedBody, } from "../scanner/html-decomposer";
5
- import { detectPatternsWithInstances } from "../scanner/pattern-detector";
6
- import { toRelativePath } from "../crawler/url-normalizer";
7
- import { PlaywrightAction, ExpectationType, ExpectationSeverity, } from "../domain/types";
8
- const LOG = (...args) => console.warn("[decomposition]", ...args);
9
- const MAX_PAGE_INTERACTION_CASES = 24;
10
- const MAX_REVERSIBLE_CHAINS = 4;
11
- const MAX_TAB_CHAINS_PER_GROUP = 2;
12
- function getStateTransitionPriority(item) {
13
- const role = (item.role || "").toLowerCase();
14
- const tagName = (item.tagName || "").toLowerCase();
15
- const text = `${item.accessibleName || ""} ${item.textContent || ""}`.toLowerCase();
16
- const attributes = item.attributes || {};
17
- if (role === "tab" || attributes["aria-selected"] != null) {
18
- return 100;
19
- }
20
- if (attributes["aria-expanded"] != null ||
21
- attributes["aria-controls"] != null) {
22
- return 95;
23
- }
24
- if (attributes["aria-haspopup"] === "menu" ||
25
- text.includes("menu") ||
26
- role === "menuitem") {
27
- return 90;
28
- }
29
- if (tagName === "summary" ||
30
- text.includes("accordion") ||
31
- text.includes("expand") ||
32
- text.includes("collapse")) {
33
- return 85;
34
- }
35
- if (text.includes("open") ||
36
- text.includes("details") ||
37
- text.includes("show") ||
38
- text.includes("more")) {
39
- return 80;
40
- }
41
- return 0;
42
- }
43
- function prioritizeItemsForExploration(items) {
44
- return [...items].sort((left, right) => {
45
- const delta = getStateTransitionPriority(right) - getStateTransitionPriority(left);
46
- if (delta !== 0) {
47
- return delta;
48
- }
49
- return (left.selector || "").localeCompare(right.selector || "");
50
- });
51
- }
52
- function escapeSelector(selector) {
53
- return selector.replace(/'/g, "\\'");
54
- }
55
- function buildExpectation() {
56
- return {
57
- expectationType: ExpectationType.NoConsoleErrors,
58
- severity: ExpectationSeverity.ShouldPass,
59
- description: "No console errors after interaction",
60
- playwrightCode: "expect(consoleErrors).toHaveLength(0);",
61
- };
62
- }
63
- function buildClickStep(item, pageStateId, description) {
64
- return {
65
- action: {
66
- actionType: PlaywrightAction.Click,
67
- pageStateId,
68
- path: item.selector,
69
- playwrightCode: `await page.click('${escapeSelector(item.selector)}');`,
70
- description,
71
- },
72
- expectations: [buildExpectation()],
73
- description,
74
- continueOnFailure: false,
75
- };
76
- }
77
- function isTabLike(item) {
78
- const role = (item.role || "").toLowerCase();
79
- const attributes = item.attributes || {};
80
- return role === "tab" || attributes["aria-selected"] != null;
81
- }
82
- function isSelfReversibleStateControl(item) {
83
- const tagName = (item.tagName || "").toLowerCase();
84
- const text = `${item.accessibleName || ""} ${item.textContent || ""}`.toLowerCase();
85
- const attributes = item.attributes || {};
86
- return (attributes["aria-expanded"] != null ||
87
- attributes["aria-haspopup"] === "menu" ||
88
- attributes["aria-controls"] != null ||
89
- tagName === "summary" ||
90
- text.includes("open") ||
91
- text.includes("close") ||
92
- text.includes("details") ||
93
- text.includes("show") ||
94
- text.includes("more"));
95
- }
96
- function isExplicitCloseControl(item) {
97
- const text = `${item.accessibleName || ""} ${item.textContent || ""}`.toLowerCase();
98
- return (text.includes("close") ||
99
- text.includes("dismiss") ||
100
- text.includes("cancel") ||
101
- text.includes("hide") ||
102
- text.includes("done"));
103
- }
104
- async function resolveCloseControl(adapter, item, items) {
105
- const controlledId = typeof item.attributes?.["aria-controls"] === "string"
106
- ? String(item.attributes["aria-controls"])
107
- : null;
108
- const closeCandidates = items.filter(candidate => candidate.selector &&
109
- candidate.selector !== item.selector &&
110
- candidate.visible &&
111
- !candidate.disabled &&
112
- isExplicitCloseControl(candidate));
113
- if (closeCandidates.length === 0 || !controlledId) {
114
- return null;
115
- }
116
- const selectors = closeCandidates.map(candidate => candidate.selector);
117
- const matchingSelector = await adapter.evaluate((...args) => {
118
- const triggerSelector = args[0];
119
- const targetId = args[1];
120
- const candidateSelectors = args[2];
121
- try {
122
- const trigger = document.querySelector(triggerSelector);
123
- const controlled = document.getElementById(targetId) ||
124
- document.querySelector(`[aria-labelledby="${targetId}"], [data-panel="${targetId}"]`);
125
- if (!trigger || !controlled)
126
- return null;
127
- for (const selector of candidateSelectors) {
128
- const candidate = document.querySelector(selector);
129
- if (candidate &&
130
- (controlled.contains(candidate) ||
131
- candidate.closest(`[id="${targetId}"]`))) {
132
- return selector;
133
- }
134
- }
135
- }
136
- catch {
137
- return null;
138
- }
139
- return null;
140
- }, item.selector, controlledId, selectors);
141
- if (!matchingSelector) {
142
- return null;
143
- }
144
- return (closeCandidates.find(candidate => candidate.selector === matchingSelector) ?? null);
145
- }
146
- async function buildStateTransitionDefinitions(adapter, items, pageStateId) {
147
- const definitions = [];
148
- const seenTitles = new Set();
149
- const reversibleItems = items
150
- .filter(item => isSelfReversibleStateControl(item) && !isExplicitCloseControl(item))
151
- .slice(0, MAX_REVERSIBLE_CHAINS);
152
- for (const item of reversibleItems) {
153
- const label = item.accessibleName ||
154
- item.textContent ||
155
- item.tagName ||
156
- item.selector?.slice(0, 30) ||
157
- "element";
158
- const title = `state chain: ${label}`;
159
- if (seenTitles.has(title))
160
- continue;
161
- seenTitles.add(title);
162
- const explicitCloseControl = await resolveCloseControl(adapter, item, items);
163
- definitions.push({
164
- title,
165
- actionType: "state_chain",
166
- item,
167
- steps: [
168
- buildClickStep(item, pageStateId, `open ${label}`),
169
- buildClickStep(explicitCloseControl ?? item, pageStateId, `close ${label}`),
170
- ],
171
- });
172
- }
173
- const tabItems = items.filter(isTabLike);
174
- const tabGroups = await resolveTabGroups(adapter, tabItems);
175
- for (const group of tabGroups) {
176
- for (let index = 0; index < Math.min(group.length - 1, MAX_TAB_CHAINS_PER_GROUP); index += 1) {
177
- const first = group[index];
178
- const second = group[index + 1];
179
- const firstLabel = first.accessibleName || first.textContent || first.selector || "tab";
180
- const secondLabel = second.accessibleName || second.textContent || second.selector || "tab";
181
- const title = `tab chain: ${firstLabel} -> ${secondLabel}`;
182
- if (seenTitles.has(title))
183
- continue;
184
- seenTitles.add(title);
185
- definitions.push({
186
- title,
187
- actionType: "tab_chain",
188
- item: first,
189
- steps: [
190
- buildClickStep(first, pageStateId, `activate ${firstLabel}`),
191
- buildClickStep(second, pageStateId, `activate ${secondLabel}`),
192
- ],
193
- });
194
- }
195
- }
196
- return definitions;
197
- }
198
- async function resolveTabGroups(adapter, items) {
199
- if (items.length < 2) {
200
- return items.length > 0 ? [items] : [];
201
- }
202
- const metadata = await adapter.evaluate((...args) => {
203
- const selectors = args[0];
204
- return selectors.map((selector, index) => {
205
- try {
206
- const el = document.querySelector(selector);
207
- if (!el) {
208
- return { selector, groupKey: "__missing__", order: index };
209
- }
210
- const container = el.closest('[role="tablist"]') ||
211
- el.closest("[data-tabs]") ||
212
- el.closest(".tabs") ||
213
- el.closest('[role="toolbar"]') ||
214
- el.parentElement;
215
- const groupKey = container?.getAttribute("id") ||
216
- container?.getAttribute("aria-label") ||
217
- container?.getAttribute("data-tabs") ||
218
- container?.tagName ||
219
- "__ungrouped__";
220
- const siblings = container
221
- ? Array.from(container.querySelectorAll('[role="tab"], [aria-selected], button, a'))
222
- : [];
223
- const order = siblings.findIndex(candidate => candidate === el);
224
- return {
225
- selector,
226
- groupKey,
227
- order: order >= 0 ? order : index,
228
- };
229
- }
230
- catch {
231
- return { selector, groupKey: "__error__", order: index };
232
- }
233
- });
234
- }, items.map(item => item.selector));
235
- const itemBySelector = new Map(items.map(item => [item.selector, item]));
236
- const groups = new Map();
237
- for (const entry of metadata) {
238
- const item = itemBySelector.get(entry.selector);
239
- if (!item)
240
- continue;
241
- const bucket = groups.get(entry.groupKey) ?? [];
242
- bucket.push({ item, order: entry.order });
243
- groups.set(entry.groupKey, bucket);
244
- }
245
- return Array.from(groups.values())
246
- .map(group => group
247
- .sort((left, right) => left.order - right.order)
248
- .map(entry => entry.item))
249
- .filter(group => group.length >= 2);
250
- }
251
- function dedupeDefinitions(definitions) {
252
- const seen = new Set();
253
- const deduped = [];
254
- for (const definition of definitions) {
255
- if (seen.has(definition.title))
256
- continue;
257
- seen.add(definition.title);
258
- deduped.push(definition);
259
- }
260
- return deduped;
261
- }
262
- function inferFillValue(item) {
263
- const inputType = (item.inputType || "").toLowerCase();
264
- const label = `${item.accessibleName || ""} ${item.textContent || ""}`.toLowerCase();
265
- if (inputType === "email" || label.includes("email")) {
266
- return "testomniac@example.com";
267
- }
268
- if (inputType === "tel" || label.includes("phone")) {
269
- return "4155550100";
270
- }
271
- if (inputType === "number") {
272
- return "1";
273
- }
274
- if (inputType === "search" || label.includes("search")) {
275
- return "test";
276
- }
277
- if (inputType === "url" ||
278
- label.includes("url") ||
279
- label.includes("website")) {
280
- return "https://example.com";
281
- }
282
- if (inputType === "password") {
283
- return "Testomniac123!";
284
- }
285
- return "Testomniac";
286
- }
287
- async function resolveSelectValue(adapter, selector) {
288
- return adapter.evaluate((...args) => {
289
- const targetSelector = args[0];
290
- try {
291
- const select = document.querySelector(targetSelector);
292
- if (!(select instanceof HTMLSelectElement)) {
293
- return null;
294
- }
295
- const options = Array.from(select.options).filter(option => !option.disabled);
296
- const preferred = options.find(option => option.value && option.value !== select.value) ||
297
- options.find(option => option.value) ||
298
- options.find(option => option.textContent?.trim());
299
- return preferred?.value || null;
300
- }
301
- catch {
302
- return null;
303
- }
304
- }, selector);
305
- }
306
- async function buildGeneratedTestCase(adapter, item, pageStateId) {
307
- const actionKind = item.actionKind || "click";
308
- const label = item.accessibleName ||
309
- item.tagName ||
310
- item.selector?.slice(0, 30) ||
311
- "element";
312
- let actionType;
313
- let playwrightAction;
314
- let playwrightCode;
315
- let value;
316
- if (actionKind === "navigate" || actionKind === "click") {
317
- actionType = "click";
318
- playwrightAction = PlaywrightAction.Click;
319
- playwrightCode = `await page.click('${escapeSelector(item.selector)}');`;
320
- }
321
- else if (actionKind === "fill") {
322
- actionType = "fill";
323
- playwrightAction = PlaywrightAction.Fill;
324
- value = inferFillValue(item);
325
- playwrightCode = `await page.fill('${escapeSelector(item.selector)}', '${value.replace(/'/g, "\\'")}');`;
326
- }
327
- else if (actionKind === "select") {
328
- actionType = "select";
329
- playwrightAction = PlaywrightAction.SelectOption;
330
- value = (await resolveSelectValue(adapter, item.selector)) ?? undefined;
331
- if (!value) {
332
- return null;
333
- }
334
- playwrightCode = `await page.selectOption('${escapeSelector(item.selector)}', '${value.replace(/'/g, "\\'")}');`;
335
- }
336
- else if (actionKind === "radio_select") {
337
- actionType = "radio_select";
338
- playwrightAction = PlaywrightAction.Click;
339
- playwrightCode = `await page.click('${escapeSelector(item.selector)}');`;
340
- }
341
- else {
342
- actionType = "click";
343
- playwrightAction = PlaywrightAction.Click;
344
- playwrightCode = `await page.click('${escapeSelector(item.selector)}');`;
345
- }
346
- const steps = [
347
- {
348
- action: {
349
- actionType: playwrightAction,
350
- pageStateId,
351
- path: item.selector,
352
- value,
353
- playwrightCode,
354
- description: `${actionType} on ${label}`,
355
- },
356
- expectations: [buildExpectation()],
357
- description: `${actionType} on ${label}`,
358
- continueOnFailure: false,
359
- },
360
- ];
361
- return {
362
- title: `${actionType}: ${label}`,
363
- actionType,
364
- item,
365
- steps,
366
- };
367
- }
368
- async function classifyItemsByScaffold(adapter, items, scaffolds) {
369
- if (items.length === 0 || scaffolds.length === 0) {
370
- return { pageItems: items, scaffoldItems: new Map() };
371
- }
372
- const assignments = await adapter.evaluate((...args) => {
373
- const selectors = args[0];
374
- const scaffoldSelectors = args[1];
375
- return selectors.map(selector => {
376
- try {
377
- const el = document.querySelector(selector);
378
- if (!el)
379
- return null;
380
- for (const scaffold of scaffoldSelectors) {
381
- try {
382
- const scaffoldEl = document.querySelector(scaffold.selector);
383
- if (scaffoldEl &&
384
- (el === scaffoldEl || scaffoldEl.contains(el))) {
385
- return scaffold.scaffoldId;
386
- }
387
- }
388
- catch {
389
- // Ignore invalid scaffold selector.
390
- }
391
- }
392
- }
393
- catch {
394
- // Ignore invalid item selector.
395
- }
396
- return null;
397
- });
398
- }, items.map(item => item.selector), scaffolds.map(scaffold => ({
399
- scaffoldId: scaffold.scaffoldId,
400
- selector: scaffold.selector,
401
- })));
402
- const scaffoldItems = new Map();
403
- const pageItems = [];
404
- for (const [index, item] of items.entries()) {
405
- const scaffoldId = assignments[index];
406
- if (typeof scaffoldId === "number") {
407
- const existing = scaffoldItems.get(scaffoldId) ?? [];
408
- existing.push(item);
409
- scaffoldItems.set(scaffoldId, existing);
410
- }
411
- else {
412
- pageItems.push(item);
413
- }
414
- }
415
- return { pageItems, scaffoldItems };
416
- }
417
- function findExistingSuiteId(suites, scaffoldId) {
418
- const suite = suites.find(item => item.scaffoldId === scaffoldId);
419
- return suite?.id ?? null;
420
- }
421
- /**
422
- * Process a decomposition job: extract actionable items from the live page
423
- * and generate click/hover test cases. Returns created test case IDs.
424
- */
425
- export async function processDecompositionJob(job, adapter, config, api, events) {
426
- LOG(`Processing job ${job.id} for pageState ${job.pageStateId}`);
427
- const pageState = await api.getPageState(job.pageStateId);
428
- if (!pageState) {
429
- throw new Error(`Page state ${job.pageStateId} not found`);
430
- }
431
- LOG(`Page state found: pageId=${pageState.pageId}`);
432
- const page = await api.getPage(pageState.pageId);
433
- if (!page) {
434
- throw new Error(`Page ${pageState.pageId} not found`);
435
- }
436
- LOG(`Page found: relativePath=${page.relativePath}`);
437
- const targetUrl = new URL(page.relativePath, config.baseUrl).toString();
438
- const currentUrl = await adapter.getUrl();
439
- if (toRelativePath(currentUrl) !== toRelativePath(targetUrl)) {
440
- LOG(`Navigating to ${targetUrl} for decomposition job ${job.id}`);
441
- await adapter.goto(targetUrl, { waitUntil: "networkidle0" });
442
- }
443
- // Extract actionable items from the live page
444
- const currentHtml = await adapter.content();
445
- const items = await extractActionableItems(adapter);
446
- LOG(`Extracted ${items.length} actionable items:`, items.map(i => ({
447
- selector: i.selector?.slice(0, 60),
448
- actionKind: i.actionKind,
449
- tagName: i.tagName,
450
- role: i.role,
451
- accessibleName: i.accessibleName?.slice(0, 40),
452
- visible: i.visible,
453
- disabled: i.disabled,
454
- })));
455
- // Detect scaffolds (header, footer, breadcrumb, etc.)
456
- const scaffolds = await detectScaffoldRegions(adapter);
457
- LOG(`Detected ${scaffolds.length} scaffolds:`, scaffolds.map(s => ({ type: s.type, selector: s.selector })));
458
- // Persist scaffolds and link to page state
459
- const persistedScaffolds = [];
460
- if (scaffolds.length > 0) {
461
- const scaffoldIds = [];
462
- for (const scaffold of scaffolds) {
463
- try {
464
- const saved = await api.findOrCreateScaffold({
465
- runnerId: config.runnerId,
466
- type: scaffold.type,
467
- hash: scaffold.hash,
468
- html: scaffold.outerHtml,
469
- });
470
- scaffoldIds.push(saved.id);
471
- persistedScaffolds.push({ ...scaffold, scaffoldId: saved.id });
472
- LOG(`Scaffold saved: type=${scaffold.type} id=${saved.id}`);
473
- }
474
- catch (err) {
475
- LOG(`Failed to save scaffold ${scaffold.type}:`, err instanceof Error ? err.message : err);
476
- }
477
- }
478
- if (scaffoldIds.length > 0) {
479
- try {
480
- await api.linkPageStateScaffolds(job.pageStateId, scaffoldIds);
481
- LOG(`Linked ${scaffoldIds.length} scaffolds to page state ${job.pageStateId}`);
482
- }
483
- catch (err) {
484
- LOG(`Failed to link scaffolds:`, err instanceof Error ? err.message : err);
485
- }
486
- }
487
- }
488
- const patterns = await detectPatternsWithInstances(adapter);
489
- const bodyHtml = getBody(currentHtml);
490
- const { contentBody } = getContentBody(bodyHtml, scaffolds);
491
- const patternInstances = patterns.flatMap(pattern => pattern.instances);
492
- const { fixedBody } = getFixedBody(contentBody, patternInstances);
493
- const decomposedHashes = await computeDecomposedHashes(fixedBody, scaffolds, patterns);
494
- const existingDecomposedState = await api.findMatchingPageStateDecomposed(page.id, decomposedHashes, config.sizeClass);
495
- await api.updatePageStateDecomposedHashes(pageState.id, decomposedHashes);
496
- await api.insertPageStatePatterns(pageState.id, patterns.map(pattern => ({
497
- type: pattern.type,
498
- selector: pattern.selector,
499
- count: pattern.count,
500
- })));
501
- if (existingDecomposedState && existingDecomposedState.id !== pageState.id) {
502
- LOG(`Skipping decomposition for pageState ${pageState.id}; matched existing decomposed state ${existingDecomposedState.id}`);
503
- return [];
504
- }
505
- // Filter to visible, enabled, interactive items
506
- const interactiveItems = items.filter(i => i.visible && !i.disabled && i.selector);
507
- LOG(`${interactiveItems.length} items after filtering (visible, enabled, has selector)`);
508
- if (interactiveItems.length === 0) {
509
- LOG("No interactive items found — skipping decomposition");
510
- return [];
511
- }
512
- const { pageItems, scaffoldItems } = await classifyItemsByScaffold(adapter, interactiveItems, persistedScaffolds);
513
- LOG(`Classified ${pageItems.length} page items and ${Array.from(scaffoldItems.values()).reduce((count, value) => count + value.length, 0)} scaffold items`);
514
- const existingSuites = await api.getTestSuitesByRunner(config.runnerId);
515
- const existingCases = await api.getTestCasesByRunner(config.runnerId);
516
- let pageSuiteId = null;
517
- // Generate one test case per actionable item (cap at 20 to avoid explosion)
518
- const createdIds = [];
519
- const prioritizedPageItems = prioritizeItemsForExploration(pageItems);
520
- const pageChainDefinitions = await buildStateTransitionDefinitions(adapter, prioritizedPageItems, pageState.id);
521
- const pageSingleDefinitions = (await Promise.all(prioritizedPageItems
522
- .slice(0, MAX_PAGE_INTERACTION_CASES)
523
- .map(item => buildGeneratedTestCase(adapter, item, pageState.id)))).filter((definition) => Boolean(definition));
524
- const pageDefinitions = dedupeDefinitions([
525
- ...pageChainDefinitions,
526
- ...pageSingleDefinitions,
527
- ]).slice(0, MAX_PAGE_INTERACTION_CASES);
528
- if (pageDefinitions.length > 0) {
529
- const pageSuite = await api.insertTestSuite(config.runnerId, {
530
- title: `Page State #${job.pageStateId}`,
531
- description: `Auto-generated test suite for page state ${job.pageStateId} on ${page.relativePath}`,
532
- startingPageStateId: job.pageStateId,
533
- startingPath: page.relativePath,
534
- sizeClass: config.sizeClass,
535
- priority: 3,
536
- suite_tags: ["auto-generated", "page-interactions"],
537
- decompositionJobId: job.id,
538
- });
539
- pageSuiteId = pageSuite.id;
540
- LOG(`Created test suite: id=${pageSuite.id}, title=${pageSuite.title}`);
541
- events.onTestSuiteCreated({
542
- suiteId: pageSuite.id,
543
- title: pageSuite.title,
544
- });
545
- }
546
- for (const [index, definition] of pageDefinitions.entries()) {
547
- if (!pageSuiteId)
548
- break;
549
- const testCase = {
550
- title: definition.title,
551
- type: "interaction",
552
- sizeClass: config.sizeClass,
553
- suite_tags: ["auto-generated", "mouse-scanning", "page-interactions"],
554
- page_id: page.id,
555
- priority: 3,
556
- startingPageStateId: pageState.id,
557
- startingPath: page.relativePath,
558
- steps: definition.steps,
559
- globalExpectations: [],
560
- };
561
- LOG(`Creating page test case ${index + 1}/${pageDefinitions.length}: "${testCase.title}" selector=${definition.item.selector?.slice(0, 60)}`);
562
- const tc = await api.insertTestCase(config.runnerId, testCase, pageSuiteId);
563
- createdIds.push(tc.id);
564
- for (const [stepIndex, step] of definition.steps.entries()) {
565
- await api.createTestAction({
566
- testCaseId: tc.id,
567
- stepOrder: stepIndex,
568
- actionType: step.action.actionType,
569
- pageStateId: step.action.pageStateId,
570
- elementIdentityId: step.action.elementIdentityId,
571
- containerType: step.action.containerType,
572
- containerElementIdentityId: step.action.containerElementIdentityId,
573
- value: step.action.value,
574
- path: step.action.path,
575
- playwrightCode: step.action.playwrightCode,
576
- description: step.description,
577
- expectations: step.expectations,
578
- continueOnFailure: step.continueOnFailure,
579
- });
580
- }
581
- }
582
- for (const scaffold of persistedScaffolds) {
583
- const itemsForScaffold = scaffoldItems.get(scaffold.scaffoldId) ?? [];
584
- if (itemsForScaffold.length === 0) {
585
- continue;
586
- }
587
- let scaffoldSuiteId = findExistingSuiteId(existingSuites, scaffold.scaffoldId);
588
- if (!scaffoldSuiteId) {
589
- const suite = await api.insertTestSuite(config.runnerId, {
590
- title: `Shared Scaffold: ${scaffold.type}`,
591
- description: `Auto-generated shared scaffold suite for ${scaffold.type}`,
592
- startingPageStateId: job.pageStateId,
593
- startingPath: page.relativePath,
594
- sizeClass: config.sizeClass,
595
- scaffoldId: scaffold.scaffoldId,
596
- scaffoldType: scaffold.type,
597
- priority: 2,
598
- suite_tags: ["auto-generated", "shared-scaffold"],
599
- });
600
- scaffoldSuiteId = suite.id;
601
- existingSuites.push(suite);
602
- LOG(`Created scaffold suite ${suite.id} for scaffold ${scaffold.scaffoldId}`);
603
- events.onTestSuiteCreated({ suiteId: suite.id, title: suite.title });
604
- }
605
- const prioritizedScaffoldItems = prioritizeItemsForExploration(itemsForScaffold);
606
- const scaffoldChainDefinitions = await buildStateTransitionDefinitions(adapter, prioritizedScaffoldItems, pageState.id);
607
- const scaffoldSingleDefinitions = (await Promise.all(prioritizedScaffoldItems
608
- .slice(0, MAX_PAGE_INTERACTION_CASES)
609
- .map(item => buildGeneratedTestCase(adapter, item, pageState.id)))).filter((definition) => Boolean(definition));
610
- const scaffoldDefinitions = dedupeDefinitions([
611
- ...scaffoldChainDefinitions,
612
- ...scaffoldSingleDefinitions,
613
- ]).slice(0, MAX_PAGE_INTERACTION_CASES);
614
- for (const definition of scaffoldDefinitions) {
615
- const duplicate = existingCases.find(testCase => testCase.scaffoldId === scaffold.scaffoldId &&
616
- testCase.title === definition.title);
617
- if (duplicate) {
618
- continue;
619
- }
620
- const testCase = {
621
- title: definition.title,
622
- type: "interaction",
623
- sizeClass: config.sizeClass,
624
- suite_tags: ["auto-generated", "mouse-scanning", "shared-scaffold"],
625
- page_id: page.id,
626
- scaffoldId: scaffold.scaffoldId,
627
- priority: 2,
628
- startingPageStateId: pageState.id,
629
- startingPath: page.relativePath,
630
- steps: definition.steps,
631
- globalExpectations: [],
632
- };
633
- const tc = await api.insertTestCase(config.runnerId, testCase, scaffoldSuiteId);
634
- existingCases.push(tc);
635
- createdIds.push(tc.id);
636
- for (const [stepIndex, step] of definition.steps.entries()) {
637
- await api.createTestAction({
638
- testCaseId: tc.id,
639
- stepOrder: stepIndex,
640
- actionType: step.action.actionType,
641
- pageStateId: step.action.pageStateId,
642
- elementIdentityId: step.action.elementIdentityId,
643
- containerType: step.action.containerType,
644
- containerElementIdentityId: step.action.containerElementIdentityId,
645
- value: step.action.value,
646
- path: step.action.path,
647
- playwrightCode: step.action.playwrightCode,
648
- description: step.description,
649
- expectations: step.expectations,
650
- continueOnFailure: step.continueOnFailure,
651
- });
652
- }
653
- }
654
- }
655
- LOG(`Decomposition complete: created ${createdIds.length} test cases`);
656
- return createdIds;
657
- }
658
- //# sourceMappingURL=decomposition.js.map