@keber/qa-framework 1.10.0 → 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/CHANGELOG.md +63 -18
- 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 +106 -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/pipelines/azure-pipeline-qa.yml +92 -25
- package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
- package/integrations/playwright/README.md +128 -4
- package/package.json +9 -6
- package/qa-framework.config.json +24 -3
- 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 +108 -45
- 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 +461 -30
- 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 +52 -3
- package/templates/automation-scaffold/fixtures/auth.ts +27 -5
- package/templates/automation-scaffold/fixtures/base.ts +130 -0
- package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
- package/templates/automation-scaffold/global-setup.ts +196 -37
- package/templates/automation-scaffold/playwright.config.ts +74 -10
- 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 +18 -4
- package/templates/execution-report.md +9 -9
- package/templates/integration-scaffold/.env.example +40 -0
- package/templates/integration-scaffold/README.md +77 -0
- package/templates/integration-scaffold/global-setup.ts +119 -0
- package/templates/integration-scaffold/package.json +15 -0
- package/templates/integration-scaffold/playwright.config.ts +92 -0
- package/templates/integration-scaffold/tests/example.spec.ts +43 -0
- package/templates/memory/ci-pipeline-findings.md +36 -0
- package/templates/memory/data-volatility-strategies.md +70 -0
- package/templates/memory/e2e-stabilization-patterns.md +54 -0
- package/templates/qa-framework.instructions.md +16 -7
- package/templates/qa-framework.rules.md +53 -0
- package/templates/qa-readme.md +26 -5
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Integration
|
|
1
|
+
# Integration - Playwright
|
|
2
2
|
|
|
3
3
|
This directory documents how `@playwright/test` is configured within the
|
|
4
4
|
`keber/qa-framework` opinionated setup.
|
|
@@ -40,9 +40,9 @@ suite execution.
|
|
|
40
40
|
|
|
41
41
|
## Debugging checklist
|
|
42
42
|
|
|
43
|
-
1. `PWDEBUG=1 npx playwright test`
|
|
44
|
-
2. `--headed`
|
|
45
|
-
3. `--trace on`
|
|
43
|
+
1. `PWDEBUG=1 npx playwright test` - step through in inspector
|
|
44
|
+
2. `--headed` - watch the browser
|
|
45
|
+
3. `--trace on` - record full trace; open with `npx playwright show-trace`
|
|
46
46
|
4. Increase `actionTimeout` if the app has slow server-side rendering
|
|
47
47
|
5. Add `await page.waitForLoadState('networkidle')` before assertions on
|
|
48
48
|
dynamically loaded content
|
|
@@ -66,3 +66,127 @@ test.describe('[TC-M-S-NNN] Module > Submodule @P0', () => {
|
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
68
|
```
|
|
69
|
+
|
|
70
|
+
## Visual AI helper (optional advanced pattern)
|
|
71
|
+
|
|
72
|
+
> **Advanced / optional.** This pattern is not part of the default scaffold and requires
|
|
73
|
+
> an AI vision API key. Use it when a selector-based assertion is too brittle or the
|
|
74
|
+
> element structure changes frequently (e.g. PDF previews, dashboard charts, export outputs).
|
|
75
|
+
|
|
76
|
+
Instead of asserting specific selectors, take a screenshot and ask an AI vision model to
|
|
77
|
+
verify that the expected content is visually present. This decouples the assertion from DOM
|
|
78
|
+
structure and is useful for generated documents or complex layouts.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// fixtures/vision-helper.ts
|
|
82
|
+
import * as https from 'https';
|
|
83
|
+
|
|
84
|
+
export interface VisionResult {
|
|
85
|
+
passes: boolean;
|
|
86
|
+
reason: string;
|
|
87
|
+
raw?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sends a screenshot to an AI vision model and asks whether the described
|
|
92
|
+
* content is visually present. Degrades gracefully when the API key is absent.
|
|
93
|
+
*
|
|
94
|
+
* Requires an API key in the environment (e.g. OPENAI_API_KEY for GPT-4o,
|
|
95
|
+
* or adapt the request body for any other vision-capable model).
|
|
96
|
+
*/
|
|
97
|
+
export async function assertScreenshotShowsContent(
|
|
98
|
+
screenshotBuffer: Buffer,
|
|
99
|
+
contentDescription: string,
|
|
100
|
+
options: { model?: string; degradeOnMissingKey?: boolean } = {}
|
|
101
|
+
): Promise<VisionResult> {
|
|
102
|
+
const apiKey = process.env.AI_VISION_API_KEY;
|
|
103
|
+
const degrade = options.degradeOnMissingKey ?? true;
|
|
104
|
+
|
|
105
|
+
if (!apiKey) {
|
|
106
|
+
if (degrade) {
|
|
107
|
+
console.warn('[vision-helper] AI_VISION_API_KEY not set. Skipping visual assertion.');
|
|
108
|
+
return { passes: true, reason: 'SKIPPED: AI_VISION_API_KEY not configured' };
|
|
109
|
+
}
|
|
110
|
+
return { passes: false, reason: 'AI_VISION_API_KEY not set in environment' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const base64Image = screenshotBuffer.toString('base64');
|
|
114
|
+
const model = options.model ?? 'gpt-4o';
|
|
115
|
+
|
|
116
|
+
const prompt = `You are a QA assistant verifying screenshots of a web application.
|
|
117
|
+
Answer YES or NO: Does the screenshot clearly show ${contentDescription}?
|
|
118
|
+
Reply in exactly this format:
|
|
119
|
+
VERDICT: YES
|
|
120
|
+
REASON: <one short sentence>`;
|
|
121
|
+
|
|
122
|
+
const body = JSON.stringify({
|
|
123
|
+
model,
|
|
124
|
+
max_tokens: 150,
|
|
125
|
+
messages: [{
|
|
126
|
+
role: 'user',
|
|
127
|
+
content: [
|
|
128
|
+
{ type: 'text', text: prompt },
|
|
129
|
+
{ type: 'image_url', image_url: { url: `data:image/png;base64,${base64Image}`, detail: 'low' } },
|
|
130
|
+
],
|
|
131
|
+
}],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const raw = await new Promise<string>((resolve, reject) => {
|
|
135
|
+
const req = https.request({
|
|
136
|
+
hostname: 'api.openai.com',
|
|
137
|
+
path: '/v1/chat/completions',
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: {
|
|
140
|
+
'Content-Type': 'application/json',
|
|
141
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
142
|
+
'Content-Length': Buffer.byteLength(body),
|
|
143
|
+
},
|
|
144
|
+
}, (res) => {
|
|
145
|
+
let data = '';
|
|
146
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
147
|
+
res.on('end', () => resolve(data));
|
|
148
|
+
});
|
|
149
|
+
req.on('error', reject);
|
|
150
|
+
req.write(body);
|
|
151
|
+
req.end();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const parsed = JSON.parse(raw);
|
|
155
|
+
const text: string = parsed.choices?.[0]?.message?.content ?? '';
|
|
156
|
+
const verdict = text.match(/VERDICT:\s*(YES|NO)/i);
|
|
157
|
+
const reason = text.match(/REASON:\s*(.+)/i);
|
|
158
|
+
|
|
159
|
+
if (!verdict) return { passes: false, reason: `Unexpected response: ${text}`, raw: text };
|
|
160
|
+
return {
|
|
161
|
+
passes: verdict[1].toUpperCase() === 'YES',
|
|
162
|
+
reason: reason?.[1]?.trim() ?? text,
|
|
163
|
+
raw: text,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Usage in a test:**
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { assertScreenshotShowsContent } from '../fixtures/vision-helper';
|
|
172
|
+
|
|
173
|
+
test('[TC-MOD-001] Export PDF contains expected content', async ({ page }) => {
|
|
174
|
+
// ... trigger export ...
|
|
175
|
+
const screenshot = await page.screenshot();
|
|
176
|
+
const result = await assertScreenshotShowsContent(
|
|
177
|
+
screenshot,
|
|
178
|
+
'a completed export confirmation message with a download link'
|
|
179
|
+
);
|
|
180
|
+
expect(result.passes, result.reason).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Setup notes:**
|
|
185
|
+
|
|
186
|
+
- Copy `vision-helper.ts` into your `fixtures/` directory.
|
|
187
|
+
- Set `AI_VISION_API_KEY` in your `.env` file (not committed). The example uses the OpenAI
|
|
188
|
+
API; adapt the request body for any other vision model.
|
|
189
|
+
- By default, the helper degrades to a skip (passes: true) when the key is absent, so CI
|
|
190
|
+
runs without the key do not fail on this assertion.
|
|
191
|
+
- `pdf-parse` (v2+) exports a `PDFParse` class rather than a function. Use
|
|
192
|
+
`new PDFParse({ data: buffer })` and call `.getText()` on the instance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keber/qa-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "Reusable spec-driven QA framework for IDE-agent-assisted automated testing. Installable as an npm package. Provides structure, templates, agent instructions, and optional integrations for Playwright and Azure DevOps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qa",
|
|
@@ -21,7 +21,11 @@
|
|
|
21
21
|
"postinstall": "node scripts/init.js --skip-if-exists",
|
|
22
22
|
"init": "node scripts/cli.js init",
|
|
23
23
|
"generate": "node scripts/cli.js generate",
|
|
24
|
-
"validate": "node scripts/cli.js validate"
|
|
24
|
+
"validate": "node scripts/cli.js validate",
|
|
25
|
+
"check-accents": "node scripts/check-spanish-accents.js",
|
|
26
|
+
"check-chars": "node scripts/check-forbidden-chars.js",
|
|
27
|
+
"test": "node --test test/*.test.js",
|
|
28
|
+
"version": "node scripts/sync-version.js && git add README.md .github/copilot-instructions.md"
|
|
25
29
|
},
|
|
26
30
|
"files": [
|
|
27
31
|
"agent-instructions/",
|
|
@@ -36,12 +40,11 @@
|
|
|
36
40
|
"qa-framework.config.json"
|
|
37
41
|
],
|
|
38
42
|
"engines": {
|
|
39
|
-
"node": ">=
|
|
43
|
+
"node": ">=20"
|
|
40
44
|
},
|
|
41
|
-
"devDependencies": {},
|
|
42
45
|
"peerDependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
46
|
+
"@keber/ado-qa": ">=1.0.0",
|
|
47
|
+
"@playwright/test": ">=1.40.0"
|
|
45
48
|
},
|
|
46
49
|
"peerDependenciesMeta": {
|
|
47
50
|
"@playwright/test": {
|
package/qa-framework.config.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./qa-framework.config.schema.json",
|
|
3
|
-
"frameworkVersion": "1.
|
|
3
|
+
"frameworkVersion": "1.10.0",
|
|
4
4
|
|
|
5
5
|
"project": {
|
|
6
6
|
"name": "{{PROJECT_NAME}}",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"testCaseIdPattern": "TC-{MODULE}-{SUBMODULE}-{NUM}",
|
|
28
28
|
"businessRuleIdPattern": "RN-{MODULE}-{NUM}",
|
|
29
29
|
"workflowIdPattern": "FL-{MODULE}-{NUM}",
|
|
30
|
-
"defectIdPattern": "DEF-{NUM}",
|
|
30
|
+
"defectIdPattern": "DEF-{PREFIX}-{NUM}",
|
|
31
31
|
"testNamingPattern": "[{ID}] {title} @{priority}",
|
|
32
32
|
"timestampFormat": "YYYY-MM-DD_HH-MM-SS",
|
|
33
33
|
"tcPriorityLevels": ["P0", "P1", "P2", "P3"]
|
|
@@ -42,6 +42,20 @@
|
|
|
42
42
|
}
|
|
43
43
|
],
|
|
44
44
|
|
|
45
|
+
"parallelLanes": {
|
|
46
|
+
"enabled": false,
|
|
47
|
+
"lanes": [
|
|
48
|
+
{
|
|
49
|
+
"id": "1",
|
|
50
|
+
"account": "QA_USER",
|
|
51
|
+
"storageState": ".auth/user-default.json",
|
|
52
|
+
"mcpNamespace": null,
|
|
53
|
+
"runnerProject": "chromium",
|
|
54
|
+
"capabilities": []
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
|
|
45
59
|
"integrations": {
|
|
46
60
|
"playwright": {
|
|
47
61
|
"enabled": false,
|
|
@@ -69,7 +83,14 @@
|
|
|
69
83
|
"variableGroup": "{{ADO_VARIABLE_GROUP}}",
|
|
70
84
|
"pipelineFile": "qa/08-azure-integration/pipelines/azure-pipeline-qa.yml",
|
|
71
85
|
"moduleRegistry": "qa/08-azure-integration/module-registry.json",
|
|
72
|
-
"reporterPackage": "@alex_neo/playwright-azure-reporter"
|
|
86
|
+
"reporterPackage": "@alex_neo/playwright-azure-reporter",
|
|
87
|
+
"sprintCycle": {
|
|
88
|
+
"enabled": false,
|
|
89
|
+
"sprintDurationDays": 8,
|
|
90
|
+
"manualTestingTimeboxDays": 2,
|
|
91
|
+
"dateFormat": "dd-mm-aaaa",
|
|
92
|
+
"timezone": "America/Santiago"
|
|
93
|
+
}
|
|
73
94
|
}
|
|
74
95
|
},
|
|
75
96
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-forbidden-chars.js
|
|
4
|
+
*
|
|
5
|
+
* Enforces the BLOCKING character-safety rule declared in
|
|
6
|
+
* .github/copilot-instructions.md: generated content must never carry typographic
|
|
7
|
+
* characters that survive a copy-paste but break on the way to a terminal, a CSV,
|
|
8
|
+
* a PowerShell script or an Azure DevOps field.
|
|
9
|
+
*
|
|
10
|
+
* Forbidden: em-dash (U+2014), en-dash (U+2013), horizontal ellipsis (U+2026),
|
|
11
|
+
* smart quotes (U+201C, U+201D, U+2018, U+2019) and arrows (U+2190-U+21FF).
|
|
12
|
+
* Each has an ASCII equivalent that renders identically everywhere.
|
|
13
|
+
*
|
|
14
|
+
* Latin Extended (U+00C0-U+024F) is explicitly NOT flagged: Spanish accents,
|
|
15
|
+
* n with tilde and u/o with umlaut are required orthography, not decoration.
|
|
16
|
+
* Emoji are not flagged either - they are outside the forbidden set.
|
|
17
|
+
*
|
|
18
|
+
* Unlike check-spanish-accents.js this scans .js and .ts as well as .md, because
|
|
19
|
+
* the generators under scripts/ embed these characters in template literals and
|
|
20
|
+
* console output that propagate into every consuming project.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node scripts/check-forbidden-chars.js <file|dir> [...] check paths
|
|
24
|
+
* node scripts/check-forbidden-chars.js --staged check staged files
|
|
25
|
+
* node scripts/check-forbidden-chars.js --report <paths> print findings, never fail
|
|
26
|
+
*
|
|
27
|
+
* Exit codes: 0 = clean, 1 = at least one forbidden character, 2 = usage error.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const { execFileSync } = require('child_process');
|
|
35
|
+
|
|
36
|
+
// '.example' covers .env.example, which ships to consuming projects like any other
|
|
37
|
+
// template. A BOM or an em-dash there is not cosmetic: some .env parsers read the
|
|
38
|
+
// first key as QA_BASE_URL and silently fail to find it.
|
|
39
|
+
const EXTENSIONS = ['.md', '.js', '.ts', '.ps1', '.example'];
|
|
40
|
+
|
|
41
|
+
// Code point -> { name, replacement }. Arrows are handled by range below.
|
|
42
|
+
const FORBIDDEN = {
|
|
43
|
+
0x2014: { name: 'em dash', replacement: ' - ' },
|
|
44
|
+
0x2013: { name: 'en dash', replacement: ' - ' },
|
|
45
|
+
0x2026: { name: 'horizontal ellipsis', replacement: '...' },
|
|
46
|
+
0x201c: { name: 'left double quotation mark', replacement: '"' },
|
|
47
|
+
0x201d: { name: 'right double quotation mark', replacement: '"' },
|
|
48
|
+
0x2018: { name: 'left single quotation mark', replacement: "'" },
|
|
49
|
+
0x2019: { name: 'right single quotation mark', replacement: "'" },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ARROW_START = 0x2190;
|
|
53
|
+
const ARROW_END = 0x21ff;
|
|
54
|
+
|
|
55
|
+
// The two arrows with an obvious ASCII spelling; every other arrow in the block
|
|
56
|
+
// has no single equivalent, so the report asks for a rewrite instead of guessing.
|
|
57
|
+
const ARROW_REPLACEMENTS = {
|
|
58
|
+
0x2190: '<-',
|
|
59
|
+
0x2192: '->',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function describe(codePoint) {
|
|
63
|
+
if (FORBIDDEN[codePoint]) return FORBIDDEN[codePoint];
|
|
64
|
+
if (codePoint >= ARROW_START && codePoint <= ARROW_END) {
|
|
65
|
+
return {
|
|
66
|
+
name: 'arrow',
|
|
67
|
+
replacement: ARROW_REPLACEMENTS[codePoint] || 'an ASCII arrow such as -> or <-',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hex(codePoint) {
|
|
74
|
+
return 'U+' + codePoint.toString(16).toUpperCase().padStart(4, '0');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Scan one file's text. Returns an array of findings; column is 1-based and
|
|
79
|
+
* counted in code points, so an astral character earlier on the line does not
|
|
80
|
+
* shift the reported position.
|
|
81
|
+
*/
|
|
82
|
+
function scanText(text, file) {
|
|
83
|
+
const findings = [];
|
|
84
|
+
const lines = text.split(/\r?\n/);
|
|
85
|
+
|
|
86
|
+
lines.forEach((line, lineIndex) => {
|
|
87
|
+
let column = 0;
|
|
88
|
+
for (const char of line) {
|
|
89
|
+
column += 1;
|
|
90
|
+
const codePoint = char.codePointAt(0);
|
|
91
|
+
const match = describe(codePoint);
|
|
92
|
+
if (!match) continue;
|
|
93
|
+
findings.push({
|
|
94
|
+
file,
|
|
95
|
+
line: lineIndex + 1,
|
|
96
|
+
column,
|
|
97
|
+
char,
|
|
98
|
+
codePoint,
|
|
99
|
+
name: match.name,
|
|
100
|
+
replacement: match.replacement,
|
|
101
|
+
text: line.trim(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return findings;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function scanFile(file) {
|
|
110
|
+
return scanText(fs.readFileSync(file, 'utf8'), file);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Archived copies of content this framework shipped in an earlier release. Their bytes
|
|
114
|
+
// are the point: test/upgrade.test.js matches them against SHIPPED_SCAFFOLD_HASHES to
|
|
115
|
+
// prove a real legacy installation is recognised as pristine. Rewriting a character in
|
|
116
|
+
// one of them changes its hash and silently voids that proof, so they are read-only
|
|
117
|
+
// history rather than authored content and the rule does not apply to them.
|
|
118
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'legacy-v1.11.3']);
|
|
119
|
+
|
|
120
|
+
function collect(target) {
|
|
121
|
+
const stat = fs.statSync(target);
|
|
122
|
+
if (stat.isFile()) return EXTENSIONS.includes(path.extname(target)) ? [target] : [];
|
|
123
|
+
return fs.readdirSync(target).flatMap((entry) => {
|
|
124
|
+
if (SKIP_DIRS.has(entry)) return [];
|
|
125
|
+
return collect(path.join(target, entry));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stagedFiles() {
|
|
130
|
+
const out = execFileSync(
|
|
131
|
+
'git',
|
|
132
|
+
['diff', '--cached', '--name-only', '--diff-filter=ACM'],
|
|
133
|
+
{ encoding: 'utf8' }
|
|
134
|
+
);
|
|
135
|
+
return out
|
|
136
|
+
.split('\n')
|
|
137
|
+
.map((s) => s.trim())
|
|
138
|
+
.filter((s) => s && EXTENSIONS.includes(path.extname(s)) && fs.existsSync(s));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function main(argv) {
|
|
142
|
+
const reportOnly = argv.includes('--report');
|
|
143
|
+
const args = argv.filter((a) => a !== '--report');
|
|
144
|
+
|
|
145
|
+
let files;
|
|
146
|
+
if (args.includes('--staged')) {
|
|
147
|
+
files = stagedFiles();
|
|
148
|
+
} else if (args.length) {
|
|
149
|
+
files = args.flatMap(collect);
|
|
150
|
+
} else {
|
|
151
|
+
console.error('usage: check-forbidden-chars.js <file|dir>... | --staged [--report]');
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const findings = files.flatMap(scanFile);
|
|
156
|
+
|
|
157
|
+
if (findings.length) {
|
|
158
|
+
const stream = reportOnly ? console.log : console.error;
|
|
159
|
+
stream(`\nForbidden characters (${findings.length} occurrence(s)):\n`);
|
|
160
|
+
for (const f of findings) {
|
|
161
|
+
stream(` ${f.file}:${f.line}:${f.column} "${f.char}" ${hex(f.codePoint)} ${f.name}`);
|
|
162
|
+
stream(` use ${f.replacement}`);
|
|
163
|
+
stream(` ${f.text}`);
|
|
164
|
+
}
|
|
165
|
+
if (!reportOnly) {
|
|
166
|
+
stream(
|
|
167
|
+
'\nThese characters break on the way to a terminal, a CSV export or an Azure' +
|
|
168
|
+
'\nDevOps field. Replace them with their ASCII equivalents rather than' +
|
|
169
|
+
'\nbypassing this check. Spanish accents are permitted and are never flagged.\n'
|
|
170
|
+
);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (reportOnly) {
|
|
177
|
+
console.log(`check-forbidden-chars: ${files.length} file(s) checked, no findings.`);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(`check-forbidden-chars: ${files.length} file(s) checked, no violations.`);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
|
186
|
+
|
|
187
|
+
module.exports = { scanText, scanFile, main, EXTENSIONS, FORBIDDEN, ARROW_START, ARROW_END };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-spanish-accents.js
|
|
4
|
+
*
|
|
5
|
+
* Fails when a Markdown file whose prose is Spanish carries no accented characters.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: a spec written as "Codigo" while the UI renders "Código" produces a
|
|
8
|
+
* page-object selector that never matches. In one project that cost 24 of 39 failing smoke
|
|
9
|
+
* tests and two wrong mitigations (raising a timeout, forcing HTTP/1.1) before the real
|
|
10
|
+
* cause was found, because the failure presents as a timeout, not as a text mismatch.
|
|
11
|
+
* Accent stripping is a defect, not a house style - and its measured incidence in
|
|
12
|
+
* agent-generated artifacts is high enough that honor-system compliance does not hold.
|
|
13
|
+
*
|
|
14
|
+
* The hard part is not detecting missing accents; it is deciding which files are Spanish.
|
|
15
|
+
* A naive "every .md must contain accents" rule fails every legitimately English artifact
|
|
16
|
+
* the framework ships. Counting Spanish marker words in raw text does not separate them
|
|
17
|
+
* either: an English document about a Spanish-language project quotes enough Spanish paths,
|
|
18
|
+
* identifiers and snippets to cross any absolute threshold that still catches short
|
|
19
|
+
* Spanish files.
|
|
20
|
+
*
|
|
21
|
+
* So this classifies on the density of Spanish function words in PROSE ONLY, after
|
|
22
|
+
* stripping fenced code, inline code, link targets, HTML tags and table pipes. English
|
|
23
|
+
* prose that merely cites Spanish identifiers scores low because those citations live in
|
|
24
|
+
* the stripped regions; real Spanish prose scores high because its function words are
|
|
25
|
+
* spread through the sentences themselves.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node scripts/check-spanish-accents.js <file|dir> [...] check paths
|
|
29
|
+
* node scripts/check-spanish-accents.js --staged check staged .md files
|
|
30
|
+
* node scripts/check-spanish-accents.js --report <paths> print scores, never fail
|
|
31
|
+
*
|
|
32
|
+
* Exit codes: 0 = clean, 1 = at least one Spanish file has no accents, 2 = usage error.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
'use strict';
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const { execFileSync } = require('child_process');
|
|
40
|
+
|
|
41
|
+
// Function words that are unavoidable in real Spanish prose and rare in English text.
|
|
42
|
+
// Deliberately excludes "no", "a", "en", "es" and other tokens that collide with English
|
|
43
|
+
// or with code, and excludes accented forms so a stripped file is not penalised twice.
|
|
44
|
+
const MARKERS = [
|
|
45
|
+
'que', 'para', 'con', 'por', 'los', 'las', 'del', 'una', 'como', 'pero',
|
|
46
|
+
'este', 'esta', 'estos', 'estas', 'sin', 'sobre', 'desde', 'cuando', 'donde',
|
|
47
|
+
'cada', 'todo', 'todos', 'toda', 'todas', 'entre', 'hasta', 'porque', 'segun',
|
|
48
|
+
'debe', 'deben', 'ser', 'son', 'hay', 'muy', 'ya', 'si', 'lo', 'le', 'se',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const ACCENTED = /[áéíóúÁÉÍÓÚñÑüÜ]/;
|
|
52
|
+
|
|
53
|
+
// A file is Spanish when at least this share of its prose words are Spanish markers.
|
|
54
|
+
// Calibrated against this package's own corpus: Spanish agent templates land near 0.13,
|
|
55
|
+
// English skill files and the FRAMEWORK-FIXES reports stay below 0.03.
|
|
56
|
+
const DENSITY_THRESHOLD = 0.07;
|
|
57
|
+
// Below this many prose words the density estimate is too noisy to act on.
|
|
58
|
+
const MIN_PROSE_WORDS = 60;
|
|
59
|
+
|
|
60
|
+
/** Remove every region where Spanish tokens are citations rather than prose. */
|
|
61
|
+
function extractProse(markdown) {
|
|
62
|
+
return markdown
|
|
63
|
+
.replace(/^---\n[\s\S]*?\n---/, ' ') // YAML frontmatter
|
|
64
|
+
.replace(/```[\s\S]*?```/g, ' ') // fenced code
|
|
65
|
+
.replace(/~~~[\s\S]*?~~~/g, ' ') // fenced code, alt syntax
|
|
66
|
+
.replace(/`[^`\n]*`/g, ' ') // inline code
|
|
67
|
+
.replace(/\]\([^)]*\)/g, '] ') // link targets, keep link text
|
|
68
|
+
.replace(/<[^>\n]*>/g, ' ') // HTML tags and <placeholders>
|
|
69
|
+
.replace(/\{\{[^}]*\}\}/g, ' ') // {{TEMPLATE_PLACEHOLDERS}}
|
|
70
|
+
.replace(/\|/g, ' ') // table pipes
|
|
71
|
+
.replace(/^\s{4,}\S.*$/gm, ' ') // indented code blocks
|
|
72
|
+
.replace(/[A-Za-z0-9_.\-]+\/[A-Za-z0-9_./\-]+/g, ' ') // bare paths
|
|
73
|
+
.replace(/[#*_>[\]]/g, ' '); // remaining markdown punctuation
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function score(markdown) {
|
|
77
|
+
const prose = extractProse(markdown);
|
|
78
|
+
const words = prose.toLowerCase().match(/[a-zà-ÿ]+/g) || [];
|
|
79
|
+
const markers = words.filter((w) => MARKERS.includes(w)).length;
|
|
80
|
+
const density = words.length ? markers / words.length : 0;
|
|
81
|
+
return {
|
|
82
|
+
words: words.length,
|
|
83
|
+
markers,
|
|
84
|
+
density,
|
|
85
|
+
isSpanish: words.length >= MIN_PROSE_WORDS && density >= DENSITY_THRESHOLD,
|
|
86
|
+
hasAccents: ACCENTED.test(markdown),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function collect(target) {
|
|
91
|
+
const stat = fs.statSync(target);
|
|
92
|
+
if (stat.isFile()) return target.endsWith('.md') ? [target] : [];
|
|
93
|
+
return fs.readdirSync(target).flatMap((entry) => {
|
|
94
|
+
if (entry === 'node_modules' || entry === '.git') return [];
|
|
95
|
+
return collect(path.join(target, entry));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function stagedMarkdown() {
|
|
100
|
+
const out = execFileSync(
|
|
101
|
+
'git',
|
|
102
|
+
['diff', '--cached', '--name-only', '--diff-filter=ACM', '--', '*.md'],
|
|
103
|
+
{ encoding: 'utf8' }
|
|
104
|
+
);
|
|
105
|
+
return out.split('\n').map((s) => s.trim()).filter((s) => s && fs.existsSync(s));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function main(argv) {
|
|
109
|
+
const reportOnly = argv.includes('--report');
|
|
110
|
+
const args = argv.filter((a) => a !== '--report');
|
|
111
|
+
|
|
112
|
+
let files;
|
|
113
|
+
if (args.includes('--staged')) {
|
|
114
|
+
files = stagedMarkdown();
|
|
115
|
+
} else if (args.length) {
|
|
116
|
+
files = args.flatMap(collect);
|
|
117
|
+
} else {
|
|
118
|
+
console.error('usage: check-spanish-accents.js <file|dir>... | --staged [--report]');
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const failures = [];
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
const result = score(fs.readFileSync(file, 'utf8'));
|
|
125
|
+
if (reportOnly) {
|
|
126
|
+
console.log(
|
|
127
|
+
`${result.isSpanish ? 'ES' : 'en'} density=${result.density.toFixed(3)} ` +
|
|
128
|
+
`words=${String(result.words).padStart(5)} accents=${result.hasAccents ? 'yes' : 'NO '} ${file}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (result.isSpanish && !result.hasAccents) failures.push({ file, ...result });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (reportOnly) return 0;
|
|
135
|
+
|
|
136
|
+
if (failures.length) {
|
|
137
|
+
console.error('\nSpanish Markdown without accented characters:\n');
|
|
138
|
+
for (const f of failures) {
|
|
139
|
+
console.error(` ${f.file}`);
|
|
140
|
+
console.error(
|
|
141
|
+
` ${f.markers} Spanish markers in ${f.words} prose words ` +
|
|
142
|
+
`(density ${f.density.toFixed(3)}), 0 accented characters.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
console.error(
|
|
146
|
+
'\nSpanish prose must carry its accents: a spec saying "Codigo" does not match a UI' +
|
|
147
|
+
'\nthat renders "Código", and the resulting selector failure looks like a timeout.' +
|
|
148
|
+
'\nFix the orthography rather than bypassing this check.\n'
|
|
149
|
+
);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(`check-spanish-accents: ${files.length} file(s) checked, no violations.`);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
|
158
|
+
|
|
159
|
+
module.exports = { score, extractProse, MARKERS, DENSITY_THRESHOLD };
|
package/scripts/cli.js
CHANGED
package/scripts/generate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* scripts/generate.js
|
|
3
|
+
* scripts/generate.js - Generate a qa artifact from a template
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* qa-framework generate <artifact> [options]
|
|
@@ -39,7 +39,7 @@ const outputDir = getArg('--output') ? path.resolve(process.cwd(), getArg
|
|
|
39
39
|
const templateDir = path.resolve(__dirname, '..', 'templates');
|
|
40
40
|
|
|
41
41
|
const ARTIFACTS = {
|
|
42
|
-
spec: null, // special
|
|
42
|
+
spec: null, // special - copies all 6 files
|
|
43
43
|
'test-plan': path.join(templateDir, 'test-plan.md'),
|
|
44
44
|
'test-case': path.join(templateDir, 'test-case.md'),
|
|
45
45
|
'execution-report': path.join(templateDir, 'execution-report.md'),
|