@keber/qa-framework 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +233 -0
  3. package/agent-instructions/00-module-analysis.md +263 -0
  4. package/agent-instructions/01-spec-generation.md +278 -0
  5. package/agent-instructions/02-test-plan-generation.md +202 -0
  6. package/agent-instructions/03-test-case-generation.md +147 -0
  7. package/agent-instructions/04-automation-generation.md +310 -0
  8. package/agent-instructions/04b-test-stabilization.md +306 -0
  9. package/agent-instructions/05-ado-integration.md +244 -0
  10. package/agent-instructions/06-maintenance.md +125 -0
  11. package/docs/architecture.md +227 -0
  12. package/docs/comparison-matrix.md +131 -0
  13. package/docs/final-report.md +279 -0
  14. package/docs/folder-structure-guide.md +291 -0
  15. package/docs/generalization-decisions.md +203 -0
  16. package/docs/installation.md +239 -0
  17. package/docs/spec-driven-philosophy.md +170 -0
  18. package/docs/usage-with-agent.md +203 -0
  19. package/examples/module-example/README.md +34 -0
  20. package/examples/module-example/suppliers/00-inventory.md +56 -0
  21. package/examples/module-example/suppliers/suppliers-create.spec.ts +148 -0
  22. package/integrations/ado-powershell/README.md +75 -0
  23. package/integrations/ado-powershell/pipelines/azure-pipeline-qa.yml +133 -0
  24. package/integrations/ado-powershell/scripts/create-testplan-from-mapping.ps1 +114 -0
  25. package/integrations/ado-powershell/scripts/inject-ado-ids.ps1 +96 -0
  26. package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +93 -0
  27. package/integrations/playwright/README.md +68 -0
  28. package/integrations/playwright-azure-reporter/README.md +88 -0
  29. package/package.json +57 -0
  30. package/qa-framework.config.json +87 -0
  31. package/scripts/cli.js +74 -0
  32. package/scripts/generate.js +92 -0
  33. package/scripts/init.js +322 -0
  34. package/scripts/validate.js +184 -0
  35. package/templates/automation-scaffold/.env.example +56 -0
  36. package/templates/automation-scaffold/fixtures/auth.ts +77 -0
  37. package/templates/automation-scaffold/fixtures/test-helpers.ts +85 -0
  38. package/templates/automation-scaffold/global-setup.ts +106 -0
  39. package/templates/automation-scaffold/package.json +24 -0
  40. package/templates/automation-scaffold/playwright.config.ts +85 -0
  41. package/templates/defect-report.md +101 -0
  42. package/templates/execution-report.md +116 -0
  43. package/templates/session-summary.md +73 -0
  44. package/templates/specification/00-inventory.md +81 -0
  45. package/templates/specification/01-business-rules.md +90 -0
  46. package/templates/specification/02-workflows.md +114 -0
  47. package/templates/specification/03-roles-permissions.md +49 -0
  48. package/templates/specification/04-test-data.md +104 -0
  49. package/templates/specification/05-test-scenarios.md +226 -0
  50. package/templates/test-case.md +81 -0
  51. package/templates/test-plan.md +130 -0
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/init.js — Scaffold qa/ folder structure from config
4
+ *
5
+ * Usage:
6
+ * qa-framework init
7
+ * qa-framework init --config ./my-project.config.json
8
+ *
9
+ * Reads qa-framework.config.json and creates the full qa/ directory tree
10
+ * including per-module and per-submodule folders with placeholder spec files.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ // --- Parse args ---
19
+ const args = process.argv.slice(2);
20
+ const configFlag = args.indexOf('--config');
21
+ const skipIfExists = args.includes('--skip-if-exists');
22
+
23
+ if (configFlag !== -1 && !args[configFlag + 1]) {
24
+ console.error('[qa-framework/init] Missing value for --config <path>');
25
+ process.exit(1);
26
+ }
27
+
28
+ const cwd = process.cwd();
29
+ const explicitConfigPath = configFlag !== -1 ? path.resolve(cwd, args[configFlag + 1]) : null;
30
+ const rootConfigPath = path.resolve(cwd, 'qa-framework.config.json');
31
+ const qaConfigPath = path.resolve(cwd, 'qa', 'qa-framework.config.json');
32
+
33
+ // --- Load or bootstrap config ---
34
+ let config;
35
+ let configSource;
36
+
37
+ if (explicitConfigPath) {
38
+ if (!fs.existsSync(explicitConfigPath)) {
39
+ console.error(`[qa-framework/init] Config file not found: ${explicitConfigPath}`);
40
+ process.exit(1);
41
+ }
42
+ config = JSON.parse(fs.readFileSync(explicitConfigPath, 'utf8'));
43
+ configSource = explicitConfigPath;
44
+ } else if (fs.existsSync(rootConfigPath)) {
45
+ config = JSON.parse(fs.readFileSync(rootConfigPath, 'utf8'));
46
+ configSource = rootConfigPath;
47
+ } else if (fs.existsSync(qaConfigPath)) {
48
+ config = JSON.parse(fs.readFileSync(qaConfigPath, 'utf8'));
49
+ configSource = qaConfigPath;
50
+ } else {
51
+ config = buildBootstrapConfig();
52
+ configSource = 'generated defaults';
53
+ console.log('[qa-framework/init] No config file found. Bootstrapping with default config.');
54
+ }
55
+
56
+ const qaRoot = path.resolve(cwd, config.conventions?.qaRoot ?? 'qa');
57
+ const localConfigPath = path.join(qaRoot, 'qa-framework.config.json');
58
+
59
+ // --skip-if-exists: bail out silently when qa/ is already initialised
60
+ if (skipIfExists && fs.existsSync(localConfigPath)) {
61
+ console.log('[qa-framework/init] qa/ already initialised — skipping (postinstall).');
62
+ process.exit(0);
63
+ }
64
+
65
+ if (!fs.existsSync(localConfigPath)) {
66
+ fs.mkdirSync(qaRoot, { recursive: true });
67
+ fs.writeFileSync(localConfigPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
68
+ console.log(` [created] ${path.relative(cwd, localConfigPath)}`);
69
+ } else {
70
+ console.log(` [exists] ${path.relative(cwd, localConfigPath)} — skipped`);
71
+ }
72
+
73
+ console.log(`[qa-framework/init] Scaffolding qa/ at: ${qaRoot}`);
74
+ console.log(`[qa-framework/init] Using config source: ${configSource}`);
75
+
76
+ // --- Top-level folders ---
77
+ const topLevelFolders = [
78
+ '00-guides',
79
+ '00-standards',
80
+ '05-test-plans',
81
+ '06-defects/open',
82
+ '06-defects/resolved',
83
+ '07-automation/e2e',
84
+ '08-azure-integration',
85
+ ];
86
+
87
+ for (const folder of topLevelFolders) {
88
+ const fullPath = path.join(qaRoot, folder);
89
+ fs.mkdirSync(fullPath, { recursive: true });
90
+ console.log(` [created] ${path.relative(process.cwd(), fullPath)}/`);
91
+ }
92
+
93
+ // --- Standards placeholder ---
94
+ const stdDir = path.join(qaRoot, '00-standards');
95
+ writeIfMissing(path.join(stdDir, 'naming-conventions.md'), `# Naming Conventions\n\n> Fill in per your project. See keber/qa-framework docs/spec-driven-philosophy.md.\n`);
96
+ writeIfMissing(path.join(stdDir, 'bug-report-template.md'), fs.readFileSync(
97
+ path.resolve(__dirname, '..', 'templates', 'defect-report.md'), 'utf8'
98
+ ));
99
+
100
+ // --- Per-module folders ---
101
+ const modules = config.modules ?? [];
102
+ if (modules.length === 0) {
103
+ console.warn('[qa-framework/init] No modules defined in config. Add "modules" array to qa-framework.config.json.');
104
+ }
105
+
106
+ const SPEC_FILES = [
107
+ '00-inventory.md',
108
+ '01-business-rules.md',
109
+ '02-workflows.md',
110
+ '03-roles-permissions.md',
111
+ '04-test-data.md',
112
+ '05-test-scenarios.md',
113
+ ];
114
+
115
+ const templateDir = path.resolve(__dirname, '..', 'templates', 'specification');
116
+
117
+ for (const mod of modules) {
118
+ const moduleKey = mod.key ?? mod.name.toLowerCase().replace(/\s+/g, '-');
119
+ const submodules = mod.submodules ?? [];
120
+
121
+ for (const sub of submodules) {
122
+ const subKey = sub.key ?? sub.name.toLowerCase().replace(/\s+/g, '-');
123
+ const subDir = path.join(qaRoot, moduleKey, subKey);
124
+ fs.mkdirSync(subDir, { recursive: true });
125
+
126
+ for (const specFile of SPEC_FILES) {
127
+ const dest = path.join(subDir, specFile);
128
+ if (!fs.existsSync(dest)) {
129
+ const src = path.join(templateDir, specFile);
130
+ if (fs.existsSync(src)) {
131
+ let content = fs.readFileSync(src, 'utf8');
132
+ content = content
133
+ .replace(/\{\{MODULE_NAME\}\}/g, mod.name)
134
+ .replace(/\{\{SUBMODULE_NAME\}\}/g, sub.name)
135
+ .replace(/\{\{MODULE\}\}/g, moduleKey.toUpperCase())
136
+ .replace(/\{\{SUB\}\}/g, subKey.toUpperCase());
137
+ fs.writeFileSync(dest, content, 'utf8');
138
+ } else {
139
+ fs.writeFileSync(dest, `# ${specFile}\n\n> Auto-generated placeholder\n`, 'utf8');
140
+ }
141
+ console.log(` [created] ${path.relative(process.cwd(), dest)}`);
142
+ } else {
143
+ console.log(` [exists] ${path.relative(process.cwd(), dest)} — skipped`);
144
+ }
145
+ }
146
+
147
+ // Create automation e2e subdir placeholder
148
+ const e2eDir = path.join(qaRoot, '07-automation', 'e2e', moduleKey);
149
+ fs.mkdirSync(e2eDir, { recursive: true });
150
+ const specTs = path.join(e2eDir, `${subKey}.spec.ts`);
151
+ if (!fs.existsSync(specTs)) {
152
+ fs.writeFileSync(specTs, specScaffold(mod.name, sub.name, moduleKey, subKey), 'utf8');
153
+ console.log(` [created] ${path.relative(process.cwd(), specTs)}`);
154
+ }
155
+ }
156
+ }
157
+
158
+ // --- Automation scaffold ---
159
+ const automationDir = path.join(qaRoot, '07-automation');
160
+ const scaffoldSrc = path.resolve(__dirname, '..', 'templates', 'automation-scaffold');
161
+ for (const file of ['playwright.config.ts', 'global-setup.ts', '.env.example', 'package.json']) {
162
+ const dest = path.join(automationDir, file);
163
+ if (!fs.existsSync(dest)) {
164
+ fs.copyFileSync(path.join(scaffoldSrc, file), dest);
165
+ console.log(` [created] ${path.relative(process.cwd(), dest)}`);
166
+ }
167
+ }
168
+ const fixturesDir = path.join(automationDir, 'fixtures');
169
+ fs.mkdirSync(fixturesDir, { recursive: true });
170
+ for (const file of ['auth.ts', 'test-helpers.ts']) {
171
+ const dest = path.join(fixturesDir, file);
172
+ if (!fs.existsSync(dest)) {
173
+ fs.copyFileSync(path.join(scaffoldSrc, 'fixtures', file), dest);
174
+ console.log(` [created] ${path.relative(process.cwd(), dest)}`);
175
+ }
176
+ }
177
+
178
+ // --- ADO integration placeholder ---
179
+ const adoDir = path.join(qaRoot, '08-azure-integration');
180
+ writeIfMissing(path.join(adoDir, 'README.md'), `# ADO Integration\n\nSee keber/qa-framework integrations/ado-powershell/ for setup instructions.\n`);
181
+ writeIfMissing(path.join(adoDir, 'module-registry.json'), JSON.stringify({ modules: [] }, null, 2));
182
+
183
+ // --- Agent instructions → qa/00-guides/ ---
184
+ const agentInstrSrc = path.resolve(__dirname, '..', 'agent-instructions');
185
+ const guidesDir = path.join(qaRoot, '00-guides');
186
+ const agentFileMap = {
187
+ '00-module-analysis.md': 'AGENT-INSTRUCTIONS-MODULE-ANALYSIS.md',
188
+ '01-spec-generation.md': 'AGENT-INSTRUCTIONS-SPEC-GENERATION.md',
189
+ '02-test-plan-generation.md': 'AGENT-INSTRUCTIONS-TEST-PLAN.md',
190
+ '03-test-case-generation.md': 'AGENT-INSTRUCTIONS-TEST-CASES.md',
191
+ '04-automation-generation.md':'AGENT-INSTRUCTIONS-AUTOMATION.md',
192
+ '04b-test-stabilization.md': 'AGENT-INSTRUCTIONS-TEST-STABILIZATION.md',
193
+ '05-ado-integration.md': 'AGENT-INSTRUCTIONS-ADO-INTEGRATION.md',
194
+ '06-maintenance.md': 'AGENT-INSTRUCTIONS-MAINTENANCE.md',
195
+ };
196
+ for (const [src, dest] of Object.entries(agentFileMap)) {
197
+ const srcPath = path.join(agentInstrSrc, src);
198
+ const destPath = path.join(guidesDir, dest);
199
+ if (fs.existsSync(srcPath)) {
200
+ writeIfMissing(destPath, fs.readFileSync(srcPath, 'utf8'));
201
+ }
202
+ }
203
+
204
+ // --- QA structure guide → qa/QA-STRUCTURE-GUIDE.md ---
205
+ const structureGuideSrc = path.resolve(__dirname, '..', 'docs', 'folder-structure-guide.md');
206
+ const structureGuideDest = path.join(qaRoot, 'QA-STRUCTURE-GUIDE.md');
207
+ if (fs.existsSync(structureGuideSrc)) {
208
+ writeIfMissing(structureGuideDest, fs.readFileSync(structureGuideSrc, 'utf8'));
209
+ }
210
+
211
+ // --- .github/copilot-instructions.md ---
212
+ const githubDir = path.join(cwd, '.github');
213
+ const copilotInstrPath = path.join(githubDir, 'copilot-instructions.md');
214
+ const copilotInstrContent = `# QA Framework Instructions
215
+
216
+ This project uses \`@keber/qa-framework\` v${config.frameworkVersion ?? '1.0.0'} for spec-driven automated testing.
217
+
218
+ ## Agent behavior rules
219
+
220
+ 1. Before performing any QA task, read the relevant instruction file from \`qa/00-guides/\`
221
+ 2. Always save artifacts in the correct \`qa/\` subfolder — refer to \`qa/QA-STRUCTURE-GUIDE.md\`
222
+ 3. Never hardcode credentials — always use env vars and \`<PLACEHOLDER>\` in documentation
223
+ 4. Follow the naming conventions in \`qa/00-standards/naming-conventions.md\`
224
+ 5. Project QA config is at \`qa/qa-framework.config.json\`
225
+
226
+ ## Available agent instructions
227
+
228
+ | Task | Instruction file |
229
+ |---|---|
230
+ | Analyze a module | \`qa/00-guides/AGENT-INSTRUCTIONS-MODULE-ANALYSIS.md\` |
231
+ | Generate specifications | \`qa/00-guides/AGENT-INSTRUCTIONS-SPEC-GENERATION.md\` |
232
+ | Generate a test plan | \`qa/00-guides/AGENT-INSTRUCTIONS-TEST-PLAN.md\` |
233
+ | Generate test cases | \`qa/00-guides/AGENT-INSTRUCTIONS-TEST-CASES.md\` |
234
+ | Generate automation | \`qa/00-guides/AGENT-INSTRUCTIONS-AUTOMATION.md\` |
235
+ | Stabilize failing tests | \`qa/00-guides/AGENT-INSTRUCTIONS-TEST-STABILIZATION.md\` |
236
+ | Sync with Azure DevOps | \`qa/00-guides/AGENT-INSTRUCTIONS-ADO-INTEGRATION.md\` |
237
+ | Maintenance after changes | \`qa/00-guides/AGENT-INSTRUCTIONS-MAINTENANCE.md\` |
238
+ `;
239
+ writeIfMissing(copilotInstrPath, copilotInstrContent);
240
+
241
+ console.log('\n[qa-framework/init] Done!\n');
242
+ console.log('Next steps:');
243
+ console.log(' 1. Copy qa/07-automation/.env.example to qa/07-automation/.env and fill in credentials');
244
+ console.log(' 2. Add qa/07-automation/.env and qa/07-automation/.auth/ to .gitignore');
245
+ console.log(' 3. Run: cd qa/07-automation && npm install && npx playwright install chromium');
246
+ console.log(' 4. Start filling in spec files with your module\'s real data');
247
+
248
+ // -----------------------------------------------------------------------
249
+ // Helpers
250
+ // -----------------------------------------------------------------------
251
+ function writeIfMissing(filePath, content) {
252
+ if (!fs.existsSync(filePath)) {
253
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
254
+ fs.writeFileSync(filePath, content, 'utf8');
255
+ console.log(` [created] ${path.relative(process.cwd(), filePath)}`);
256
+ }
257
+ }
258
+
259
+ function specScaffold(modName, subName, modKey, subKey) {
260
+ return `import { test, expect } from '@playwright/test';
261
+
262
+ const EXEC_IDX = Math.floor(Date.now() / 60_000) % 100_000;
263
+
264
+ test.describe('${modName} > ${subName} @P0', () => {
265
+
266
+ test('[TC-${modKey.toUpperCase()}-${subKey.toUpperCase()}-001] TODO: Add test title @P0', async ({ page }) => {
267
+ // TODO: Replace with real test steps from 05-test-scenarios.md
268
+ await page.goto('/');
269
+ expect(true).toBe(true);
270
+ });
271
+
272
+ });
273
+ `;
274
+ }
275
+
276
+ function buildBootstrapConfig() {
277
+ const templatePath = path.resolve(__dirname, '..', 'qa-framework.config.json');
278
+ const projectName = path.basename(cwd);
279
+
280
+ let base = {
281
+ frameworkVersion: '1.0.0',
282
+ project: {
283
+ name: projectName,
284
+ displayName: projectName,
285
+ description: '',
286
+ qaBaseUrl: '',
287
+ techStack: '',
288
+ loginPath: ''
289
+ },
290
+ modules: [],
291
+ conventions: {
292
+ qaRoot: 'qa',
293
+ language: 'es',
294
+ locale: 'es-CL'
295
+ },
296
+ integrations: {
297
+ playwright: { enabled: false },
298
+ azureDevOps: { enabled: false }
299
+ }
300
+ };
301
+
302
+ if (fs.existsSync(templatePath)) {
303
+ try {
304
+ base = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
305
+ } catch {
306
+ // Keep fallback defaults above.
307
+ }
308
+ }
309
+
310
+ base.project = {
311
+ ...(base.project ?? {}),
312
+ name: projectName,
313
+ displayName: projectName
314
+ };
315
+ base.modules = [];
316
+ base.conventions = {
317
+ ...(base.conventions ?? {}),
318
+ qaRoot: 'qa'
319
+ };
320
+
321
+ return base;
322
+ }
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/validate.js — Validate qa/ folder structure and conventions
4
+ *
5
+ * Usage:
6
+ * qa-framework validate
7
+ * qa-framework validate --strict
8
+ * qa-framework validate --config ./my-config.json
9
+ *
10
+ * Checks:
11
+ * 1. Required top-level folders exist
12
+ * 2. All submodule folders contain the 6 required spec files
13
+ * 3. No plaintext credentials in spec files (basic scan)
14
+ * 4. Test case naming convention (TC-NNN pattern in spec files)
15
+ * 5. [--strict] Automation spec files exist for every submodule
16
+ * 6. [--strict] EXEC_IDX pattern present in all spec files
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ // --- Args ---
25
+ const args = process.argv.slice(2);
26
+ const strict = args.includes('--strict');
27
+ const configFlag = args.indexOf('--config');
28
+ const configPath = configFlag !== -1
29
+ ? path.resolve(process.cwd(), args[configFlag + 1])
30
+ : path.resolve(process.cwd(), 'qa-framework.config.json');
31
+
32
+ // --- Load config (optional) ---
33
+ let qaRoot = path.resolve(process.cwd(), 'qa');
34
+ if (fs.existsSync(configPath)) {
35
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
36
+ qaRoot = path.resolve(process.cwd(), config.conventions?.qaRoot ?? 'qa');
37
+ }
38
+
39
+ console.log(`[qa-framework/validate] Validating: ${qaRoot}`);
40
+ if (strict) console.log('[qa-framework/validate] Mode: STRICT');
41
+
42
+ const errors = [];
43
+ const warnings = [];
44
+
45
+ // -----------------------------------------------------------------------
46
+ // Check 1: Required top-level folders
47
+ // -----------------------------------------------------------------------
48
+ const requiredFolders = [
49
+ '00-standards',
50
+ '05-test-plans',
51
+ '06-defects',
52
+ '07-automation',
53
+ ];
54
+
55
+ for (const folder of requiredFolders) {
56
+ if (!fs.existsSync(path.join(qaRoot, folder))) {
57
+ errors.push(`Missing required folder: qa/${folder}/`);
58
+ }
59
+ }
60
+
61
+ // -----------------------------------------------------------------------
62
+ // Check 2: Submodule spec completeness
63
+ // -----------------------------------------------------------------------
64
+ const SPEC_FILES = [
65
+ '00-inventory.md',
66
+ '01-business-rules.md',
67
+ '02-workflows.md',
68
+ '03-roles-permissions.md',
69
+ '04-test-data.md',
70
+ '05-test-scenarios.md',
71
+ ];
72
+
73
+ const SKIP_DIRS = new Set([
74
+ '00-guides', '00-standards', '05-test-plans',
75
+ '06-defects', '07-automation', '08-azure-integration',
76
+ ]);
77
+
78
+ if (fs.existsSync(qaRoot)) {
79
+ const topDirs = fs.readdirSync(qaRoot).filter(d => {
80
+ return fs.statSync(path.join(qaRoot, d)).isDirectory() && !SKIP_DIRS.has(d);
81
+ });
82
+
83
+ for (const moduleDir of topDirs) {
84
+ const modulePath = path.join(qaRoot, moduleDir);
85
+ const subDirs = fs.readdirSync(modulePath).filter(d =>
86
+ fs.statSync(path.join(modulePath, d)).isDirectory()
87
+ );
88
+
89
+ for (const subDir of subDirs) {
90
+ const subPath = path.join(modulePath, subDir);
91
+ for (const specFile of SPEC_FILES) {
92
+ if (!fs.existsSync(path.join(subPath, specFile))) {
93
+ errors.push(`Missing spec file: qa/${moduleDir}/${subDir}/${specFile}`);
94
+ }
95
+ }
96
+
97
+ // Strict: automation spec must exist
98
+ if (strict) {
99
+ const specTs = path.join(qaRoot, '07-automation', 'e2e', moduleDir, `${subDir}.spec.ts`);
100
+ if (!fs.existsSync(specTs)) {
101
+ warnings.push(`[STRICT] No automation spec found: qa/07-automation/e2e/${moduleDir}/${subDir}.spec.ts`);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ // -----------------------------------------------------------------------
109
+ // Check 3: Credential scan (basic)
110
+ // -----------------------------------------------------------------------
111
+ const CREDENTIAL_PATTERNS = [
112
+ /password\s*=\s*['"][^'"]{4,}['"]/i,
113
+ /token\s*=\s*['"][a-zA-Z0-9+/]{20,}['"]/i,
114
+ /secret\s*=\s*['"][^'"]{4,}['"]/i,
115
+ ];
116
+
117
+ function scanFileForCredentials(filePath) {
118
+ try {
119
+ const content = fs.readFileSync(filePath, 'utf8');
120
+ for (const pattern of CREDENTIAL_PATTERNS) {
121
+ if (pattern.test(content)) {
122
+ errors.push(`Potential credential in: ${path.relative(process.cwd(), filePath)}`);
123
+ break;
124
+ }
125
+ }
126
+ } catch {}
127
+ }
128
+
129
+ const specFiles = walkFiles(path.join(qaRoot, '07-automation'), '.spec.ts');
130
+ for (const f of specFiles) {
131
+ scanFileForCredentials(f);
132
+
133
+ // Check 4: TC naming pattern in spec files
134
+ const content = fs.readFileSync(f, 'utf8');
135
+ if (!content.includes('[TC-') && !content.includes('EXEC_IDX')) {
136
+ warnings.push(`No TC-ID naming convention found in: ${path.relative(process.cwd(), f)}`);
137
+ }
138
+
139
+ // Check 6 (strict): EXEC_IDX
140
+ if (strict && !content.includes('EXEC_IDX')) {
141
+ warnings.push(`[STRICT] EXEC_IDX pattern missing in: ${path.relative(process.cwd(), f)}`);
142
+ }
143
+ }
144
+
145
+ // -----------------------------------------------------------------------
146
+ // Report
147
+ // -----------------------------------------------------------------------
148
+ console.log('');
149
+ if (errors.length === 0 && warnings.length === 0) {
150
+ console.log('✅ Validation passed — no issues found.');
151
+ process.exit(0);
152
+ }
153
+
154
+ if (warnings.length > 0) {
155
+ console.log(`⚠️ ${warnings.length} warning(s):`);
156
+ for (const w of warnings) console.log(` WARN ${w}`);
157
+ console.log('');
158
+ }
159
+
160
+ if (errors.length > 0) {
161
+ console.log(`❌ ${errors.length} error(s):`);
162
+ for (const e of errors) console.log(` ERROR ${e}`);
163
+ console.log('');
164
+ process.exit(1);
165
+ } else {
166
+ process.exit(0);
167
+ }
168
+
169
+ // -----------------------------------------------------------------------
170
+ // Helper
171
+ // -----------------------------------------------------------------------
172
+ function walkFiles(dir, ext) {
173
+ if (!fs.existsSync(dir)) return [];
174
+ const results = [];
175
+ for (const entry of fs.readdirSync(dir)) {
176
+ const full = path.join(dir, entry);
177
+ if (fs.statSync(full).isDirectory()) {
178
+ results.push(...walkFiles(full, ext));
179
+ } else if (full.endsWith(ext)) {
180
+ results.push(full);
181
+ }
182
+ }
183
+ return results;
184
+ }
@@ -0,0 +1,56 @@
1
+ # -----------------------------------------------------------------------
2
+ # keber/qa-framework — Environment Variables Reference
3
+ # -----------------------------------------------------------------------
4
+ # Copy this file to .env and fill in real values.
5
+ # NEVER commit .env to source control.
6
+ # -----------------------------------------------------------------------
7
+
8
+ # -----------------------------------------------------------------------
9
+ # Application Under Test
10
+ # -----------------------------------------------------------------------
11
+ QA_BASE_URL=https://your-app.qa.example.com
12
+
13
+ # -----------------------------------------------------------------------
14
+ # Login Configuration
15
+ # These selectors allow the framework to work with any login form.
16
+ # -----------------------------------------------------------------------
17
+ QA_LOGIN_PATH=/login
18
+ QA_LOGIN_EMAIL_SELECTOR=input[type="email"]
19
+ QA_LOGIN_PASSWORD_SELECTOR=input[type="password"]
20
+ QA_LOGIN_SUBMIT_SELECTOR=button[type="submit"]
21
+ # Selector that appears AFTER successful login (used by global-setup to confirm):
22
+ QA_LOGIN_SUCCESS_SELECTOR=.dashboard
23
+
24
+ # -----------------------------------------------------------------------
25
+ # Test Users
26
+ # Add one block per role your project uses.
27
+ # -----------------------------------------------------------------------
28
+
29
+ # Default / Standard user
30
+ QA_USER_EMAIL=qa-user@example.com
31
+ QA_USER_PASSWORD=CHANGE_ME
32
+
33
+ # Admin user (uncomment if multi-role setup is needed)
34
+ # QA_ADMIN_EMAIL=qa-admin@example.com
35
+ # QA_ADMIN_PASSWORD=CHANGE_ME
36
+
37
+ # Read-only / viewer user
38
+ # QA_READONLY_EMAIL=qa-readonly@example.com
39
+ # QA_READONLY_PASSWORD=CHANGE_ME
40
+
41
+ # If your app uses RUT-based login instead of email:
42
+ # QA_USER_RUT=12345678-9
43
+
44
+ # -----------------------------------------------------------------------
45
+ # Azure DevOps Integration (optional — leave blank to disable)
46
+ # -----------------------------------------------------------------------
47
+ # ADO_ORG_URL=https://dev.azure.com/your-org
48
+ # ADO_PROJECT_NAME=YourProject
49
+ # ADO_PLAN_ID=00000
50
+ # AZURE_TOKEN=your-pat-token-here # Personal Access Token — NEVER commit
51
+
52
+ # -----------------------------------------------------------------------
53
+ # CI / Pipeline overrides
54
+ # -----------------------------------------------------------------------
55
+ # CI=true # Set automatically by most CI systems
56
+ # ADO_SYNC_DISABLED=false # Set to true to skip ADO reporter in local runs
@@ -0,0 +1,77 @@
1
+ /**
2
+ * fixtures/auth.ts
3
+ *
4
+ * Provides a `loginAs(page, role)` helper that navigates the login form
5
+ * and waits for successful authentication.
6
+ *
7
+ * Usage in spec files:
8
+ * import { loginAs } from '../../fixtures/auth';
9
+ * // inside a test:
10
+ * await loginAs(page, 'admin');
11
+ *
12
+ * Alternatively, use storageState via global-setup.ts (recommended for
13
+ * performance — avoids repeating the login flow on every test).
14
+ * Use loginAs() only in tests that need to validate the login flow itself
15
+ * or switch users mid-test.
16
+ *
17
+ * Supported roles are driven by env vars — see .env.example.
18
+ */
19
+
20
+ import { Page } from '@playwright/test';
21
+
22
+ export type QARole = 'default' | 'admin' | 'readonly' | string;
23
+
24
+ interface LoginConfig {
25
+ email: string;
26
+ password: string;
27
+ }
28
+
29
+ /** Map role name → credentials from environment variables */
30
+ function getCredentials(role: QARole): LoginConfig {
31
+ switch (role) {
32
+ case 'admin':
33
+ return {
34
+ email: process.env.QA_ADMIN_EMAIL ?? '',
35
+ password: process.env.QA_ADMIN_PASSWORD ?? '',
36
+ };
37
+ case 'readonly':
38
+ return {
39
+ email: process.env.QA_READONLY_EMAIL ?? '',
40
+ password: process.env.QA_READONLY_PASSWORD ?? '',
41
+ };
42
+ case 'default':
43
+ default:
44
+ return {
45
+ email: process.env.QA_USER_EMAIL ?? '',
46
+ password: process.env.QA_USER_PASSWORD ?? '',
47
+ };
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Navigate to the login page and authenticate as the given role.
53
+ * Waits for the post-login success selector before resolving.
54
+ */
55
+ export async function loginAs(page: Page, role: QARole = 'default'): Promise<void> {
56
+ const { email, password } = getCredentials(role);
57
+
58
+ if (!email || !password) {
59
+ throw new Error(
60
+ `[qa-framework/auth] Missing credentials for role "${role}". ` +
61
+ `Check your .env file — expected ${role.toUpperCase()}_EMAIL and ${role.toUpperCase()}_PASSWORD.`
62
+ );
63
+ }
64
+
65
+ const loginPath = process.env.QA_LOGIN_PATH ?? '/login';
66
+ const emailSelector = process.env.QA_LOGIN_EMAIL_SELECTOR ?? 'input[type="email"]';
67
+ const passwordSelector = process.env.QA_LOGIN_PASSWORD_SELECTOR ?? 'input[type="password"]';
68
+ const submitSelector = process.env.QA_LOGIN_SUBMIT_SELECTOR ?? 'button[type="submit"]';
69
+ const successSelector = process.env.QA_LOGIN_SUCCESS_SELECTOR ?? '.dashboard';
70
+ const baseURL = process.env.QA_BASE_URL ?? '';
71
+
72
+ await page.goto(`${baseURL}${loginPath}`, { waitUntil: 'domcontentloaded' });
73
+ await page.locator(emailSelector).fill(email);
74
+ await page.locator(passwordSelector).fill(password);
75
+ await page.locator(submitSelector).click();
76
+ await page.waitForSelector(successSelector, { timeout: 15_000 });
77
+ }