@sun-asterisk/sungen 2.2.2 → 2.3.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/README.md +66 -37
- package/dist/cli/commands/update.d.ts +3 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +21 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +2 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +17 -3
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +12 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +12 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.js +95 -57
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +9 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +32 -0
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js +56 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/table-patterns.js +8 -5
- package/dist/generators/test-generator/patterns/table-patterns.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +16 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts +13 -0
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -0
- package/dist/orchestrator/ai-rules-updater.js +157 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +2 -27
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts +1 -0
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +70 -3
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +18 -9
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -4
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -6
- package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +22 -9
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +170 -24
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +118 -12
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +16 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +124 -71
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -5
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +12 -4
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +11 -6
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +22 -9
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +170 -24
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +93 -12
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +16 -2
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +124 -72
- package/dist/orchestrator/templates/readme.md +13 -8
- package/package.json +1 -1
- package/src/cli/commands/update.ts +18 -0
- package/src/cli/index.ts +3 -1
- package/src/generators/gherkin-parser/index.ts +20 -3
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/attribute-assertion.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-value-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +12 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +12 -1
- package/src/generators/test-generator/patterns/assertion-patterns.ts +106 -65
- package/src/generators/test-generator/patterns/index.ts +41 -0
- package/src/generators/test-generator/patterns/interaction-patterns.ts +58 -1
- package/src/generators/test-generator/patterns/table-patterns.ts +8 -5
- package/src/generators/test-generator/utils/selector-resolver.ts +16 -0
- package/src/orchestrator/ai-rules-updater.ts +139 -0
- package/src/orchestrator/project-initializer.ts +2 -32
- package/src/orchestrator/screen-manager.ts +72 -3
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +18 -9
- package/src/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -4
- package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -6
- package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +22 -9
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +170 -24
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +118 -12
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +16 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +124 -71
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +13 -5
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +12 -4
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +11 -6
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +22 -9
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +170 -24
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +93 -12
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +16 -2
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +124 -72
- package/src/orchestrator/templates/readme.md +13 -8
- package/docs/gherkin standards/gherkin-core-standard.md +0 -428
- package/docs/gherkin standards/gherkin-core-standard.vi.md +0 -513
- package/docs/gherkin-dictionary.md +0 -1071
- package/docs/makeauth.md +0 -225
|
@@ -6,6 +6,30 @@ import { StepPattern, StepTemplateData } from './types';
|
|
|
6
6
|
* Uses template engine for framework-agnostic code generation
|
|
7
7
|
*/
|
|
8
8
|
export const assertionPatterns: StepPattern[] = [
|
|
9
|
+
// Browser alert text assertion: "see [Are you sure?] alert"
|
|
10
|
+
{
|
|
11
|
+
name: 'alert-text-assertion',
|
|
12
|
+
matcher: (step: ParsedStep) =>
|
|
13
|
+
(step.text.includes('see') || step.text.includes('sees')) &&
|
|
14
|
+
step.elementType === 'alert' &&
|
|
15
|
+
!!step.selectorRef,
|
|
16
|
+
resolver: (step, context): StepTemplateData => {
|
|
17
|
+
let dataValue = step.selectorRef || '';
|
|
18
|
+
if (step.dataRef) {
|
|
19
|
+
try {
|
|
20
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
21
|
+
} catch {
|
|
22
|
+
dataValue = `\${${step.dataRef}}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
templateName: 'alert-text-assertion',
|
|
27
|
+
data: { dataValue, stepCounter: context.stepCounter },
|
|
28
|
+
comment: `Assert browser alert contains "${step.selectorRef}"`,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
priority: 21,
|
|
32
|
+
},
|
|
9
33
|
// Column cell assertion: "see [Department] column 1 with {{value}}" -> check table cell text
|
|
10
34
|
{
|
|
11
35
|
name: 'column-cell-assertion',
|
|
@@ -218,6 +242,7 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
218
242
|
// Pattern: Then User see [error] message with {{fail_message}}
|
|
219
243
|
// If selector YAML has empty value, uses variable-only matching
|
|
220
244
|
// If selector has value, combines both (static text + variable)
|
|
245
|
+
// Input types → toHaveValue, everything else → toHaveText
|
|
221
246
|
{
|
|
222
247
|
name: 'see-with-variable',
|
|
223
248
|
matcher: (step: ParsedStep) =>
|
|
@@ -227,6 +252,12 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
227
252
|
!!step.dataRef &&
|
|
228
253
|
step.text.includes('with'),
|
|
229
254
|
generator: (step, context) => {
|
|
255
|
+
// Input element types use toHaveValue instead of toHaveText
|
|
256
|
+
const INPUT_TYPES = new Set([
|
|
257
|
+
'field', 'textarea', 'search', 'dropdown', 'slider', 'date-picker',
|
|
258
|
+
'input', 'textbox', 'editor', 'select', 'combobox',
|
|
259
|
+
]);
|
|
260
|
+
|
|
230
261
|
// Resolve data variable value
|
|
231
262
|
let dataValue: string;
|
|
232
263
|
try {
|
|
@@ -235,19 +266,55 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
235
266
|
dataValue = `\${${step.dataRef}}`;
|
|
236
267
|
}
|
|
237
268
|
|
|
238
|
-
//
|
|
239
|
-
|
|
269
|
+
// Resolve selector from YAML with feature context
|
|
270
|
+
let resolved: any = {};
|
|
271
|
+
try {
|
|
272
|
+
resolved = context.selectorResolver.resolveSelector(
|
|
273
|
+
step.selectorRef!,
|
|
274
|
+
context.featureName,
|
|
275
|
+
step.elementType,
|
|
276
|
+
step.nth
|
|
277
|
+
);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Selector not in YAML or context issue - will use variable-only
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- Attribute assertion: toHaveAttribute ---
|
|
283
|
+
// When selector YAML has `attribute` field (e.g., attribute: 'src', 'href')
|
|
284
|
+
if (resolved.attribute) {
|
|
285
|
+
const code = context.templateEngine.renderStep('attribute-assertion', {
|
|
286
|
+
...resolved,
|
|
287
|
+
dataValue,
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
code,
|
|
291
|
+
comment: `Assert ${step.selectorRef} ${resolved.attribute} matches ${step.dataRef}`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Input types: toHaveValue ---
|
|
296
|
+
if (step.elementType && INPUT_TYPES.has(step.elementType)) {
|
|
297
|
+
const code = context.templateEngine.renderStep('have-value-assertion', {
|
|
298
|
+
...resolved,
|
|
299
|
+
dataValue,
|
|
300
|
+
});
|
|
301
|
+
return {
|
|
302
|
+
code,
|
|
303
|
+
comment: `Assert ${step.selectorRef} has value ${step.dataRef}`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --- Label-value pattern: "User see [X] label with {{Y}}" ---
|
|
240
308
|
if (step.elementType === 'label' && step.dataRef) {
|
|
241
|
-
// Check if selector override has empty value — if so, omit label from regex
|
|
242
309
|
let label: string | undefined = step.selectorRef;
|
|
243
310
|
try {
|
|
244
|
-
const
|
|
311
|
+
const labelResolved = context.selectorResolver.resolveSelector(
|
|
245
312
|
step.selectorRef!,
|
|
246
313
|
context.featureName,
|
|
247
314
|
step.elementType,
|
|
248
315
|
step.nth
|
|
249
316
|
);
|
|
250
|
-
if (
|
|
317
|
+
if (labelResolved.value !== undefined && labelResolved.value.trim() === '') {
|
|
251
318
|
label = undefined;
|
|
252
319
|
}
|
|
253
320
|
} catch {
|
|
@@ -264,93 +331,67 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
264
331
|
};
|
|
265
332
|
}
|
|
266
333
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
resolved = context.selectorResolver.resolveSelector(
|
|
271
|
-
step.selectorRef!,
|
|
272
|
-
context.featureName,
|
|
273
|
-
step.elementType,
|
|
274
|
-
step.nth
|
|
275
|
-
);
|
|
276
|
-
} catch (error) {
|
|
277
|
-
// Selector not in YAML or context issue - will use variable-only
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const escapedVariable = dataValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
281
|
-
|
|
282
|
-
// Check if it's a locator strategy - use locator with filter
|
|
283
|
-
if (resolved.strategy === 'locator') {
|
|
284
|
-
const code = context.templateEngine.renderStep('visible-with-locator-variable-assertion', {
|
|
285
|
-
...resolved,
|
|
334
|
+
// --- Dialog role: check heading inside dialog ---
|
|
335
|
+
if (resolved.strategy === 'role' && resolved.role === 'dialog') {
|
|
336
|
+
const code = context.templateEngine.renderStep('visible-dialog-heading-assertion', {
|
|
286
337
|
dataValue,
|
|
287
|
-
nth: resolved.nth,
|
|
288
338
|
});
|
|
289
339
|
return {
|
|
290
340
|
code,
|
|
291
|
-
comment: `Assert ${step.selectorRef} with
|
|
341
|
+
comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
|
|
292
342
|
};
|
|
293
343
|
}
|
|
294
344
|
|
|
295
|
-
//
|
|
296
|
-
if (resolved.strategy === 'role' && resolved.role) {
|
|
297
|
-
// Dialog role: check heading inside dialog instead of filter on full text content
|
|
298
|
-
if (resolved.role === 'dialog') {
|
|
299
|
-
const code = context.templateEngine.renderStep('visible-dialog-heading-assertion', {
|
|
300
|
-
dataValue,
|
|
301
|
-
});
|
|
302
|
-
return {
|
|
303
|
-
code,
|
|
304
|
-
comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
345
|
+
// --- Image role: images have no text, use name ---
|
|
346
|
+
if (resolved.strategy === 'role' && resolved.role === 'img') {
|
|
308
347
|
const hasName = resolved.name && resolved.name.trim();
|
|
309
|
-
// For img role: images have no text content, so put dataValue in name instead of filter
|
|
310
|
-
const isImgRole = resolved.role === 'img';
|
|
311
348
|
const code = context.templateEngine.renderStep('visible-with-role-variable-assertion', {
|
|
312
349
|
role: resolved.role,
|
|
313
|
-
name:
|
|
314
|
-
dataValue: isImgRole ? undefined : dataValue,
|
|
350
|
+
name: hasName ? resolved.name : dataValue,
|
|
315
351
|
exact: resolved.exact || false,
|
|
316
352
|
nth: resolved.nth,
|
|
317
353
|
});
|
|
318
354
|
return {
|
|
319
355
|
code,
|
|
320
|
-
comment: `Assert ${step.selectorRef} with
|
|
356
|
+
comment: `Assert ${step.selectorRef} image with name ${step.dataRef}`,
|
|
321
357
|
};
|
|
322
358
|
}
|
|
323
359
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const code = context.templateEngine.renderStep('visible-with-value-assertion', {
|
|
331
|
-
value: selectorValue,
|
|
332
|
-
dataValue,
|
|
333
|
-
dataRef: step.dataRef,
|
|
334
|
-
nth: resolved.nth,
|
|
335
|
-
exact: resolved.exact || false,
|
|
360
|
+
// --- Everything else: toHaveText (exact full match) ---
|
|
361
|
+
// Locator strategy
|
|
362
|
+
if (resolved.strategy === 'locator') {
|
|
363
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
364
|
+
...resolved,
|
|
365
|
+
expectedText: dataValue,
|
|
336
366
|
});
|
|
337
367
|
return {
|
|
338
368
|
code,
|
|
339
|
-
comment: `Assert ${step.selectorRef}
|
|
369
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
340
370
|
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Role-based selector
|
|
374
|
+
if (resolved.strategy === 'role' && resolved.role) {
|
|
375
|
+
const hasName = resolved.name && resolved.name.trim();
|
|
376
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
377
|
+
...resolved,
|
|
378
|
+
expectedText: dataValue,
|
|
348
379
|
});
|
|
349
380
|
return {
|
|
350
381
|
code,
|
|
351
|
-
comment: `Assert ${step.selectorRef}
|
|
382
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
352
383
|
};
|
|
353
384
|
}
|
|
385
|
+
|
|
386
|
+
// Text/placeholder/testid/other strategies
|
|
387
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
388
|
+
...resolved,
|
|
389
|
+
expectedText: dataValue,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
code,
|
|
393
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
394
|
+
};
|
|
354
395
|
},
|
|
355
396
|
priority: 13,
|
|
356
397
|
},
|
|
@@ -72,6 +72,14 @@ export class PatternRegistry {
|
|
|
72
72
|
// Prefer resolver (framework-agnostic) over generator (legacy)
|
|
73
73
|
if (pattern.resolver) {
|
|
74
74
|
const resolved = pattern.resolver(step, context);
|
|
75
|
+
|
|
76
|
+
// Auto-inject parent scoping if step has parentRef
|
|
77
|
+
if (step.parentRef && step.parentType) {
|
|
78
|
+
resolved.data.parentLocator = PatternRegistry.resolveParentLocator(
|
|
79
|
+
step.parentRef, step.parentType, context
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
const code = context.templateEngine.renderStep(resolved.templateName, resolved.data);
|
|
76
84
|
return {
|
|
77
85
|
code,
|
|
@@ -86,6 +94,39 @@ export class PatternRegistry {
|
|
|
86
94
|
return null;
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Resolve parent scoping to a Playwright locator string.
|
|
99
|
+
* Tries YAML lookup first, falls back to auto-infer from parentType.
|
|
100
|
+
*
|
|
101
|
+
* Parent type → Playwright role:
|
|
102
|
+
* table → 'table', list → 'list', section → 'region',
|
|
103
|
+
* dialog → 'dialog', form → 'form'
|
|
104
|
+
*/
|
|
105
|
+
private static resolveParentLocator(
|
|
106
|
+
parentRef: string, parentType: string, context: PatternContext
|
|
107
|
+
): string {
|
|
108
|
+
// Try resolving from selectors YAML
|
|
109
|
+
try {
|
|
110
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
111
|
+
parentRef, context.featureName, parentType, 0
|
|
112
|
+
);
|
|
113
|
+
return context.renderLocator(resolved);
|
|
114
|
+
} catch {
|
|
115
|
+
// Fallback: auto-infer from parentType + parentRef as accessible name
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const roleMap: Record<string, string> = {
|
|
119
|
+
table: 'table',
|
|
120
|
+
list: 'list',
|
|
121
|
+
section: 'region',
|
|
122
|
+
dialog: 'dialog',
|
|
123
|
+
form: 'form',
|
|
124
|
+
};
|
|
125
|
+
const role = roleMap[parentType] || parentType;
|
|
126
|
+
const escapedName = parentRef.replace(/'/g, "\\'");
|
|
127
|
+
return `page.getByRole('${role}', { name: '${escapedName}' })`;
|
|
128
|
+
}
|
|
129
|
+
|
|
89
130
|
/**
|
|
90
131
|
* Check if step matches a pattern matcher
|
|
91
132
|
*/
|
|
@@ -27,9 +27,66 @@ function elementKeywordToRole(text: string): string | null {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Interaction patterns: click, hover, press, wait
|
|
30
|
+
* Interaction patterns: click, hover, press, wait, alert
|
|
31
31
|
*/
|
|
32
32
|
export const interactionPatterns: StepPattern[] = [
|
|
33
|
+
// === Browser Alert (system dialog) patterns ===
|
|
34
|
+
// These must appear BEFORE the click that triggers the alert in Gherkin:
|
|
35
|
+
// When User click [OK] alert ← registers page.once('dialog', ...)
|
|
36
|
+
// And User click [Delete] button ← triggers the alert
|
|
37
|
+
{
|
|
38
|
+
name: 'alert-accept',
|
|
39
|
+
matcher: (step: ParsedStep) =>
|
|
40
|
+
step.elementType === 'alert' &&
|
|
41
|
+
(step.text.includes('click') || step.text.includes('clicks')) &&
|
|
42
|
+
!step.dataRef &&
|
|
43
|
+
!!(step.selectorRef && /^(ok|accept|yes|confirm)$/i.test(step.selectorRef)),
|
|
44
|
+
resolver: (step, context) => {
|
|
45
|
+
return {
|
|
46
|
+
templateName: 'alert-accept-action',
|
|
47
|
+
data: {},
|
|
48
|
+
comment: `Accept browser alert`,
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
priority: 20,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'alert-dismiss',
|
|
55
|
+
matcher: (step: ParsedStep) =>
|
|
56
|
+
step.elementType === 'alert' &&
|
|
57
|
+
(step.text.includes('click') || step.text.includes('clicks')) &&
|
|
58
|
+
!step.dataRef &&
|
|
59
|
+
!!(step.selectorRef && /^(cancel|dismiss|no)$/i.test(step.selectorRef)),
|
|
60
|
+
resolver: (step, context) => {
|
|
61
|
+
return {
|
|
62
|
+
templateName: 'alert-dismiss-action',
|
|
63
|
+
data: {},
|
|
64
|
+
comment: `Dismiss browser alert`,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
priority: 20,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'alert-fill',
|
|
71
|
+
matcher: (step: ParsedStep) =>
|
|
72
|
+
step.elementType === 'alert' &&
|
|
73
|
+
step.text.includes('fill') &&
|
|
74
|
+
!!step.dataRef,
|
|
75
|
+
resolver: (step, context) => {
|
|
76
|
+
let fillValue: string;
|
|
77
|
+
try {
|
|
78
|
+
fillValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
79
|
+
} catch {
|
|
80
|
+
fillValue = `\${${step.dataRef}}`;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
templateName: 'alert-fill-action',
|
|
84
|
+
data: { fillValue },
|
|
85
|
+
comment: `Fill browser prompt with ${step.dataRef}`,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
priority: 20,
|
|
89
|
+
},
|
|
33
90
|
{
|
|
34
91
|
name: 'unknown-element-action',
|
|
35
92
|
matcher: (step: ParsedStep) => {
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
import { StepPattern } from './types';
|
|
7
7
|
|
|
8
8
|
export const tablePatterns: StepPattern[] = [
|
|
9
|
-
// "User see [Users] table
|
|
9
|
+
// "User see [Users] table with {{count}} rows" (preferred)
|
|
10
|
+
// Also accepts: "User see [Users] table has {{count}} rows" (backward compat)
|
|
10
11
|
{
|
|
11
12
|
name: 'table-row-count',
|
|
12
13
|
matcher: (step) => {
|
|
13
|
-
return /\btable\s+has\b/i.test(step.text) &&
|
|
14
|
+
return /\btable\s+(?:has|with)\b/i.test(step.text) &&
|
|
14
15
|
/\}\}\s*rows?\b/i.test(step.text) &&
|
|
15
16
|
!!step.dataRef;
|
|
16
17
|
},
|
|
@@ -29,7 +30,8 @@ export const tablePatterns: StepPattern[] = [
|
|
|
29
30
|
priority: 16,
|
|
30
31
|
},
|
|
31
32
|
|
|
32
|
-
// "User see [Users] table has [Email] column"
|
|
33
|
+
// "User see [Users] table has [Email] column" (backward compat)
|
|
34
|
+
// Preferred: "User see [Email] column in [Users] table" (parent scoping)
|
|
33
35
|
{
|
|
34
36
|
name: 'table-column-exists',
|
|
35
37
|
matcher: (step) => {
|
|
@@ -152,11 +154,12 @@ export const tablePatterns: StepPattern[] = [
|
|
|
152
154
|
priority: 17,
|
|
153
155
|
},
|
|
154
156
|
|
|
155
|
-
// "User see [Users] table
|
|
157
|
+
// "User see [Users] table row with {{name}}" (preferred)
|
|
158
|
+
// Also accepts: "User see [Users] table has row with {{name}}" (backward compat)
|
|
156
159
|
{
|
|
157
160
|
name: 'table-row-exists',
|
|
158
161
|
matcher: (step) => {
|
|
159
|
-
return /\btable\s+has\s+row\s+with\b/i.test(step.text) && !!step.dataRef;
|
|
162
|
+
return /\btable\s+(?:has\s+)?row\s+with\b/i.test(step.text) && !!step.dataRef;
|
|
160
163
|
},
|
|
161
164
|
resolver: (step, context) => {
|
|
162
165
|
const resolved = context.selectorResolver.resolveSelector(
|
|
@@ -211,6 +211,13 @@ export class SelectorResolver {
|
|
|
211
211
|
'heading': () => ({ strategy: 'role', role: 'heading', name: label, value: 'heading' }),
|
|
212
212
|
'list': () => ({ strategy: 'role', role: 'list', name: label, value: 'list' }),
|
|
213
213
|
'listitem': () => ({ strategy: 'role', role: 'listitem', value: 'listitem' }),
|
|
214
|
+
'searchbox': () => ({ strategy: 'role', role: 'searchbox', name: label, value: 'searchbox' }),
|
|
215
|
+
'option': () => ({ strategy: 'role', role: 'option', name: label, value: 'option' }),
|
|
216
|
+
'slider': () => ({ strategy: 'role', role: 'slider', name: label, value: 'slider' }),
|
|
217
|
+
'switch': () => ({ strategy: 'role', role: 'switch', name: label, value: 'switch' }),
|
|
218
|
+
'tab': () => ({ strategy: 'role', role: 'tab', name: label, value: 'tab' }),
|
|
219
|
+
'dialog': () => ({ strategy: 'role', role: 'dialog', name: label, value: 'dialog' }),
|
|
220
|
+
'alertdialog': () => ({ strategy: 'role', role: 'alertdialog', name: label, value: 'alertdialog' }),
|
|
214
221
|
};
|
|
215
222
|
|
|
216
223
|
const factory = strategyMap[normalized];
|
|
@@ -247,6 +254,15 @@ export class SelectorResolver {
|
|
|
247
254
|
'textbox': 'field',
|
|
248
255
|
'textarea': 'field',
|
|
249
256
|
'editor': 'field',
|
|
257
|
+
// Search alias → searchbox role
|
|
258
|
+
'search': 'searchbox',
|
|
259
|
+
// Toggle alias → switch role
|
|
260
|
+
'toggle': 'switch',
|
|
261
|
+
// Alert alias → alertdialog role
|
|
262
|
+
'alert': 'alertdialog',
|
|
263
|
+
// Modal/drawer alias → dialog role
|
|
264
|
+
'modal': 'dialog',
|
|
265
|
+
'drawer': 'dialog',
|
|
250
266
|
};
|
|
251
267
|
return aliasMap[elementType] || elementType;
|
|
252
268
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Rules Updater
|
|
3
|
+
* Updates AI rules, commands, and skills from bundled templates.
|
|
4
|
+
* Used by `sungen update` command.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
// File mapping: [templateFile, outputPath]
|
|
11
|
+
// Shared with project-initializer.ts
|
|
12
|
+
export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
13
|
+
// Config
|
|
14
|
+
['claude-config.md', 'CLAUDE.md'],
|
|
15
|
+
['copilot-config.md', '.github/copilot-instructions.md'],
|
|
16
|
+
|
|
17
|
+
// Commands — Claude Code
|
|
18
|
+
['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
|
|
19
|
+
['claude-cmd-make-tc.md', '.claude/commands/sungen/make-tc.md'],
|
|
20
|
+
['claude-cmd-make-test.md', '.claude/commands/sungen/make-test.md'],
|
|
21
|
+
|
|
22
|
+
// Commands — GitHub Copilot
|
|
23
|
+
['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
|
|
24
|
+
['copilot-cmd-make-tc.md', '.github/prompts/sungen-make-tc.prompt.md'],
|
|
25
|
+
['copilot-cmd-make-test.md', '.github/prompts/sungen-make-test.prompt.md'],
|
|
26
|
+
|
|
27
|
+
// Skills — Claude Code
|
|
28
|
+
['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
29
|
+
['claude-skill-selector-keys.md', '.claude/skills/sungen-selector-keys/SKILL.md'],
|
|
30
|
+
['claude-skill-error-mapping.md', '.claude/skills/sungen-error-mapping/SKILL.md'],
|
|
31
|
+
['claude-skill-tc-generation.md', '.claude/skills/sungen-tc-generation/SKILL.md'],
|
|
32
|
+
['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
|
|
33
|
+
|
|
34
|
+
// Skills — GitHub Copilot
|
|
35
|
+
['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
36
|
+
['github-skill-sungen-selector-keys.md', '.github/skills/sungen-selector-keys/SKILL.md'],
|
|
37
|
+
['github-skill-sungen-error-mapping.md', '.github/skills/sungen-error-mapping/SKILL.md'],
|
|
38
|
+
['github-skill-sungen-tc-generation.md', '.github/skills/sungen-tc-generation/SKILL.md'],
|
|
39
|
+
['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export class AIRulesUpdater {
|
|
43
|
+
private cwd: string;
|
|
44
|
+
private aiTemplateDir: string;
|
|
45
|
+
|
|
46
|
+
constructor(cwd: string) {
|
|
47
|
+
this.cwd = cwd;
|
|
48
|
+
this.aiTemplateDir = path.join(__dirname, 'templates', 'ai-instructions');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async update(dryRun: boolean): Promise<void> {
|
|
52
|
+
console.log('🔄 Updating AI rules, commands, and skills...\n');
|
|
53
|
+
|
|
54
|
+
const updated: string[] = [];
|
|
55
|
+
const created: string[] = [];
|
|
56
|
+
const unchanged: string[] = [];
|
|
57
|
+
const missing: string[] = [];
|
|
58
|
+
|
|
59
|
+
for (const [templateFile, outputRelPath] of AI_RULES_FILE_MAPPING) {
|
|
60
|
+
const templatePath = path.join(this.aiTemplateDir, templateFile);
|
|
61
|
+
const outputPath = path.join(this.cwd, outputRelPath);
|
|
62
|
+
|
|
63
|
+
if (!fs.existsSync(templatePath)) {
|
|
64
|
+
missing.push(templateFile);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const newContent = fs.readFileSync(templatePath, 'utf-8');
|
|
69
|
+
|
|
70
|
+
if (fs.existsSync(outputPath)) {
|
|
71
|
+
const currentContent = fs.readFileSync(outputPath, 'utf-8');
|
|
72
|
+
if (currentContent === newContent) {
|
|
73
|
+
unchanged.push(outputRelPath);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!dryRun) {
|
|
78
|
+
fs.writeFileSync(outputPath, newContent, 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
updated.push(outputRelPath);
|
|
81
|
+
} else {
|
|
82
|
+
if (!dryRun) {
|
|
83
|
+
const outputDir = path.dirname(outputPath);
|
|
84
|
+
if (!fs.existsSync(outputDir)) {
|
|
85
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
fs.writeFileSync(outputPath, newContent, 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
created.push(outputRelPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Print results
|
|
94
|
+
if (dryRun) {
|
|
95
|
+
console.log('📋 Dry run — no files changed\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (updated.length > 0) {
|
|
99
|
+
console.log(`✏️ Updated (${updated.length}):`);
|
|
100
|
+
for (const f of updated) {
|
|
101
|
+
console.log(` ${f}`);
|
|
102
|
+
}
|
|
103
|
+
console.log();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (created.length > 0) {
|
|
107
|
+
console.log(`✨ Created (${created.length}):`);
|
|
108
|
+
for (const f of created) {
|
|
109
|
+
console.log(` ${f}`);
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (unchanged.length > 0) {
|
|
115
|
+
console.log(`✅ Already up to date (${unchanged.length}):`);
|
|
116
|
+
for (const f of unchanged) {
|
|
117
|
+
console.log(` ${f}`);
|
|
118
|
+
}
|
|
119
|
+
console.log();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (missing.length > 0) {
|
|
123
|
+
console.log(`⚠️ Template not found (${missing.length}):`);
|
|
124
|
+
for (const f of missing) {
|
|
125
|
+
console.log(` ${f}`);
|
|
126
|
+
}
|
|
127
|
+
console.log();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const totalChanges = updated.length + created.length;
|
|
131
|
+
if (totalChanges === 0) {
|
|
132
|
+
console.log('All AI rules are up to date. No changes needed.');
|
|
133
|
+
} else if (dryRun) {
|
|
134
|
+
console.log(`${totalChanges} file(s) would be changed. Run without --dry-run to apply.`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`${totalChanges} file(s) updated successfully.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
|
+
import { AI_RULES_FILE_MAPPING } from './ai-rules-updater';
|
|
9
10
|
|
|
10
11
|
export class ProjectInitializer {
|
|
11
12
|
private baseCwd: string;
|
|
@@ -250,38 +251,7 @@ export class ProjectInitializer {
|
|
|
250
251
|
private createAIRules(): void {
|
|
251
252
|
const aiTemplateDir = path.join(__dirname, 'templates', 'ai-instructions');
|
|
252
253
|
|
|
253
|
-
|
|
254
|
-
const fileMapping: [string, string][] = [
|
|
255
|
-
// Config
|
|
256
|
-
['claude-config.md', 'CLAUDE.md'],
|
|
257
|
-
['copilot-config.md', '.github/copilot-instructions.md'],
|
|
258
|
-
|
|
259
|
-
// Commands — Claude Code
|
|
260
|
-
['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
|
|
261
|
-
['claude-cmd-make-tc.md', '.claude/commands/sungen/make-tc.md'],
|
|
262
|
-
['claude-cmd-make-test.md', '.claude/commands/sungen/make-test.md'],
|
|
263
|
-
|
|
264
|
-
// Commands — GitHub Copilot
|
|
265
|
-
['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
|
|
266
|
-
['copilot-cmd-make-tc.md', '.github/prompts/sungen-make-tc.prompt.md'],
|
|
267
|
-
['copilot-cmd-make-test.md', '.github/prompts/sungen-make-test.prompt.md'],
|
|
268
|
-
|
|
269
|
-
// Skills — Claude Code
|
|
270
|
-
['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
271
|
-
['claude-skill-selector-keys.md', '.claude/skills/sungen-selector-keys/SKILL.md'],
|
|
272
|
-
['claude-skill-error-mapping.md', '.claude/skills/sungen-error-mapping/SKILL.md'],
|
|
273
|
-
['claude-skill-tc-generation.md', '.claude/skills/sungen-tc-generation/SKILL.md'],
|
|
274
|
-
['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
|
|
275
|
-
|
|
276
|
-
// Skills — GitHub Copilot (separate copies with Copilot-friendly descriptions)
|
|
277
|
-
['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
278
|
-
['github-skill-sungen-selector-keys.md', '.github/skills/sungen-selector-keys/SKILL.md'],
|
|
279
|
-
['github-skill-sungen-error-mapping.md', '.github/skills/sungen-error-mapping/SKILL.md'],
|
|
280
|
-
['github-skill-sungen-tc-generation.md', '.github/skills/sungen-tc-generation/SKILL.md'],
|
|
281
|
-
['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
|
|
282
|
-
];
|
|
283
|
-
|
|
284
|
-
for (const [templateFile, outputRelPath] of fileMapping) {
|
|
254
|
+
for (const [templateFile, outputRelPath] of AI_RULES_FILE_MAPPING) {
|
|
285
255
|
const outputPath = path.join(this.cwd, outputRelPath);
|
|
286
256
|
|
|
287
257
|
if (fs.existsSync(outputPath)) {
|