@keber/qa-framework 1.11.3 → 1.12.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 +2 -2
- package/docs/architecture.md +30 -26
- package/docs/decisions/lane-heartbeat-caller.md +86 -0
- package/docs/final-report.md +23 -23
- package/docs/folder-structure-guide.md +100 -80
- package/docs/installation.md +35 -7
- package/docs/skills-architecture.md +23 -23
- package/docs/spec-driven-philosophy.md +9 -9
- package/docs/usage-with-agent.md +83 -9
- package/docs/visuals/diag-01-ejecutivo-1.png +0 -0
- package/docs/visuals/diag-02-tactico-1.png +0 -0
- package/docs/visuals/diag-03-tecnico-1.png +0 -0
- package/docs/visuals/info-01-ejecutivo-1.png +0 -0
- package/docs/visuals/info-02-tactico-1.png +0 -0
- package/docs/visuals/info-03-tecnico-1.png +0 -0
- package/docs/visuals/pres-01-ejecutivo-1.png +0 -0
- package/docs/visuals/pres-02-tactico-1.png +0 -0
- package/docs/visuals/pres-03-tecnico-1.png +0 -0
- package/docs/visuals/qa-01-problema-1.png +0 -0
- package/docs/visuals/qa-02-que-es-1.png +0 -0
- package/docs/visuals/qa-03-pipeline-1.png +0 -0
- package/docs/visuals/qa-04-6-archivos-1.png +0 -0
- package/docs/visuals/qa-05-humano-1.png +0 -0
- package/docs/visuals/qa-06-agente-1.png +0 -0
- package/docs/visuals/qa-07-skills-arch-1.png +0 -0
- package/docs/visuals/qa-08-traceability-1.png +0 -0
- package/docs/visuals/qa-en-01-problem-1.png +0 -0
- package/docs/visuals/qa-en-02-what-is-1.png +0 -0
- package/docs/visuals/qa-en-03-pipeline-1.png +0 -0
- package/docs/visuals/qa-en-04-6files-1.png +0 -0
- package/docs/visuals/qa-en-05-human-1.png +0 -0
- package/docs/visuals/qa-en-06-agent-1.png +0 -0
- package/docs/visuals/qa-en-07-skills-arch-1.png +0 -0
- package/docs/visuals/qa-en-08-traceability-1.png +0 -0
- package/examples/module-example/suppliers/00-inventory.md +1 -1
- package/examples/module-example/suppliers/suppliers-create.spec.ts +8 -8
- package/integrations/ado-powershell/README.md +1 -1
- package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
- package/integrations/playwright/README.md +4 -4
- package/package.json +7 -3
- package/qa-framework.config.json +23 -2
- package/scripts/check-forbidden-chars.js +187 -0
- package/scripts/check-spanish-accents.js +159 -0
- package/scripts/cli.js +1 -1
- package/scripts/generate.js +2 -2
- package/scripts/init.js +55 -7
- package/scripts/lib/claude-agents.js +78 -0
- package/scripts/lib/claude-commands.js +56 -0
- package/scripts/sync-version.js +36 -0
- package/scripts/upgrade.js +356 -20
- package/scripts/validate.js +17 -17
- package/skills/qa-ado-integration/SKILL.md +19 -19
- package/skills/qa-ado-integration/references/scripts-and-config.md +6 -6
- package/skills/qa-automation/SKILL.md +49 -27
- package/skills/qa-automation/references/config-checklist.md +4 -4
- package/skills/qa-automation/references/dom-inspection-template.js +7 -7
- package/skills/qa-automation/references/patterns.md +20 -20
- package/skills/qa-automation/references/pom-template.md +161 -0
- package/skills/qa-maintenance/SKILL.md +11 -11
- package/skills/qa-maintenance/references/update-rules.md +8 -8
- package/skills/qa-module-analysis/SKILL.md +20 -19
- package/skills/qa-module-analysis/references/exploration-checklist.md +4 -4
- package/skills/qa-module-analysis/references/output-verification.md +69 -0
- package/skills/qa-module-analysis/references/spec-file-formats.md +14 -14
- package/skills/qa-spec-generation/SKILL.md +13 -7
- package/skills/qa-test-cases/SKILL.md +11 -81
- package/skills/qa-test-cases/references/test-case-template.md +5 -5
- package/skills/qa-test-plan/SKILL.md +22 -86
- package/skills/qa-test-plan/references/plan-de-pruebas-template.md +24 -24
- package/skills/qa-test-stabilization/SKILL.md +59 -30
- package/skills/qa-test-stabilization/references/classification-protocol.md +35 -35
- package/templates/agent-next-steps.md +10 -10
- package/templates/agents/qa-analisis.md +142 -0
- package/templates/agents/qa-asesoria.md +48 -0
- package/templates/agents/qa-informe-resultados.md +234 -0
- package/templates/agents/qa-plan.md +131 -0
- package/templates/automation-scaffold/.env.example +36 -6
- package/templates/automation-scaffold/fixtures/auth.ts +27 -5
- package/templates/automation-scaffold/fixtures/base.ts +23 -1
- package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
- package/templates/automation-scaffold/global-setup.ts +117 -48
- package/templates/automation-scaffold/playwright.config.ts +72 -32
- package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
- package/templates/automation-scaffold/scripts/lane-config.js +269 -0
- package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
- package/templates/defect-report.md +17 -3
- package/templates/execution-report.md +9 -9
- package/templates/integration-scaffold/.env.example +1 -1
- package/templates/integration-scaffold/global-setup.ts +21 -1
- package/templates/integration-scaffold/playwright.config.ts +15 -1
- package/templates/qa-framework.instructions.md +8 -7
- package/templates/qa-framework.rules.md +53 -0
- package/templates/qa-readme.md +8 -8
- package/templates/session-summary.md +2 -2
- package/templates/specification/00-inventory.md +3 -3
- package/templates/specification/01-business-rules.md +6 -6
- package/templates/specification/02-workflows.md +1 -1
- package/templates/specification/03-roles-permissions.md +1 -1
- package/templates/specification/04-test-data.md +8 -8
- package/templates/specification/05-test-scenarios.md +5 -5
- package/templates/test-case.md +3 -3
- package/templates/test-data-guidelines.md +7 -7
- package/templates/test-plan-sprint.md +13 -13
- package/templates/test-plan.md +3 -3
- package/skills/qa-test-plan/references/priority-and-feasibility.md +0 -159
package/scripts/upgrade.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* scripts/upgrade.js
|
|
3
|
+
* scripts/upgrade.js - Upgrade framework-owned files in an existing project
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* qa-framework upgrade
|
|
@@ -27,7 +27,37 @@
|
|
|
27
27
|
* - qa/02-test-plans/manual/ -> qa/02-test-plans/sprints/legacy-manual/
|
|
28
28
|
* - qa/02-test-plans/*.md -> qa/02-test-plans/sprints/legacy/
|
|
29
29
|
* - Creates qa/02-test-plans/sprints/ if absent
|
|
30
|
-
* - Creates qa/03-test-cases/README.md optional marker if absent
|
|
30
|
+
* - Creates qa/03-test-cases/README.md optional marker if absent
|
|
31
|
+
*
|
|
32
|
+
* MIGRATES (v1.11.x -> v1.12.0, lane-aware automation scaffold):
|
|
33
|
+
* Installs the lane scaffold only where it cannot break working user code.
|
|
34
|
+
* A file counts as framework-owned ("pristine") when its normalised sha256
|
|
35
|
+
* matches one this framework actually shipped; otherwise it is user-owned.
|
|
36
|
+
*
|
|
37
|
+
* ATOMIC GROUP (all installed, or none - config/global-setup require() the scripts):
|
|
38
|
+
* - qa/07-automation/e2e/scripts/lane-config.js (new in v1.12.0)
|
|
39
|
+
* - qa/07-automation/e2e/scripts/lane-lock.js (new in v1.12.0)
|
|
40
|
+
* - qa/07-automation/e2e/scripts/global-setup-guards.js (new in v1.12.0)
|
|
41
|
+
* - qa/07-automation/e2e/playwright.config.ts
|
|
42
|
+
* - qa/07-automation/e2e/global-setup.ts
|
|
43
|
+
* If any member is user-owned the whole group is skipped with a warning and
|
|
44
|
+
* the upgrade continues; the project keeps its working setup rather than
|
|
45
|
+
* being left half-migrated.
|
|
46
|
+
*
|
|
47
|
+
* PER FILE (leaf modules - a mixed state is still coherent):
|
|
48
|
+
* - qa/07-automation/e2e/fixtures/{auth,base,test-helpers}.ts
|
|
49
|
+
* - qa/07-automation/integration/{playwright.config.ts,global-setup.ts}
|
|
50
|
+
* - .env.example in both scaffolds (reference template only; your .env
|
|
51
|
+
* is never touched)
|
|
52
|
+
* Pristine -> overwritten. User-owned -> kept, with a warning.
|
|
53
|
+
*
|
|
54
|
+
* REPORTS only (never patched):
|
|
55
|
+
* - .fill(password) in any .ts under e2e/ or integration/ - leaks the
|
|
56
|
+
* password into Playwright traces. See "Pattern 7" in
|
|
57
|
+
* .github/skills/qa-automation/references/patterns.md. Not auto-fixed:
|
|
58
|
+
* the surrounding code differs per project and a bad rewrite of a
|
|
59
|
+
* working login is worse than the finding.
|
|
60
|
+
*
|
|
31
61
|
* NEVER touches (project-owned):
|
|
32
62
|
* - qa/01-specifications/ <- Your specs
|
|
33
63
|
* - qa/02-test-plans/ <- Your test plans
|
|
@@ -46,6 +76,9 @@
|
|
|
46
76
|
const fs = require('fs');
|
|
47
77
|
const path = require('path');
|
|
48
78
|
|
|
79
|
+
const { buildCommandContent, discoverSkillNames, commandFileName } = require('./lib/claude-commands');
|
|
80
|
+
const { AGENT_NAMES, isSprintCycleEnabled, buildAgentContent, agentFileName } = require('./lib/claude-agents');
|
|
81
|
+
|
|
49
82
|
const args = process.argv.slice(2);
|
|
50
83
|
const dryRun = args.includes('--dry-run');
|
|
51
84
|
|
|
@@ -70,6 +103,52 @@ const githubDir = path.join(cwd, '.github');
|
|
|
70
103
|
const skillsDest = path.join(githubDir, 'skills');
|
|
71
104
|
const skillsSrc = path.resolve(__dirname, '..', 'skills');
|
|
72
105
|
|
|
106
|
+
// Normalised sha256 of every version of these scaffold files that this framework has
|
|
107
|
+
// shipped (v1.8.0 through v1.11.3). A project file matching one of these was written
|
|
108
|
+
// by the framework and never edited, so v1.12.0 may safely replace it. Anything else
|
|
109
|
+
// is user-owned. Regenerate by hashing `git show <tag>:templates/<key>` for each tag.
|
|
110
|
+
//
|
|
111
|
+
// Deliberately hash-based rather than version-based: the frameworkVersion recorded in
|
|
112
|
+
// real installations is unreliable (observed values include 1.1.3 and 1.0.0, neither
|
|
113
|
+
// of which corresponds to a released scaffold).
|
|
114
|
+
const SHIPPED_SCAFFOLD_HASHES = {
|
|
115
|
+
'automation-scaffold/playwright.config.ts': [
|
|
116
|
+
'948635c118553e293447930ba3dcff9155446718554dd3bbcffac2720baebba1',
|
|
117
|
+
'f145fda0a7215942b5431a779188609f8958e51a048b161ede605576efba3ef8',
|
|
118
|
+
],
|
|
119
|
+
'automation-scaffold/global-setup.ts': [
|
|
120
|
+
'077580914881ee71ac082d47579dc4cffa01e8348b0744fced6b8de2863e0bd1',
|
|
121
|
+
'7b3e7ca54929e525055d19ba8cd9643de722c7092c67b1a2e273072dd712ec7a',
|
|
122
|
+
],
|
|
123
|
+
'automation-scaffold/.env.example': [
|
|
124
|
+
'18fab7c165085a4a585a6911b42c49b97177265d46261c8362c3508a7436c65c',
|
|
125
|
+
'29dc3a97e42350fbe7a16681837ba8db2a539574f8aa86fe269204bc3081bff9',
|
|
126
|
+
],
|
|
127
|
+
'automation-scaffold/fixtures/auth.ts': [
|
|
128
|
+
'a856df26d80d521a0ad349879096698191ed6a2f919fac96d85980ab1d435c3e',
|
|
129
|
+
],
|
|
130
|
+
'automation-scaffold/fixtures/base.ts': [
|
|
131
|
+
'7c52ea17fad732825e327af59a7f0f3bf53db166f7191932ae51c97fd2825a39',
|
|
132
|
+
],
|
|
133
|
+
'automation-scaffold/fixtures/test-helpers.ts': [
|
|
134
|
+
'e2298972dea50e7df54265a7a94a232c0ff35c969f1b8778929079cfa2a1a541',
|
|
135
|
+
],
|
|
136
|
+
// Introduced in v1.12.0 - never shipped before, so an existing copy in a project is
|
|
137
|
+
// always user-authored (two live installations hand-wrote their own lane-lock.js).
|
|
138
|
+
'automation-scaffold/scripts/lane-config.js': [],
|
|
139
|
+
'automation-scaffold/scripts/lane-lock.js': [],
|
|
140
|
+
'automation-scaffold/scripts/global-setup-guards.js': [],
|
|
141
|
+
'integration-scaffold/playwright.config.ts': [
|
|
142
|
+
'7276ed4fda85f80e37381257bc559f4bb48055d359858f69f6b838f3671a9e2d',
|
|
143
|
+
],
|
|
144
|
+
'integration-scaffold/global-setup.ts': [
|
|
145
|
+
'f63f9588c4746a16e46345d1d4b781f2905b652481d563cfc075d9d8c8481bde',
|
|
146
|
+
],
|
|
147
|
+
'integration-scaffold/.env.example': [
|
|
148
|
+
'671461d7dc9883505cec314aefa1129a336b6a03b31a0eb2eb2ede2adda7c4ac',
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
|
|
73
152
|
const updated = [];
|
|
74
153
|
const skipped = [];
|
|
75
154
|
const warnings = [];
|
|
@@ -78,17 +157,17 @@ console.log(`\n[qa-framework/upgrade] ${dryRun ? '(dry-run) ' : ''}Upgrading fra
|
|
|
78
157
|
console.log(`[qa-framework/upgrade] Project root: ${cwd}\n`);
|
|
79
158
|
|
|
80
159
|
// ---------------------------------------------------------------------------
|
|
81
|
-
// 1. .github/skills/
|
|
160
|
+
// 1. .github/skills/ - always overwrite (framework-owned)
|
|
82
161
|
// ---------------------------------------------------------------------------
|
|
83
162
|
if (!fs.existsSync(skillsSrc)) {
|
|
84
|
-
warnings.push('skills/ source directory not found in package
|
|
163
|
+
warnings.push('skills/ source directory not found in package - skipping skill install');
|
|
85
164
|
} else {
|
|
86
165
|
fs.mkdirSync(skillsDest, { recursive: true });
|
|
87
166
|
copyDirForce(skillsSrc, skillsDest);
|
|
88
167
|
}
|
|
89
168
|
|
|
90
169
|
// ---------------------------------------------------------------------------
|
|
91
|
-
// 2. .github/instructions/qa-framework.instructions.md
|
|
170
|
+
// 2. .github/instructions/qa-framework.instructions.md - overwrite (framework-owned)
|
|
92
171
|
// ---------------------------------------------------------------------------
|
|
93
172
|
const copilotInstrPath = path.join(githubDir, 'instructions', 'qa-framework.instructions.md');
|
|
94
173
|
const instrTemplatePath = path.resolve(__dirname, '..', 'templates', 'qa-framework.instructions.md');
|
|
@@ -96,13 +175,40 @@ const copilotContent = fs.readFileSync(instrTemplatePath, 'utf8')
|
|
|
96
175
|
.replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
|
|
97
176
|
forceWrite(copilotInstrPath, copilotContent);
|
|
98
177
|
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// 2a. .claude/commands/qa-*.md and .claude/rules/qa-framework.md - overwrite (framework-owned)
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
const claudeCommandsDest = path.join(cwd, '.claude', 'commands');
|
|
182
|
+
for (const skillName of discoverSkillNames(skillsSrc)) {
|
|
183
|
+
forceWrite(path.join(claudeCommandsDest, commandFileName(skillName)), buildCommandContent(skillName));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const claudeRulesPath = path.join(cwd, '.claude', 'rules', 'qa-framework.md');
|
|
187
|
+
const rulesTemplatePath = path.resolve(__dirname, '..', 'templates', 'qa-framework.rules.md');
|
|
188
|
+
const claudeRulesContent = fs.readFileSync(rulesTemplatePath, 'utf8')
|
|
189
|
+
.replace('{{VERSION}}', config.frameworkVersion ?? '1.0.0');
|
|
190
|
+
forceWrite(claudeRulesPath, claudeRulesContent);
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// 2c. .claude/agents/qa-*.md - optional ANALISIS/PLAN sprint-cycle mode (Claude Code only)
|
|
194
|
+
// Gated by integrations.azureDevOps.sprintCycle.enabled. Framework-owned when the
|
|
195
|
+
// flag is on; never generated (and never deleted if it was hand-authored) otherwise.
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
if (isSprintCycleEnabled(config)) {
|
|
198
|
+
const templatesDir = path.resolve(__dirname, '..', 'templates');
|
|
199
|
+
const claudeAgentsDest = path.join(cwd, '.claude', 'agents');
|
|
200
|
+
for (const agentName of AGENT_NAMES) {
|
|
201
|
+
forceWrite(path.join(claudeAgentsDest, agentFileName(agentName)), buildAgentContent(agentName, templatesDir, config));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
99
205
|
// ---------------------------------------------------------------------------
|
|
100
206
|
// 2b. Migration: strip QA Framework section from old copilot-instructions.md
|
|
101
207
|
//
|
|
102
208
|
// Handles all cases:
|
|
103
|
-
// A. File has custom instructions + QA section
|
|
104
|
-
// B. File has ONLY QA section (any version)
|
|
105
|
-
// C. File does not mention QA Framework
|
|
209
|
+
// A. File has custom instructions + QA section -> keep custom, strip QA section
|
|
210
|
+
// B. File has ONLY QA section (any version) -> delete the file
|
|
211
|
+
// C. File does not mention QA Framework -> leave untouched
|
|
106
212
|
//
|
|
107
213
|
// The QA section always starts with "# QA Framework Instructions" across all
|
|
108
214
|
// previous versions, so that heading is the reliable split point.
|
|
@@ -118,22 +224,22 @@ if (fs.existsSync(oldCopilotPath)) {
|
|
|
118
224
|
const before = oldContent.slice(0, qaIdx).replace(/\s*\n---\s*$/, '').trim();
|
|
119
225
|
|
|
120
226
|
if (before.length === 0) {
|
|
121
|
-
// Case B: file contained only QA Framework content
|
|
227
|
+
// Case B: file contained only QA Framework content -> delete it
|
|
122
228
|
if (!dryRun) fs.unlinkSync(oldCopilotPath);
|
|
123
229
|
updated.push(oldCopilotPath);
|
|
124
|
-
console.log(` [deleted] .github/copilot-instructions.md (contained only QA Framework rules
|
|
230
|
+
console.log(` [deleted] .github/copilot-instructions.md (contained only QA Framework rules - now in .github/instructions/qa-framework.instructions.md)`);
|
|
125
231
|
} else {
|
|
126
|
-
// Case A: file had custom content too
|
|
232
|
+
// Case A: file had custom content too -> write back only the custom part
|
|
127
233
|
if (!dryRun) fs.writeFileSync(oldCopilotPath, before + '\n', 'utf8');
|
|
128
234
|
updated.push(oldCopilotPath);
|
|
129
|
-
console.log(` [cleaned] .github/copilot-instructions.md
|
|
235
|
+
console.log(` [cleaned] .github/copilot-instructions.md - removed QA Framework section, kept custom instructions`);
|
|
130
236
|
}
|
|
131
237
|
}
|
|
132
|
-
// Case C: no QA marker found
|
|
238
|
+
// Case C: no QA marker found -> leave untouched (no log noise)
|
|
133
239
|
}
|
|
134
240
|
|
|
135
241
|
// ---------------------------------------------------------------------------
|
|
136
|
-
// 3. qa/QA-STRUCTURE-GUIDE.md
|
|
242
|
+
// 3. qa/QA-STRUCTURE-GUIDE.md - overwrite (framework doc)
|
|
137
243
|
// ---------------------------------------------------------------------------
|
|
138
244
|
const structureGuideSrc = path.resolve(__dirname, '..', 'docs', 'folder-structure-guide.md');
|
|
139
245
|
const structureGuideDest = path.join(qaRoot, 'QA-STRUCTURE-GUIDE.md');
|
|
@@ -220,7 +326,7 @@ if (fs.existsSync(e2eDir)) {
|
|
|
220
326
|
console.log(` [migrated] e2e/${entry.name}/ -> e2e/tests/${entry.name}/`);
|
|
221
327
|
} else {
|
|
222
328
|
warnings.push(
|
|
223
|
-
`Cannot migrate e2e/${entry.name}/
|
|
329
|
+
`Cannot migrate e2e/${entry.name}/ - target e2e/tests/${entry.name}/ already exists. Merge manually.`
|
|
224
330
|
);
|
|
225
331
|
}
|
|
226
332
|
}
|
|
@@ -275,7 +381,7 @@ if (fs.existsSync(playwrightConfigPath)) {
|
|
|
275
381
|
if (/testDir\s*:\s*['"]\.['"]/.test(cfg)) {
|
|
276
382
|
cfg = cfg.replace(/testDir\s*:\s*['"]\.['"]/g, "testDir: './tests'");
|
|
277
383
|
cfgChanged = true;
|
|
278
|
-
console.log(` [patched] e2e/playwright.config.ts
|
|
384
|
+
console.log(` [patched] e2e/playwright.config.ts - testDir: '.' -> './tests'`);
|
|
279
385
|
}
|
|
280
386
|
|
|
281
387
|
// Inject testIgnore after testDir line if not present
|
|
@@ -285,7 +391,7 @@ if (fs.existsSync(playwrightConfigPath)) {
|
|
|
285
391
|
"$1 testIgnore: ['**/helpers/debug/**', '**/seeds/**'],\n"
|
|
286
392
|
);
|
|
287
393
|
cfgChanged = true;
|
|
288
|
-
console.log(` [patched] e2e/playwright.config.ts
|
|
394
|
+
console.log(` [patched] e2e/playwright.config.ts - added testIgnore`);
|
|
289
395
|
}
|
|
290
396
|
|
|
291
397
|
if (cfgChanged) {
|
|
@@ -344,7 +450,7 @@ if (fs.existsSync(testPlansDir)) {
|
|
|
344
450
|
updated.push(newFile);
|
|
345
451
|
console.log(` [migrated] 02-test-plans/${entry.name} -> 02-test-plans/sprints/legacy/${entry.name}`);
|
|
346
452
|
} else {
|
|
347
|
-
warnings.push(`Cannot migrate 02-test-plans/${entry.name}
|
|
453
|
+
warnings.push(`Cannot migrate 02-test-plans/${entry.name} - target already exists. Move manually.`);
|
|
348
454
|
}
|
|
349
455
|
}
|
|
350
456
|
}
|
|
@@ -363,7 +469,7 @@ if (!fs.existsSync(testCasesReadme)) {
|
|
|
363
469
|
if (!dryRun) {
|
|
364
470
|
fs.mkdirSync(path.dirname(testCasesReadme), { recursive: true });
|
|
365
471
|
fs.writeFileSync(testCasesReadme,
|
|
366
|
-
'# 03-test-cases/
|
|
472
|
+
'# 03-test-cases/ - Optional Standalone Test Cases\n\n' +
|
|
367
473
|
'> **v1.7.0+:** The primary location for test cases (with detailed steps) is now\n' +
|
|
368
474
|
'> `qa/02-test-plans/sprints/Sprint-{N}/Plan-de-Pruebas-{project}-Sprint-{N}-{module}.md`.\n' +
|
|
369
475
|
'>\n' +
|
|
@@ -441,6 +547,146 @@ if (fs.existsSync(memoryDir)) {
|
|
|
441
547
|
}
|
|
442
548
|
}
|
|
443
549
|
|
|
550
|
+
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// 9. Migration v1.11.x -> v1.12.0: lane-aware automation scaffold
|
|
553
|
+
//
|
|
554
|
+
// v1.12.0 rewrote the automation scaffold around a lane table: playwright.config.ts
|
|
555
|
+
// and global-setup.ts now require() three CommonJS helpers under e2e/scripts/.
|
|
556
|
+
// init.js installs those for NEW projects only, so existing installations never
|
|
557
|
+
// receive them. This section carries them across.
|
|
558
|
+
//
|
|
559
|
+
// Classification per file (the governing rule is: never break working user code):
|
|
560
|
+
//
|
|
561
|
+
// a) Lane scripts (scripts/lane-config.js, lane-lock.js, global-setup-guards.js)
|
|
562
|
+
// plus playwright.config.ts and global-setup.ts form ONE ATOMIC GROUP,
|
|
563
|
+
// because config/global-setup require() the scripts by path. Installing a
|
|
564
|
+
// subset yields a project where Playwright cannot start at all. The group is
|
|
565
|
+
// installed only when EVERY member is either absent or provably pristine;
|
|
566
|
+
// otherwise the whole group is skipped with a warning and the rest of the
|
|
567
|
+
// upgrade continues. This is a coherent skip, not an abort: the project keeps
|
|
568
|
+
// the working setup it already had, so it is never left half-migrated.
|
|
569
|
+
//
|
|
570
|
+
// b) fixtures/{auth,base,test-helpers}.ts are per-file. They are leaf modules -
|
|
571
|
+
// nothing in the scaffold require()s them by path - so a mixed state is
|
|
572
|
+
// still coherent.
|
|
573
|
+
//
|
|
574
|
+
// c) .env.example is a reference template, never live config (the real file is
|
|
575
|
+
// .env, which this migration never touches). Overwritten when pristine.
|
|
576
|
+
//
|
|
577
|
+
// "Pristine" means the file's normalised sha256 matches a hash this framework
|
|
578
|
+
// actually shipped in some earlier release (SHIPPED_SCAFFOLD_HASHES). Anything
|
|
579
|
+
// else is user-owned and is only ever warned about. The project's recorded
|
|
580
|
+
// frameworkVersion is deliberately NOT used: live installations report values
|
|
581
|
+
// like 1.1.3 and 1.0.0 that do not correspond to any real scaffold release.
|
|
582
|
+
//
|
|
583
|
+
// Files that stay user-owned are additionally scanned for the trace-safety
|
|
584
|
+
// defect (.fill(password) leaks the password into Playwright traces). That is
|
|
585
|
+
// reported with file and line only - never patched, because the surrounding
|
|
586
|
+
// shapes differ per project (passwordSelector vs PASSWORD_SEL) and a bad regex
|
|
587
|
+
// edit to a working login is exactly the breakage this section must avoid.
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
const AUTOMATION_LANE_SCRIPTS = ['lane-config.js', 'lane-lock.js', 'global-setup-guards.js'];
|
|
590
|
+
const AUTOMATION_FIXTURES = ['auth.ts', 'base.ts', 'test-helpers.ts'];
|
|
591
|
+
|
|
592
|
+
// A bare e2e/ directory is not evidence of an automation setup: section 5 above
|
|
593
|
+
// creates e2e/tests/{helpers/debug,seeds}/ unconditionally. Only migrate a project
|
|
594
|
+
// that actually has a scaffold, i.e. one of the files this migration would replace.
|
|
595
|
+
const hasAutomationScaffold = ['playwright.config.ts', 'global-setup.ts', 'package.json']
|
|
596
|
+
.some((f) => fs.existsSync(path.join(e2eDir, f)));
|
|
597
|
+
|
|
598
|
+
if (hasAutomationScaffold) {
|
|
599
|
+
const scaffoldSrc = path.resolve(__dirname, '..', 'templates', 'automation-scaffold');
|
|
600
|
+
|
|
601
|
+
// --- (a) the atomic lane group -------------------------------------------
|
|
602
|
+
const groupMembers = [
|
|
603
|
+
...AUTOMATION_LANE_SCRIPTS.map((f) => ({
|
|
604
|
+
rel: `scripts/${f}`,
|
|
605
|
+
src: path.join(scaffoldSrc, 'scripts', f),
|
|
606
|
+
dest: path.join(e2eDir, 'scripts', f),
|
|
607
|
+
key: `automation-scaffold/scripts/${f}`,
|
|
608
|
+
})),
|
|
609
|
+
...['playwright.config.ts', 'global-setup.ts'].map((f) => ({
|
|
610
|
+
rel: f,
|
|
611
|
+
src: path.join(scaffoldSrc, f),
|
|
612
|
+
dest: path.join(e2eDir, f),
|
|
613
|
+
key: `automation-scaffold/${f}`,
|
|
614
|
+
})),
|
|
615
|
+
];
|
|
616
|
+
|
|
617
|
+
const blockers = groupMembers.filter((m) => fs.existsSync(m.dest) && !isPristine(m.dest, m.key));
|
|
618
|
+
|
|
619
|
+
if (blockers.length === 0) {
|
|
620
|
+
for (const m of groupMembers) {
|
|
621
|
+
forceWrite(m.dest, fs.readFileSync(m.src, 'utf8'));
|
|
622
|
+
console.log(` [updated] e2e/${m.rel}`);
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
warnings.push(
|
|
626
|
+
`Lane-aware automation scaffold (v1.12.0) NOT installed - these files are user-owned:\n` +
|
|
627
|
+
blockers.map((m) => ` e2e/${m.rel}`).join('\n') +
|
|
628
|
+
`\n playwright.config.ts and global-setup.ts require() e2e/scripts/*.js, so the\n` +
|
|
629
|
+
` group is installed all-or-nothing. Your current setup is left working and\n` +
|
|
630
|
+
` untouched. To adopt lanes, merge these by hand from:\n` +
|
|
631
|
+
` node_modules/@keber/qa-framework/templates/automation-scaffold/`
|
|
632
|
+
);
|
|
633
|
+
for (const m of groupMembers) skipped.push(m.dest);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// --- (b) fixtures, per file ----------------------------------------------
|
|
637
|
+
for (const file of AUTOMATION_FIXTURES) {
|
|
638
|
+
const dest = path.join(e2eDir, 'fixtures', file);
|
|
639
|
+
const key = `automation-scaffold/fixtures/${file}`;
|
|
640
|
+
if (!fs.existsSync(dest) || isPristine(dest, key)) {
|
|
641
|
+
forceWrite(dest, fs.readFileSync(path.join(scaffoldSrc, 'fixtures', file), 'utf8'));
|
|
642
|
+
console.log(` [updated] e2e/fixtures/${file}`);
|
|
643
|
+
} else {
|
|
644
|
+
skipped.push(dest);
|
|
645
|
+
warnings.push(
|
|
646
|
+
`e2e/fixtures/${file} is user-owned - kept as is (v1.12.0 version not applied).`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// --- (c) .env.example ------------------------------------------------------
|
|
652
|
+
upgradeReferenceEnvExample(
|
|
653
|
+
path.join(e2eDir, '.env.example'),
|
|
654
|
+
path.join(scaffoldSrc, '.env.example'),
|
|
655
|
+
'automation-scaffold/.env.example',
|
|
656
|
+
'e2e/.env.example'
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
// --- trace safety on everything left user-owned ---------------------------
|
|
660
|
+
reportTraceSafety(e2eDir);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// --- integration scaffold: no lane coupling, so purely per-file -------------
|
|
664
|
+
// Same reasoning as above: require a real scaffold file, not just the folder.
|
|
665
|
+
const hasIntegrationScaffold = ['playwright.config.ts', 'global-setup.ts', 'package.json']
|
|
666
|
+
.some((f) => fs.existsSync(path.join(integrationDir, f)));
|
|
667
|
+
|
|
668
|
+
if (hasIntegrationScaffold) {
|
|
669
|
+
const integrationSrc = path.resolve(__dirname, '..', 'templates', 'integration-scaffold');
|
|
670
|
+
for (const file of ['playwright.config.ts', 'global-setup.ts']) {
|
|
671
|
+
const dest = path.join(integrationDir, file);
|
|
672
|
+
const key = `integration-scaffold/${file}`;
|
|
673
|
+
if (!fs.existsSync(dest) || isPristine(dest, key)) {
|
|
674
|
+
forceWrite(dest, fs.readFileSync(path.join(integrationSrc, file), 'utf8'));
|
|
675
|
+
console.log(` [updated] integration/${file}`);
|
|
676
|
+
} else {
|
|
677
|
+
skipped.push(dest);
|
|
678
|
+
warnings.push(`integration/${file} is user-owned - kept as is (v1.12.0 version not applied).`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
upgradeReferenceEnvExample(
|
|
682
|
+
path.join(integrationDir, '.env.example'),
|
|
683
|
+
path.join(integrationSrc, '.env.example'),
|
|
684
|
+
'integration-scaffold/.env.example',
|
|
685
|
+
'integration/.env.example'
|
|
686
|
+
);
|
|
687
|
+
reportTraceSafety(integrationDir);
|
|
688
|
+
}
|
|
689
|
+
|
|
444
690
|
// ---------------------------------------------------------------------------
|
|
445
691
|
// Summary
|
|
446
692
|
// ---------------------------------------------------------------------------
|
|
@@ -451,7 +697,7 @@ if (updated.length) {
|
|
|
451
697
|
}
|
|
452
698
|
if (skipped.length) {
|
|
453
699
|
console.log('\n Skipped (already up to date):');
|
|
454
|
-
for (const f of skipped) console.log(`
|
|
700
|
+
for (const f of skipped) console.log(` - ${path.relative(cwd, f)}`);
|
|
455
701
|
}
|
|
456
702
|
if (warnings.length) {
|
|
457
703
|
console.log('\n Warnings:');
|
|
@@ -491,3 +737,93 @@ function copyDirForce(srcDir, destDir) {
|
|
|
491
737
|
}
|
|
492
738
|
}
|
|
493
739
|
|
|
740
|
+
|
|
741
|
+
// Normalised content hash: strips a UTF-8 BOM and CRLF so that a file which only
|
|
742
|
+
// differs by line endings or a BOM (both of which tooling rewrites silently) is
|
|
743
|
+
// still recognised as the framework's own output.
|
|
744
|
+
function scaffoldHash(content) {
|
|
745
|
+
const normalised = content.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
746
|
+
return require('crypto').createHash('sha256').update(normalised, 'utf8').digest('hex');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// A file is "pristine" when its content is byte-identical (after normalisation) to
|
|
750
|
+
// something this framework actually shipped. A key with no recorded hashes - a file
|
|
751
|
+
// introduced in the current release - can never be pristine, so an existing copy is
|
|
752
|
+
// always treated as user-owned.
|
|
753
|
+
function isPristine(filePath, key) {
|
|
754
|
+
let content;
|
|
755
|
+
try {
|
|
756
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
757
|
+
} catch {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
const hash = scaffoldHash(content);
|
|
761
|
+
|
|
762
|
+
// Already identical to what this release ships: nothing to do, and certainly not a
|
|
763
|
+
// user edit. Computed rather than tabulated so the table never goes stale on release.
|
|
764
|
+
try {
|
|
765
|
+
const current = path.resolve(__dirname, '..', 'templates', ...key.split('/'));
|
|
766
|
+
if (fs.existsSync(current) && scaffoldHash(fs.readFileSync(current, 'utf8')) === hash) return true;
|
|
767
|
+
} catch {
|
|
768
|
+
/* fall through to the historical table */
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const known = SHIPPED_SCAFFOLD_HASHES[key];
|
|
772
|
+
return Boolean(known) && known.includes(hash);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// .env.example is a reference template, never live configuration - the real values
|
|
776
|
+
// live in .env, which is never touched here. Overwrite it when pristine; when the
|
|
777
|
+
// user has edited it, keep theirs and say so.
|
|
778
|
+
function upgradeReferenceEnvExample(dest, src, key, label) {
|
|
779
|
+
if (!fs.existsSync(src)) return;
|
|
780
|
+
if (!fs.existsSync(dest) || isPristine(dest, key)) {
|
|
781
|
+
forceWrite(dest, fs.readFileSync(src, 'utf8'));
|
|
782
|
+
console.log(` [updated] ${label}`);
|
|
783
|
+
} else {
|
|
784
|
+
skipped.push(dest);
|
|
785
|
+
warnings.push(
|
|
786
|
+
`${label} is user-owned - kept as is. New v1.12.0 keys may be missing; compare against\n` +
|
|
787
|
+
` node_modules/@keber/qa-framework/templates/${key}`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Trace safety (see skills/qa-automation/references/patterns.md, "Pattern 7"):
|
|
793
|
+
// page.locator(...).fill(password) records the password in the Playwright trace.
|
|
794
|
+
// Report location only. The surrounding code differs per project, so an automated
|
|
795
|
+
// rewrite of a working login is more dangerous than the finding itself.
|
|
796
|
+
function reportTraceSafety(rootDir) {
|
|
797
|
+
const offenders = [];
|
|
798
|
+
const FILL_PASSWORD = /\.fill\(\s*(?:password|pwd|[A-Za-z_$]*(?:[Pp]ass(?:word)?|PASSWORD)[A-Za-z_$]*)\s*\)/;
|
|
799
|
+
|
|
800
|
+
(function walk(dir) {
|
|
801
|
+
let entries;
|
|
802
|
+
try {
|
|
803
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
804
|
+
} catch {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
for (const entry of entries) {
|
|
808
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
809
|
+
const full = path.join(dir, entry.name);
|
|
810
|
+
if (entry.isDirectory()) {
|
|
811
|
+
walk(full);
|
|
812
|
+
} else if (/\.ts$/.test(entry.name)) {
|
|
813
|
+
const lines = fs.readFileSync(full, 'utf8').split(/\r?\n/);
|
|
814
|
+
lines.forEach((line, i) => {
|
|
815
|
+
if (FILL_PASSWORD.test(line)) offenders.push(`${path.relative(cwd, full)}:${i + 1}`);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
})(rootDir);
|
|
820
|
+
|
|
821
|
+
if (offenders.length) {
|
|
822
|
+
warnings.push(
|
|
823
|
+
`Trace safety: the password is written with .fill(), which stores it in the\n` +
|
|
824
|
+
` Playwright trace. Replace with the page.evaluate() form ("Pattern 7" in\n` +
|
|
825
|
+
` .github/skills/qa-automation/references/patterns.md):\n` +
|
|
826
|
+
offenders.map((o) => ` ${o}`).join('\n')
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
package/scripts/validate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* scripts/validate.js
|
|
3
|
+
* scripts/validate.js - Validate qa/ folder structure and conventions
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* qa-framework validate
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Checks:
|
|
11
11
|
* 1. Required top-level folders exist
|
|
12
|
-
* 2. All submodule folders contain the 6 required spec files
|
|
12
|
+
* 2. All submodule folders under 01-specifications/ contain the 6 required spec files
|
|
13
13
|
* 3. No plaintext credentials in spec files (basic scan)
|
|
14
14
|
* 4. Test case naming convention (TC-NNN pattern in spec files)
|
|
15
15
|
* 5. [--strict] Automation spec files exist for every submodule
|
|
@@ -47,7 +47,7 @@ const warnings = [];
|
|
|
47
47
|
// -----------------------------------------------------------------------
|
|
48
48
|
const requiredFolders = [
|
|
49
49
|
'00-standards',
|
|
50
|
-
'05-test-
|
|
50
|
+
'05-test-execution',
|
|
51
51
|
'06-defects',
|
|
52
52
|
'07-automation',
|
|
53
53
|
];
|
|
@@ -70,18 +70,18 @@ const SPEC_FILES = [
|
|
|
70
70
|
'05-test-scenarios.md',
|
|
71
71
|
];
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
// Specs live at qa/01-specifications/{module}/{submodule}/. Rooting the scan
|
|
74
|
+
// here keeps it at the right depth and means sibling folders such as
|
|
75
|
+
// 02-test-plans/ or memory/ are never mistaken for modules in the first place.
|
|
76
|
+
const specsRoot = path.join(qaRoot, '01-specifications');
|
|
77
77
|
|
|
78
|
-
if (fs.existsSync(
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
if (fs.existsSync(specsRoot)) {
|
|
79
|
+
const moduleDirs = fs.readdirSync(specsRoot).filter(d =>
|
|
80
|
+
fs.statSync(path.join(specsRoot, d)).isDirectory()
|
|
81
|
+
);
|
|
82
82
|
|
|
83
|
-
for (const moduleDir of
|
|
84
|
-
const modulePath = path.join(
|
|
83
|
+
for (const moduleDir of moduleDirs) {
|
|
84
|
+
const modulePath = path.join(specsRoot, moduleDir);
|
|
85
85
|
const subDirs = fs.readdirSync(modulePath).filter(d =>
|
|
86
86
|
fs.statSync(path.join(modulePath, d)).isDirectory()
|
|
87
87
|
);
|
|
@@ -90,15 +90,15 @@ if (fs.existsSync(qaRoot)) {
|
|
|
90
90
|
const subPath = path.join(modulePath, subDir);
|
|
91
91
|
for (const specFile of SPEC_FILES) {
|
|
92
92
|
if (!fs.existsSync(path.join(subPath, specFile))) {
|
|
93
|
-
errors.push(`Missing spec file: qa/${moduleDir}/${subDir}/${specFile}`);
|
|
93
|
+
errors.push(`Missing spec file: qa/01-specifications/${moduleDir}/${subDir}/${specFile}`);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// Strict: automation spec must exist
|
|
98
98
|
if (strict) {
|
|
99
|
-
const specTs = path.join(qaRoot, '07-automation', 'e2e', moduleDir, `${subDir}.spec.ts`);
|
|
99
|
+
const specTs = path.join(qaRoot, '07-automation', 'e2e', 'tests', moduleDir, `${subDir}.spec.ts`);
|
|
100
100
|
if (!fs.existsSync(specTs)) {
|
|
101
|
-
warnings.push(`[STRICT] No automation spec found: qa/07-automation/e2e/${moduleDir}/${subDir}.spec.ts`);
|
|
101
|
+
warnings.push(`[STRICT] No automation spec found: qa/07-automation/e2e/tests/${moduleDir}/${subDir}.spec.ts`);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
|
@@ -147,7 +147,7 @@ for (const f of specFiles) {
|
|
|
147
147
|
// -----------------------------------------------------------------------
|
|
148
148
|
console.log('');
|
|
149
149
|
if (errors.length === 0 && warnings.length === 0) {
|
|
150
|
-
console.log('✅ Validation passed
|
|
150
|
+
console.log('✅ Validation passed - no issues found.');
|
|
151
151
|
process.exit(0);
|
|
152
152
|
}
|
|
153
153
|
|