@sun-asterisk/sungen 2.6.10 → 2.6.12
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/cli/index.js +1 -1
- package/dist/dashboard/templates/index.html +112 -101
- package/dist/exporters/xlsx-exporter.d.ts +6 -0
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +53 -19
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +9 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +18 -2
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +5 -5
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +38 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +38 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +2 -0
- package/package.json +1 -1
- package/src/cli/index.ts +1 -1
- package/src/dashboard/templates/index.html +112 -101
- package/src/exporters/xlsx-exporter.ts +58 -19
- package/src/generators/test-generator/utils/selector-resolver.ts +19 -2
- package/src/orchestrator/project-initializer.ts +5 -5
- package/src/orchestrator/templates/ai-instructions/claude-config.md +2 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +38 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +2 -0
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +2 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +1 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +38 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +2 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import ExcelJS from 'exceljs';
|
|
15
|
+
import JSZip from 'jszip';
|
|
15
16
|
import { ScreenSummary, TestCaseRow } from './types';
|
|
16
17
|
import { getPackageVersion } from './package-info';
|
|
17
18
|
import { SUN_LOGO_PNG_BASE64 } from './sun-logo';
|
|
@@ -54,7 +55,7 @@ export function renderXlsx(
|
|
|
54
55
|
|
|
55
56
|
// -- Column widths matching template_report.xlsx (Sample sheet) --
|
|
56
57
|
ws.columns = [
|
|
57
|
-
{ width:
|
|
58
|
+
{ width: 20 }, // A — TC ID (wider to fit long flow IDs like FLOW-KUDO-…)
|
|
58
59
|
{ width: 12.5 }, // B — Screen/Function
|
|
59
60
|
{ width: 15.38 }, // C — Big item
|
|
60
61
|
{ width: 16.25 }, // D — Medium item
|
|
@@ -92,28 +93,24 @@ export function renderXlsx(
|
|
|
92
93
|
ws.mergeCells('A4:F4');
|
|
93
94
|
ws.mergeCells('G4:H4');
|
|
94
95
|
|
|
95
|
-
// A1 (logo band, merged A1:C3) —
|
|
96
|
+
// A1 (logo band, merged A1:C3) — border + embedded Sun* logo (base64 inline).
|
|
96
97
|
const a1 = ws.getCell('A1');
|
|
97
98
|
a1.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
98
99
|
a1.border = allBordersBlack;
|
|
99
100
|
|
|
100
|
-
// Embed Sun* logo using a oneCellAnchor + explicit pixel `ext`, exactly
|
|
101
|
-
// like the template. tl is offset into the merged band so the logo lands
|
|
102
|
-
// centred in A1:C3 instead of pinned to the top-left corner.
|
|
103
101
|
try {
|
|
104
102
|
const imageId = wb.addImage({
|
|
105
103
|
buffer: Buffer.from(SUN_LOGO_PNG_BASE64, 'base64') as unknown as ExcelJS.Buffer,
|
|
106
104
|
extension: 'png',
|
|
107
105
|
});
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// the same field names).
|
|
106
|
+
// Centre a fixed-size 90×51 logo inside A1:C3 using absolute EMU offsets.
|
|
107
|
+
// Col widths (A=20, B=12.5, C=15.38) → pixels via Excel's `w*7+5` rule:
|
|
108
|
+
// A=145, B=92.5, C=112.66 ⇒ A1:C3 ≈ 350 px wide.
|
|
109
|
+
// Left padding for a 90-px image = (350−90)/2 ≈ 130 px = 1,237,500 EMU.
|
|
110
|
+
// 3 default rows × 20 px = 60 px; top padding ≈ 4.5 px ≈ 42,862 EMU.
|
|
114
111
|
ws.addImage(imageId, {
|
|
115
|
-
tl: { nativeCol: 0, nativeColOff:
|
|
116
|
-
ext: { width: 90, height:
|
|
112
|
+
tl: { nativeCol: 0, nativeColOff: 1237500, nativeRow: 0, nativeRowOff: 42862 } as unknown as ExcelJS.Anchor,
|
|
113
|
+
ext: { width: 90, height: 51 },
|
|
117
114
|
editAs: 'oneCell',
|
|
118
115
|
} as unknown as ExcelJS.ImageRange);
|
|
119
116
|
} catch { /* logo is decorative — never block export */ }
|
|
@@ -161,8 +158,7 @@ export function renderXlsx(
|
|
|
161
158
|
g4.alignment = { horizontal: 'right', vertical: 'middle', wrapText: true };
|
|
162
159
|
g4.border = allBordersBlack;
|
|
163
160
|
|
|
164
|
-
// -- Row 5: spacer (height
|
|
165
|
-
ws.getRow(5).height = 12;
|
|
161
|
+
// -- Row 5: spacer (height auto-fit). --
|
|
166
162
|
|
|
167
163
|
// -- Row 6: Summary headers (cols C..H), lavender fill --
|
|
168
164
|
const sumLabels: Record<string, string> = {
|
|
@@ -176,7 +172,7 @@ export function renderXlsx(
|
|
|
176
172
|
c.alignment = { horizontal: 'center', vertical: 'top' };
|
|
177
173
|
c.border = allBordersBlack;
|
|
178
174
|
}
|
|
179
|
-
|
|
175
|
+
// No explicit height → Excel auto-fits this row's content.
|
|
180
176
|
|
|
181
177
|
// -- Row 7: Counts (cols C..H), live formulas referencing the data area --
|
|
182
178
|
// Data table starts at row 15 (col header at row 14, divider at row 14? actually
|
|
@@ -200,7 +196,7 @@ export function renderXlsx(
|
|
|
200
196
|
c.border = allBordersBlack;
|
|
201
197
|
if (col === 'H') c.numFmt = '#,##0';
|
|
202
198
|
}
|
|
203
|
-
|
|
199
|
+
// No explicit height → Excel auto-fits.
|
|
204
200
|
|
|
205
201
|
// C8 has no value in the template but still needs a border so the band
|
|
206
202
|
// visually closes underneath "Total TCs".
|
|
@@ -227,7 +223,7 @@ export function renderXlsx(
|
|
|
227
223
|
c.border = allBordersBlack;
|
|
228
224
|
c.numFmt = '0%';
|
|
229
225
|
}
|
|
230
|
-
|
|
226
|
+
// No explicit height → Excel auto-fits.
|
|
231
227
|
|
|
232
228
|
// After rows 1-8 are populated, append a blank row 9 spacer.
|
|
233
229
|
while (ws.rowCount < 9) ws.addRow([]);
|
|
@@ -353,6 +349,17 @@ export function renderXlsx(
|
|
|
353
349
|
to: { row: ws.rowCount, column: COL_COUNT },
|
|
354
350
|
};
|
|
355
351
|
|
|
352
|
+
// Sheet protection lets the picLocks on the embedded logo take effect
|
|
353
|
+
// (Excel honours image locks only when the sheet is protected). Empty
|
|
354
|
+
// password — QA can run Review → Unprotect Sheet if they need to edit.
|
|
355
|
+
// `objects: false` here is ExcelJS's inverted semantic that writes
|
|
356
|
+
// `objects="1"` in XML (= objects ARE locked).
|
|
357
|
+
(ws as unknown as { sheetProtection: object }).sheetProtection = {
|
|
358
|
+
sheet: true,
|
|
359
|
+
objects: false,
|
|
360
|
+
scenarios: false,
|
|
361
|
+
};
|
|
362
|
+
|
|
356
363
|
return wb;
|
|
357
364
|
}
|
|
358
365
|
|
|
@@ -368,9 +375,41 @@ export async function writeXlsx(
|
|
|
368
375
|
const outDir = path.join(cwd, 'qa', 'deliverables');
|
|
369
376
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
370
377
|
const outPath = path.join(outDir, `${screen}-testcases.xlsx`);
|
|
371
|
-
await wb.xlsx.
|
|
378
|
+
const buffer = await wb.xlsx.writeBuffer();
|
|
379
|
+
const locked = await lockEmbeddedImages(Buffer.from(buffer as ArrayBuffer));
|
|
380
|
+
fs.writeFileSync(outPath, locked);
|
|
372
381
|
return outPath;
|
|
373
382
|
}
|
|
374
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Post-process the workbook buffer: extend `<a:picLocks>` on every drawing
|
|
386
|
+
* with `noMove`, `noResize`, `noSelect`. Combined with the sheet protection
|
|
387
|
+
* above, the embedded logo can't be dragged, resized, or selected via the UI.
|
|
388
|
+
*/
|
|
389
|
+
export async function lockEmbeddedImages(buffer: Buffer): Promise<Buffer> {
|
|
390
|
+
const zip = await JSZip.loadAsync(buffer);
|
|
391
|
+
const drawingFiles = Object.keys(zip.files).filter(
|
|
392
|
+
(name) => /^xl\/drawings\/drawing\d+\.xml$/.test(name)
|
|
393
|
+
);
|
|
394
|
+
for (const name of drawingFiles) {
|
|
395
|
+
const file = zip.file(name);
|
|
396
|
+
if (!file) continue;
|
|
397
|
+
const xml = await file.async('string');
|
|
398
|
+
const patched = xml.replace(
|
|
399
|
+
/<a:picLocks([^/]*)\/>/g,
|
|
400
|
+
(_match, attrs: string) => {
|
|
401
|
+
const has = (k: string) => new RegExp(`\\b${k}=`).test(attrs);
|
|
402
|
+
const additions = ['noMove', 'noResize', 'noSelect']
|
|
403
|
+
.filter((k) => !has(k))
|
|
404
|
+
.map((k) => ` ${k}="1"`)
|
|
405
|
+
.join('');
|
|
406
|
+
return `<a:picLocks${attrs}${additions}/>`;
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
zip.file(name, patched);
|
|
410
|
+
}
|
|
411
|
+
return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
|
|
412
|
+
}
|
|
413
|
+
|
|
375
414
|
void applyBorder;
|
|
376
415
|
void ({} as AnyRow);
|
|
@@ -4,6 +4,7 @@ import yaml from 'yaml';
|
|
|
4
4
|
import { readYaml, readYamlIfExists } from '../../../utils/yaml-io';
|
|
5
5
|
|
|
6
6
|
import { SelectorType } from '../../../utils/selector-types';
|
|
7
|
+
import { DataResolver } from './data-resolver';
|
|
7
8
|
|
|
8
9
|
// Structured selector format v2
|
|
9
10
|
interface SelectorEntry {
|
|
@@ -414,6 +415,20 @@ export class SelectorResolver {
|
|
|
414
415
|
return this.resolveNaturalLanguage(selectorRef, this.featureName);
|
|
415
416
|
}
|
|
416
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Replace {{variable}} references in selector values with runtime data markers.
|
|
420
|
+
* This enables i18n: selectors can reference locale-dependent text from test-data.
|
|
421
|
+
*
|
|
422
|
+
* "{{lbl_submit}}" → "__SUNGEN_TD_lbl_submit__"
|
|
423
|
+
* "Hello {{name}}" → "Hello __SUNGEN_TD_name__"
|
|
424
|
+
* "No variables" → "No variables" (unchanged)
|
|
425
|
+
*/
|
|
426
|
+
private static resolveTemplateVars(value: string): string {
|
|
427
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_, varName) => {
|
|
428
|
+
return DataResolver.encodeMarker(varName.trim());
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
417
432
|
/**
|
|
418
433
|
* Resolve from new structured entry format
|
|
419
434
|
*/
|
|
@@ -422,9 +437,11 @@ export class SelectorResolver {
|
|
|
422
437
|
const locator = entry.locator || '';
|
|
423
438
|
const type = entry.type || 'placeholder';
|
|
424
439
|
// Check if value exists in entry (even if empty string), use it; otherwise use originalLabel
|
|
425
|
-
const
|
|
440
|
+
const rawValue = entry.value !== undefined && entry.value !== null ? entry.value : originalLabel;
|
|
426
441
|
// Check if name exists in entry (even if empty string), use it; otherwise use originalLabel
|
|
427
|
-
const
|
|
442
|
+
const rawName = entry.name !== undefined && entry.name !== null ? entry.name : originalLabel;
|
|
443
|
+
const value = SelectorResolver.resolveTemplateVars(rawValue);
|
|
444
|
+
const name = SelectorResolver.resolveTemplateVars(rawName);
|
|
428
445
|
const nth = entry.nth !== undefined && entry.nth !== null ? entry.nth : undefined;
|
|
429
446
|
const exact = entry.exact === true || entry.match === 'exact';
|
|
430
447
|
const inputMethod = entry.inputMethod;
|
|
@@ -147,7 +147,6 @@ export class ProjectInitializer {
|
|
|
147
147
|
const gitignorePath = path.join(this.cwd, '.gitignore');
|
|
148
148
|
const sungenEntries = [
|
|
149
149
|
'# Sungen generated files',
|
|
150
|
-
'specs/generated/',
|
|
151
150
|
'specs/.auth/',
|
|
152
151
|
'test-results/',
|
|
153
152
|
'playwright-report/',
|
|
@@ -162,7 +161,7 @@ export class ProjectInitializer {
|
|
|
162
161
|
} else {
|
|
163
162
|
// Append Sungen entries if not already present
|
|
164
163
|
let content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
165
|
-
const needsUpdate = !content.includes('specs/
|
|
164
|
+
const needsUpdate = !content.includes('specs/.auth/');
|
|
166
165
|
|
|
167
166
|
if (needsUpdate) {
|
|
168
167
|
content += '\n' + sungenEntries.join('\n') + '\n';
|
|
@@ -450,8 +449,9 @@ export class ProjectInitializer {
|
|
|
450
449
|
console.log(`📦 Installing ${missingDeps.join(', ')}...\n`);
|
|
451
450
|
execSync(`npm install -D ${missingDeps.join(' ')}`, execOpts);
|
|
452
451
|
|
|
453
|
-
console.log('\n🎭 Installing
|
|
454
|
-
execSync('npx playwright install', execOpts);
|
|
452
|
+
console.log('\n🎭 Installing Chromium (default browser)...\n');
|
|
453
|
+
execSync('npx playwright install --with-deps chromium', execOpts);
|
|
454
|
+
console.log('\n💡 To install other browsers: npm run install:browsers -- firefox webkit\n');
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
/**
|
|
@@ -477,7 +477,7 @@ export class ProjectInitializer {
|
|
|
477
477
|
'test:ui': 'playwright test specs/generated/ --ui',
|
|
478
478
|
'report': 'playwright show-report',
|
|
479
479
|
'generate': 'sungen generate --all',
|
|
480
|
-
'install:browsers': 'npx playwright install
|
|
480
|
+
'install:browsers': 'npx playwright install',
|
|
481
481
|
};
|
|
482
482
|
|
|
483
483
|
let added = 0;
|
|
@@ -82,6 +82,8 @@ qa/deliverables/<name>-testcases.xlsx # Styled workbook for client hand-off
|
|
|
82
82
|
|
|
83
83
|
**Environment overrides**: `SUNGEN_ENV=staging npx playwright test` merges `<screen>.staging.yaml` on top of `<screen>.yaml`. Create `<screen>.<env>.yaml` for environment-specific values (different credentials, URLs, test users).
|
|
84
84
|
|
|
85
|
+
**i18n support**: for multilingual sites, use `{{variable}}` in selector `name`/`value` fields to reference locale-dependent text from test-data. Create locale overlay files (e.g., `<screen>.vi.yaml`, `<screen>.staging-ja.yaml`) and run with `SUNGEN_ENV=vi`. One feature file + one selector file works across all locales. See `sungen-selector-keys` skill for details.
|
|
86
|
+
|
|
85
87
|
## CLI Commands
|
|
86
88
|
|
|
87
89
|
```bash
|
|
@@ -61,6 +61,7 @@ When running Phase 0 for a **flow** (`qa/flows/<name>/`), check existing screen
|
|
|
61
61
|
- Selector priority: follow the table in **Diagnosis & Fix § Step 3** (`testid` > `role`+name > `placeholder` > `label` > `locator` > `text`).
|
|
62
62
|
- Copy names **character-for-character** from the snapshot. Never infer from the Gherkin label.
|
|
63
63
|
- If an element is auto-inferable per `sungen-selector-keys` § Auto-Infer, **omit it** from YAML — keep the file minimal.
|
|
64
|
+
- **i18n sites**: if the site supports multiple languages, use `{{variable}}` in `name`/`value` fields instead of hardcoded text. Add corresponding `lbl_*` keys to `test-data.yaml` + locale overlay files (see `sungen-selector-keys` § i18n).
|
|
64
65
|
7. **Substring ambiguity check**: for each `role` + `name` selector, check if any other element in the snapshot has a name that **contains** this name as a substring (e.g., `"Đăng ký"` vs `"Đăng ký bằng Google"`). If yes → add `exact: true` to prevent strict mode violation at runtime.
|
|
65
66
|
8. **Merge, don't overwrite**: preserve the page selector and any user-authored entries in `selectors.yaml`. Only add missing keys.
|
|
66
67
|
9. **Show summary + confirm**: list the keys that will be added, ask the user to approve, then write the file.
|
|
@@ -115,6 +115,44 @@ gửi lời cảm ơn--3:
|
|
|
115
115
|
name: 'Gửi lời cảm ơn'
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
## i18n: Template Variables in Selectors
|
|
119
|
+
|
|
120
|
+
For multilingual sites without `data-testid`, use `{{variable}}` in `name` or `value` fields to reference locale-dependent text from `test-data.yaml`.
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
# selectors — one file for all locales
|
|
124
|
+
submit:
|
|
125
|
+
type: role
|
|
126
|
+
value: button
|
|
127
|
+
name: "{{lbl_submit}}"
|
|
128
|
+
|
|
129
|
+
search:
|
|
130
|
+
type: placeholder
|
|
131
|
+
value: "{{lbl_search}}"
|
|
132
|
+
|
|
133
|
+
logo:
|
|
134
|
+
type: testid
|
|
135
|
+
value: app-logo # testid is locale-independent — no variable needed
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
# test-data/login.yaml (base — English)
|
|
140
|
+
lbl_submit: "Sign in"
|
|
141
|
+
lbl_search: "Search..."
|
|
142
|
+
|
|
143
|
+
# test-data/login.vi.yaml (Vietnamese)
|
|
144
|
+
lbl_submit: "Đăng nhập"
|
|
145
|
+
lbl_search: "Tìm kiếm..."
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Run: `SUNGEN_ENV=vi npx playwright test`
|
|
149
|
+
|
|
150
|
+
**Rules:**
|
|
151
|
+
1. Prefix i18n keys with `lbl_`, `msg_`, `txt_` to separate from test data
|
|
152
|
+
2. Prefer `data-testid` — only use `{{variable}}` when no stable selector exists
|
|
153
|
+
3. Feature file stays identical across locales
|
|
154
|
+
4. Requires runtime data mode (default, not `--inline-data`)
|
|
155
|
+
|
|
118
156
|
## Lookup Priority
|
|
119
157
|
|
|
120
158
|
Resolver searches in this order:
|
|
@@ -352,6 +352,8 @@ Feature: <Screen> Screen
|
|
|
352
352
|
|
|
353
353
|
**Environment-specific data**: create `<screen>.<env>.yaml` alongside the base file with only the keys that change. Users run `SUNGEN_ENV=staging npx playwright test` to merge overrides.
|
|
354
354
|
|
|
355
|
+
**i18n / multilingual**: use the same `SUNGEN_ENV` overlay for locale variants — e.g., `login.vi.yaml`, `login.staging-ja.yaml`. Include `lbl_*` / `msg_*` keys for selector `{{variable}}` references (see `sungen-selector-keys` § i18n). One feature file + one selector file works across all locales.
|
|
356
|
+
|
|
355
357
|
## Flow Test Generation
|
|
356
358
|
|
|
357
359
|
When generating tests for a **flow** (`qa/flows/<name>/`), adapt the strategy:
|
|
@@ -82,6 +82,8 @@ qa/deliverables/<name>-testcases.xlsx # Styled workbook for client hand-off
|
|
|
82
82
|
|
|
83
83
|
**Environment overrides**: `SUNGEN_ENV=staging npx playwright test` merges `<screen>.staging.yaml` on top of `<screen>.yaml`. Create `<screen>.<env>.yaml` for environment-specific values (different credentials, URLs, test users).
|
|
84
84
|
|
|
85
|
+
**i18n support**: for multilingual sites, use `{{variable}}` in selector `name`/`value` fields to reference locale-dependent text from test-data. Create locale overlay files (e.g., `<screen>.vi.yaml`, `<screen>.staging-ja.yaml`) and run with `SUNGEN_ENV=vi`. One feature file + one selector file works across all locales. See `sungen-selector-keys` skill for details.
|
|
86
|
+
|
|
85
87
|
## CLI Commands
|
|
86
88
|
|
|
87
89
|
```bash
|
|
@@ -61,6 +61,7 @@ When running Phase 0 for a **flow** (`qa/flows/<name>/`), check existing screen
|
|
|
61
61
|
- Selector priority: follow the table in **Diagnosis & Fix § Step 3** (`testid` > `role`+name > `placeholder` > `label` > `locator` > `text`).
|
|
62
62
|
- Copy names **character-for-character** from the snapshot. Never infer from the Gherkin label.
|
|
63
63
|
- If an element is auto-inferable per `sungen-selector-keys` § Auto-Infer, **omit it** from YAML — keep the file minimal.
|
|
64
|
+
- **i18n sites**: if the site supports multiple languages, use `{{variable}}` in `name`/`value` fields instead of hardcoded text. Add corresponding `lbl_*` keys to `test-data.yaml` + locale overlay files (see `sungen-selector-keys` § i18n).
|
|
64
65
|
7. **Substring ambiguity check**: for each `role` + `name` selector, check if any other element in the snapshot has a name that **contains** this name as a substring (e.g., `"Đăng ký"` vs `"Đăng ký bằng Google"`). If yes → add `exact: true` to prevent strict mode violation at runtime.
|
|
65
66
|
8. **Merge, don't overwrite**: preserve the page selector and any user-authored entries in `selectors.yaml`. Only add missing keys.
|
|
66
67
|
9. **Show summary + confirm**: list the keys that will be added, ask the user to approve, then write the file.
|
|
@@ -115,6 +115,44 @@ gửi lời cảm ơn--3:
|
|
|
115
115
|
name: 'Gửi lời cảm ơn'
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
## i18n: Template Variables in Selectors
|
|
119
|
+
|
|
120
|
+
For multilingual sites without `data-testid`, use `{{variable}}` in `name` or `value` fields to reference locale-dependent text from `test-data.yaml`.
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
# selectors — one file for all locales
|
|
124
|
+
submit:
|
|
125
|
+
type: role
|
|
126
|
+
value: button
|
|
127
|
+
name: "{{lbl_submit}}"
|
|
128
|
+
|
|
129
|
+
search:
|
|
130
|
+
type: placeholder
|
|
131
|
+
value: "{{lbl_search}}"
|
|
132
|
+
|
|
133
|
+
logo:
|
|
134
|
+
type: testid
|
|
135
|
+
value: app-logo # testid is locale-independent — no variable needed
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
# test-data/login.yaml (base — English)
|
|
140
|
+
lbl_submit: "Sign in"
|
|
141
|
+
lbl_search: "Search..."
|
|
142
|
+
|
|
143
|
+
# test-data/login.vi.yaml (Vietnamese)
|
|
144
|
+
lbl_submit: "Đăng nhập"
|
|
145
|
+
lbl_search: "Tìm kiếm..."
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Run: `SUNGEN_ENV=vi npx playwright test`
|
|
149
|
+
|
|
150
|
+
**Rules:**
|
|
151
|
+
1. Prefix i18n keys with `lbl_`, `msg_`, `txt_` to separate from test data
|
|
152
|
+
2. Prefer `data-testid` — only use `{{variable}}` when no stable selector exists
|
|
153
|
+
3. Feature file stays identical across locales
|
|
154
|
+
4. Requires runtime data mode (default, not `--inline-data`)
|
|
155
|
+
|
|
118
156
|
## Lookup Priority
|
|
119
157
|
|
|
120
158
|
Resolver searches in this order:
|
|
@@ -352,6 +352,8 @@ Feature: <Screen> Screen
|
|
|
352
352
|
|
|
353
353
|
**Environment-specific data**: create `<screen>.<env>.yaml` alongside the base file with only the keys that change. Users run `SUNGEN_ENV=staging npx playwright test` to merge overrides.
|
|
354
354
|
|
|
355
|
+
**i18n / multilingual**: use the same `SUNGEN_ENV` overlay for locale variants — e.g., `login.vi.yaml`, `login.staging-ja.yaml`. Include `lbl_*` / `msg_*` keys for selector `{{variable}}` references (see `sungen-selector-keys` § i18n). One feature file + one selector file works across all locales.
|
|
356
|
+
|
|
355
357
|
## Flow Test Generation
|
|
356
358
|
|
|
357
359
|
When generating tests for a **flow** (`qa/flows/<name>/`), adapt the strategy:
|