@activate-core/test-forge 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ai-generator.js +32 -25
- package/dist/core/harness.js +8 -51
- package/dist/core/report.js +1 -1
- package/dist/core/run-generate.js +12 -21
- package/dist/core/suite-planner.js +11 -3
- package/package.json +1 -1
- package/reporters/_shared.cjs +45 -1
- package/reporters/jest-reporter.cjs +5 -3
- package/reporters/playwright-reporter.cjs +4 -3
|
@@ -83,21 +83,9 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
83
83
|
: 'TypeScript';
|
|
84
84
|
return [
|
|
85
85
|
`You are a senior QA engineer. Generate a complete ${framework}`,
|
|
86
|
-
`for the user journey described below
|
|
86
|
+
`for the user journey described below. (The specific file to write is`,
|
|
87
|
+
`stated at the very end, after the journey — read all the context first.)`,
|
|
87
88
|
``,
|
|
88
|
-
...(focus
|
|
89
|
-
? [
|
|
90
|
-
`SCOPE — this is ONE file in a larger suite for this journey.`,
|
|
91
|
-
`Cover ONLY this concern in this file:`,
|
|
92
|
-
` ${focus.title}`,
|
|
93
|
-
` ${focus.covers}`,
|
|
94
|
-
`Do NOT test the journey's other concerns here — separate files`,
|
|
95
|
-
`cover those. Go DEEP on this one concern (happy path + its own`,
|
|
96
|
-
`edge/negative/security cases) rather than broad-but-shallow.`,
|
|
97
|
-
`Use this exact describe() title: "${focus.title}".`,
|
|
98
|
-
``,
|
|
99
|
-
]
|
|
100
|
-
: []),
|
|
101
89
|
`Think like an experienced tester whose goal is to break the`,
|
|
102
90
|
`application. Cover, where relevant: happy path, missing/empty`,
|
|
103
91
|
`required fields, invalid formats and boundary values (very long,`,
|
|
@@ -241,17 +229,6 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
241
229
|
`module (aliased as apiRequest above).`,
|
|
242
230
|
].join('\n'),
|
|
243
231
|
``,
|
|
244
|
-
...(testPath
|
|
245
|
-
? [
|
|
246
|
-
`This test file will be saved at EXACTLY this path:`,
|
|
247
|
-
` ${testPath}`,
|
|
248
|
-
`Compute every require()/import path (to app source, helpers, the`,
|
|
249
|
-
`seed file, etc.) RELATIVE TO THAT EXACT location — count the`,
|
|
250
|
-
`directory depth from that path, do not assume a conventional`,
|
|
251
|
-
`depth. Getting the relative depth wrong breaks the whole file.`,
|
|
252
|
-
``,
|
|
253
|
-
]
|
|
254
|
-
: []),
|
|
255
232
|
...(transport
|
|
256
233
|
? [transport, `Do NOT import any library that is not already a dependency of this project.`, ``]
|
|
257
234
|
: []),
|
|
@@ -298,6 +275,36 @@ export function buildPrompt(journey, map, testPath, focus, harnessPath, transpor
|
|
|
298
275
|
`Data section. Use ALL of it. Respect any "# TODO:" notes.`,
|
|
299
276
|
``,
|
|
300
277
|
journey.rawMarkdown,
|
|
278
|
+
// ---- PER-FILE TASK (kept LAST so the big context above stays a stable,
|
|
279
|
+
// cache-reusable prefix across every file in this journey, and so the
|
|
280
|
+
// concrete task is the most recent thing the model reads) ----
|
|
281
|
+
``,
|
|
282
|
+
...(focus
|
|
283
|
+
? [
|
|
284
|
+
`--- THIS FILE (the one to write now) ---`,
|
|
285
|
+
`This is ONE file in a larger suite for this journey.`,
|
|
286
|
+
`Cover ONLY this concern here:`,
|
|
287
|
+
` ${focus.title}`,
|
|
288
|
+
` ${focus.covers}`,
|
|
289
|
+
`Do NOT test the journey's other concerns here — separate files`,
|
|
290
|
+
`cover those. Go DEEP on this one concern (happy path + its own`,
|
|
291
|
+
`edge/negative/security cases) rather than broad-but-shallow.`,
|
|
292
|
+
`Use this exact describe() title: "${focus.title}".`,
|
|
293
|
+
]
|
|
294
|
+
: []),
|
|
295
|
+
...(testPath
|
|
296
|
+
? [
|
|
297
|
+
``,
|
|
298
|
+
`Save this file at EXACTLY: ${testPath}`,
|
|
299
|
+
`Compute every require()/import path (app source, helpers, the seed`,
|
|
300
|
+
`file, etc.) RELATIVE TO THAT EXACT location — count the directory`,
|
|
301
|
+
`depth from that path; do not assume a conventional depth. Getting`,
|
|
302
|
+
`the relative depth wrong breaks the whole file.`,
|
|
303
|
+
]
|
|
304
|
+
: []),
|
|
305
|
+
``,
|
|
306
|
+
`Now output ONLY the raw ${isJs ? 'JavaScript' : 'TypeScript'} test code`,
|
|
307
|
+
`for THIS file — the first character must be the first line of code.`,
|
|
301
308
|
].join('\n');
|
|
302
309
|
}
|
|
303
310
|
/**
|
package/dist/core/harness.js
CHANGED
|
@@ -285,10 +285,10 @@ export function playwrightConfig(testDirRelToPackage, globalSetupRelToPackage, c
|
|
|
285
285
|
` testMatch: /.*\\.(test|spec)\\.[jt]s$/,`,
|
|
286
286
|
` globalSetup: require.resolve('${globalSetupRelToPackage}'),`,
|
|
287
287
|
` timeout: 60000,`,
|
|
288
|
-
` // Console line output
|
|
289
|
-
` //
|
|
290
|
-
` //
|
|
291
|
-
` reporter: [['line'], [
|
|
288
|
+
` // Console line output. The last reporter auto-publishes the run to the`,
|
|
289
|
+
` // TestForge dashboard (needs TESTFORGE_KEY in env + project/dashboardUrl`,
|
|
290
|
+
` // in testforge.config.json).`,
|
|
291
|
+
` reporter: [['line'], [require.resolve('@activate-core/test-forge/reporters/playwright-reporter.cjs'), { category: ${JSON.stringify(category)} }]],`,
|
|
292
292
|
` use: {`,
|
|
293
293
|
` baseURL: process.env.BASE_URL || 'http://localhost:3000',`,
|
|
294
294
|
` trace: 'on-first-retry',`,
|
|
@@ -300,8 +300,8 @@ export function playwrightConfig(testDirRelToPackage, globalSetupRelToPackage, c
|
|
|
300
300
|
}
|
|
301
301
|
/**
|
|
302
302
|
* Jest config for a backend (jest) area — self-contained so `npm test` / a bare
|
|
303
|
-
* `npx jest` runs with no shell setup
|
|
304
|
-
* - testEnvironment '
|
|
303
|
+
* `npx jest` runs with no shell setup.
|
|
304
|
+
* - testEnvironment 'node': the default Node test environment
|
|
305
305
|
* - setupFiles: loads .env.testing + NODE_ENV + the reset opt-in before any
|
|
306
306
|
* test file imports the app
|
|
307
307
|
* - serial (maxWorkers 1): suites share one test DB and reset between tests
|
|
@@ -309,9 +309,9 @@ export function playwrightConfig(testDirRelToPackage, globalSetupRelToPackage, c
|
|
|
309
309
|
export function jestConfig(testDirRelToPackage, setupRelToPackage, category = 'api') {
|
|
310
310
|
return [
|
|
311
311
|
`// @generated by testforge`,
|
|
312
|
-
`// Install once: npm i -D jest
|
|
312
|
+
`// Install once: npm i -D jest`,
|
|
313
313
|
`module.exports = {`,
|
|
314
|
-
` testEnvironment: '
|
|
314
|
+
` testEnvironment: 'node',`,
|
|
315
315
|
` setupFiles: ['<rootDir>/${setupRelToPackage}'],`,
|
|
316
316
|
` testMatch: ['<rootDir>/${testDirRelToPackage}/**/*.test.js'],`,
|
|
317
317
|
` // Serial: the suites share ONE test DB and reset it between tests.`,
|
|
@@ -350,49 +350,6 @@ export function jestSetupContent() {
|
|
|
350
350
|
``,
|
|
351
351
|
].join('\n');
|
|
352
352
|
}
|
|
353
|
-
/** Allure failure categories — buckets that show up in the report's "Categories"
|
|
354
|
-
* tab. Copied into allure-results/ so the report groups failures meaningfully. */
|
|
355
|
-
export function allureCategories() {
|
|
356
|
-
return JSON.stringify([
|
|
357
|
-
{ name: 'Product defect', matchedStatuses: ['failed'], messageRegex: '.*expect.*|.*assert.*' },
|
|
358
|
-
{ name: 'Test / harness issue', matchedStatuses: ['broken'] },
|
|
359
|
-
{ name: 'Flaky', matchedStatuses: ['failed', 'broken'], flaky: true },
|
|
360
|
-
], null, 2);
|
|
361
|
-
}
|
|
362
|
-
/** ALLURE.md — how to install the reporter deps and view the report. */
|
|
363
|
-
export function allureDoc() {
|
|
364
|
-
return [
|
|
365
|
-
`# Allure reporting (generated by testforge)`,
|
|
366
|
-
``,
|
|
367
|
-
`testforge wires the suites to emit **Allure** results. To view the report:`,
|
|
368
|
-
``,
|
|
369
|
-
`## 1. Install once (per package)`,
|
|
370
|
-
'```bash',
|
|
371
|
-
`# web (Playwright): npm i -D allure-playwright allure-commandline`,
|
|
372
|
-
`# api (Jest): npm i -D allure-jest allure-commandline`,
|
|
373
|
-
'```',
|
|
374
|
-
``,
|
|
375
|
-
`## 2. Run the tests`,
|
|
376
|
-
`Running the suite writes raw results into \`allure-results/\`.`,
|
|
377
|
-
``,
|
|
378
|
-
`## 3. Generate + open the report`,
|
|
379
|
-
'```bash',
|
|
380
|
-
`npx allure generate allure-results --clean -o allure-report`,
|
|
381
|
-
`npx allure open allure-report`,
|
|
382
|
-
'```',
|
|
383
|
-
`Or add scripts to package.json:`,
|
|
384
|
-
'```json',
|
|
385
|
-
`"report": "allure generate allure-results --clean -o allure-report && allure open allure-report"`,
|
|
386
|
-
'```',
|
|
387
|
-
``,
|
|
388
|
-
`## Categories`,
|
|
389
|
-
`\`allure-categories.json\` groups failures (Product defect / Test-harness / Flaky).`,
|
|
390
|
-
`Copy it to \`allure-results/categories.json\` before generating to activate it.`,
|
|
391
|
-
``,
|
|
392
|
-
`> \`allure-results/\`, \`allure-report/\` are build output — add them to .gitignore.`,
|
|
393
|
-
``,
|
|
394
|
-
].join('\n');
|
|
395
|
-
}
|
|
396
353
|
/**
|
|
397
354
|
* Playwright globalSetup file: runs the testkit's one-call setup() once
|
|
398
355
|
* before the suite (connect + seed + mint/populate tokens). Sits next to
|
package/dist/core/report.js
CHANGED
|
@@ -145,7 +145,7 @@ for (const f of ['jest.config.js','jest.config.cjs','jest.config.json']) {
|
|
|
145
145
|
if (!Object.keys(base).length && fs.existsSync(path.join(__dirname,'package.json'))) base = require('./package.json').jest || {};
|
|
146
146
|
module.exports = {
|
|
147
147
|
...base,
|
|
148
|
-
testEnvironment: 'node', //
|
|
148
|
+
testEnvironment: 'node', // plain node env for a count-only run
|
|
149
149
|
setupFilesAfterEnv: [...(base.setupFilesAfterEnv || []), path.resolve(__dirname, '.tf-collect-patch.cjs')],
|
|
150
150
|
};
|
|
151
151
|
`;
|
|
@@ -27,7 +27,7 @@ import { generateSeed } from './seed-generator.js';
|
|
|
27
27
|
import { planSuite } from './suite-planner.js';
|
|
28
28
|
import { isSyntaxValid, checkTestGates, analyzeFile } from './validate.js';
|
|
29
29
|
import { installedModules, transportHint, projectSelectors } from './detect.js';
|
|
30
|
-
import { harnessInfo, generateHarnessAsync, generateHarness, playwrightConfig, globalSetupContent, fixturesDoc, jestConfig, jestSetupContent,
|
|
30
|
+
import { harnessInfo, generateHarnessAsync, generateHarness, playwrightConfig, globalSetupContent, fixturesDoc, jestConfig, jestSetupContent, } from './harness.js';
|
|
31
31
|
import { hashContent, loadCache, saveCache } from './cache.js';
|
|
32
32
|
import { findMarkdownFiles } from './find-files.js';
|
|
33
33
|
import { loadConfig, testDirForType, resolveFramework } from './config.js';
|
|
@@ -356,7 +356,7 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
356
356
|
writeFileSync(info.path, kit, 'utf-8');
|
|
357
357
|
writeFileSync(join(info.dir, 'FIXTURES.md'), fixturesDoc(), 'utf-8');
|
|
358
358
|
// Config + globalSetup (playwright) / config + setup (jest) so the specs run
|
|
359
|
-
// AND get their tokens minted once, plus
|
|
359
|
+
// AND get their tokens minted once, plus the runner config scaffolding.
|
|
360
360
|
writeHarnessConfigs(framework, areaDir, info.dir, area);
|
|
361
361
|
harnessByArea.set(area, info.path);
|
|
362
362
|
return info.path;
|
|
@@ -376,16 +376,15 @@ export async function runGenerate(docsDir, useAI = true, force = false, only, ar
|
|
|
376
376
|
// --journey <x>: generate just the one matching journey.
|
|
377
377
|
if (only && !matchesJourney(file, journey.id, only))
|
|
378
378
|
continue;
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
// (testDir/<journey>-<slug>). Otherwise they'd silently not run.
|
|
379
|
+
// EVERY area gets its own per-journey FOLDER of focused files
|
|
380
|
+
// (testDir/<journey>/<slug>), for both Playwright and Jest. The folder
|
|
381
|
+
// namespaces the files, so the on-disk name is just the clean <slug>
|
|
382
|
+
// (no journey prefix) — which is also what the dashboard shows as the
|
|
383
|
+
// suite name. testforge's generated jest.config uses a recursive
|
|
384
|
+
// `tests/**/*.test.js` glob, so foldered jest files are still discovered.
|
|
386
385
|
const areaDir = testDirForType(config, journey.area);
|
|
387
386
|
const framework = resolveFramework(config, journey.area, journey.type);
|
|
388
|
-
const flat =
|
|
387
|
+
const flat = false; // foldered layout for all runners (clean, categorised)
|
|
389
388
|
const testDir = flat ? areaDir : join(areaDir, journey.id);
|
|
390
389
|
const seedPath = join(seedDir, `${journey.id}${seedExt}`);
|
|
391
390
|
// Dependency-aware guardrails (F1): what this area may import, and how
|
|
@@ -631,23 +630,15 @@ function writeCoverageManifest(results) {
|
|
|
631
630
|
}
|
|
632
631
|
}
|
|
633
632
|
/**
|
|
634
|
-
* Write the per-area runner config
|
|
635
|
-
* - playwright: global-setup.js + playwright.config.js
|
|
636
|
-
* - jest: helpers/jest.setup.js + jest.config.js
|
|
637
|
-
* - both: ALLURE.md + allure-categories.json at the package root
|
|
633
|
+
* Write the per-area runner config.
|
|
634
|
+
* - playwright: global-setup.js + playwright.config.js
|
|
635
|
+
* - jest: helpers/jest.setup.js + jest.config.js
|
|
638
636
|
* Config files are written only if absent, so a project's hand-tuned config is
|
|
639
637
|
* never clobbered. `harnessDir` is the area's helpers dir (where setup lives).
|
|
640
638
|
*/
|
|
641
639
|
function writeHarnessConfigs(framework, areaDir, harnessDir, area) {
|
|
642
640
|
const pkgRoot = dirname(areaDir);
|
|
643
641
|
const rel = areaDir.slice(pkgRoot.length + 1) || '.';
|
|
644
|
-
// Allure docs + categories (both frameworks); never overwrite.
|
|
645
|
-
const allureMd = join(pkgRoot, 'ALLURE.md');
|
|
646
|
-
if (!existsSync(allureMd))
|
|
647
|
-
writeFileSync(allureMd, allureDoc(), 'utf-8');
|
|
648
|
-
const cats = join(pkgRoot, 'allure-categories.json');
|
|
649
|
-
if (!existsSync(cats))
|
|
650
|
-
writeFileSync(cats, allureCategories(), 'utf-8');
|
|
651
642
|
if (framework === 'playwright') {
|
|
652
643
|
const gsPath = join(harnessDir, 'global-setup.js');
|
|
653
644
|
writeFileSync(gsPath, globalSetupContent(), 'utf-8');
|
|
@@ -44,7 +44,13 @@ function buildPlanPrompt(journey, map) {
|
|
|
44
44
|
`Output ONLY a JSON array (no prose, no markdown fences) of objects:`,
|
|
45
45
|
` [{ "slug": "<short-kebab-name>", "title": "<describe title>",`,
|
|
46
46
|
` "covers": "<one or two sentences: exactly what this file tests>" }]`,
|
|
47
|
-
`slug: lowercase
|
|
47
|
+
`slug: lowercase kebab-case, no path, no extension — and SHORT: name the`,
|
|
48
|
+
` one concern in 2–4 words (4 max), the way a human names a file. Do NOT`,
|
|
49
|
+
` restate the journey name and do NOT pile on qualifiers.`,
|
|
50
|
+
` Good: "amount-validation", "3ds-challenge", "zero-amount", "rate-limit".`,
|
|
51
|
+
` Bad: "charge-amount-sanity-negative-nonnumeric-oversized",`,
|
|
52
|
+
` "otp-session-otp-send-rate-limit".`,
|
|
53
|
+
`title: human-readable and specific, e.g. "Payment — Amount validation".`,
|
|
48
54
|
`The very first character of your response must be "[".`,
|
|
49
55
|
``,
|
|
50
56
|
`--- PROJECT MAP (data layer) ---`,
|
|
@@ -71,14 +77,16 @@ function extractJsonArray(text) {
|
|
|
71
77
|
return [];
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
|
-
/** Normalise a raw slug into a safe kebab-case file stem. */
|
|
80
|
+
/** Normalise a raw slug into a safe, SHORT kebab-case file stem. */
|
|
75
81
|
function safeSlug(raw) {
|
|
76
|
-
|
|
82
|
+
const kebab = String(raw ?? '')
|
|
77
83
|
.trim()
|
|
78
84
|
.toLowerCase()
|
|
79
85
|
.replace(/\.(spec|test)\.[jt]sx?$/, '') // drop any extension the AI added
|
|
80
86
|
.replace(/[^a-z0-9]+/g, '-')
|
|
81
87
|
.replace(/^-|-$/g, '');
|
|
88
|
+
// Hard cap: keep names short & human — at most 4 words, whatever the AI returns.
|
|
89
|
+
return kebab.split('-').filter(Boolean).slice(0, 4).join('-');
|
|
82
90
|
}
|
|
83
91
|
/**
|
|
84
92
|
* Plan the suite for one journey: returns the list of focused files to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@activate-core/test-forge",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "AI-powered QA automation. Scans a project, drafts journey docs, and generates seeded Jest/Playwright tests. Works in any project, with any AI CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/reporters/_shared.cjs
CHANGED
|
@@ -42,6 +42,50 @@ function loadConfig(root) {
|
|
|
42
42
|
|
|
43
43
|
const strip = (s) => (s || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
44
44
|
|
|
45
|
+
// jest's raw matcher line (`expect(received).toBeGreaterThanOrEqual(expected)`) reads
|
|
46
|
+
// backwards to a human. Translate the matcher into a plain, direction-correct sentence
|
|
47
|
+
// so an engineer sees intent, not jest internals.
|
|
48
|
+
const MATCHER = {
|
|
49
|
+
toBe: 'equal', toEqual: 'equal', toStrictEqual: 'equal',
|
|
50
|
+
toBeGreaterThanOrEqual: 'be at least', toBeGreaterThan: 'be greater than',
|
|
51
|
+
toBeLessThanOrEqual: 'be at most', toBeLessThan: 'be less than',
|
|
52
|
+
toContain: 'contain', toContainEqual: 'contain', toMatch: 'match',
|
|
53
|
+
toHaveLength: 'have length', toHaveProperty: 'have property', toBeCloseTo: 'be about',
|
|
54
|
+
};
|
|
55
|
+
function humanize(body) {
|
|
56
|
+
const m = body.match(/\.(\w+)\((?:expected|n|value|substring|sample|element)?\)/) || body.match(/expect\([^)]*\)\.(\w+)/);
|
|
57
|
+
const phrase = m && MATCHER[m[1]];
|
|
58
|
+
if (!phrase) return null;
|
|
59
|
+
const exp = (body.match(/Expected:?\s*(.+)/) || [])[1];
|
|
60
|
+
const rec = (body.match(/Received:?\s*(.+)/) || [])[1];
|
|
61
|
+
const ce = (exp || '').replace(/^[><=!~\s]+/, '').trim();
|
|
62
|
+
const cr = (rec || '').trim();
|
|
63
|
+
if (!ce || !cr) return null;
|
|
64
|
+
return `The check expected the result to ${phrase} ${ce}, but it was ${cr}.`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A readable failure detail for an engineer: a plain-language lead (what the check
|
|
68
|
+
// wanted vs what it got) + the Expected/Received values + any custom message. Drops
|
|
69
|
+
// jest's cryptic matcher line and the noisy stack frames / internal paths. Capped so
|
|
70
|
+
// it stays a summary, not a full dump.
|
|
71
|
+
function cleanError(raw) {
|
|
72
|
+
const s = strip(raw || '');
|
|
73
|
+
if (!s) return '';
|
|
74
|
+
const kept = s.split('\n').filter((l) => {
|
|
75
|
+
const t = l.trim();
|
|
76
|
+
if (!t) return true; // keep blank lines for readability (collapsed below)
|
|
77
|
+
if (/^at\s/.test(t)) return false; // stack frame
|
|
78
|
+
if (/node_modules|node:internal|\/dist\//.test(t)) return false; // library/internal noise
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
const body = kept.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
82
|
+
const lead = humanize(body);
|
|
83
|
+
if (!lead) return body.slice(0, 700);
|
|
84
|
+
// The plain lead replaces jest's `expect(received)…` matcher line — drop it, keep the rest.
|
|
85
|
+
const rest = body.split('\n').filter((l) => !/^(Error:\s*)?expect\(/.test(l.trim())).join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
86
|
+
return (lead + (rest ? '\n\n' + rest : '')).slice(0, 700);
|
|
87
|
+
}
|
|
88
|
+
|
|
45
89
|
function sh(cmd, cwd) {
|
|
46
90
|
try { return (execSync(cmd, { cwd, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()) || undefined; }
|
|
47
91
|
catch { return undefined; }
|
|
@@ -106,4 +150,4 @@ async function publish(category, suites, durationSec) {
|
|
|
106
150
|
}
|
|
107
151
|
}
|
|
108
152
|
|
|
109
|
-
module.exports = { strip, sig, publish, gitInfo, resolveEnv };
|
|
153
|
+
module.exports = { strip, cleanError, sig, publish, gitInfo, resolveEnv };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// TestForge Jest reporter — auto-publishes the run to the dashboard on completion.
|
|
2
2
|
// Add to jest.config.js:
|
|
3
3
|
// reporters: ['default', ['<abs>/reporters/jest-reporter.cjs', { category: 'api' }]]
|
|
4
|
-
const {
|
|
4
|
+
const { cleanError, sig, publish } = require('./_shared.cjs');
|
|
5
5
|
|
|
6
6
|
module.exports = class TestForgeJestReporter {
|
|
7
7
|
constructor(_globalConfig, options) {
|
|
@@ -15,12 +15,14 @@ module.exports = class TestForgeJestReporter {
|
|
|
15
15
|
for (const a of (f.testResults || f.assertionResults || [])) {
|
|
16
16
|
const status = a.status === 'passed' ? 'passed' : a.status === 'failed' ? 'failed' : 'skipped';
|
|
17
17
|
const raw = (a.failureMessages || [])[0];
|
|
18
|
-
const err = strip(raw || '').split('\n').find((l) => l && l.trim()) || '';
|
|
19
18
|
su.tests.push({
|
|
20
19
|
name: a.fullName || a.title,
|
|
21
20
|
status,
|
|
22
|
-
error: status === 'failed' ?
|
|
21
|
+
error: status === 'failed' ? cleanError(raw) : undefined,
|
|
23
22
|
sig: status === 'failed' ? sig(raw) : undefined,
|
|
23
|
+
note: status === 'skipped'
|
|
24
|
+
? (a.status === 'todo' ? 'Marked to-do — this scenario isn’t implemented yet' : 'Skipped — not run in this execution')
|
|
25
|
+
: undefined,
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// TestForge Playwright reporter — auto-publishes the run to the dashboard on completion.
|
|
2
2
|
// Add to playwright.config.js reporter array:
|
|
3
3
|
// ['<abs>/reporters/playwright-reporter.cjs', { category: 'web' }]
|
|
4
|
-
const {
|
|
4
|
+
const { cleanError, sig, publish } = require('./_shared.cjs');
|
|
5
5
|
|
|
6
6
|
module.exports = class TestForgePlaywrightReporter {
|
|
7
7
|
constructor(options) {
|
|
@@ -15,12 +15,13 @@ module.exports = class TestForgePlaywrightReporter {
|
|
|
15
15
|
const su = this.suites[name] || (this.suites[name] = { name, category: this.category, tests: [] });
|
|
16
16
|
const status = result.status === 'passed' ? 'passed' : result.status === 'skipped' ? 'skipped' : 'failed';
|
|
17
17
|
const raw = result.error && result.error.message;
|
|
18
|
-
const
|
|
18
|
+
const skipAnno = (test.annotations || []).find((x) => x.type === 'skip' || x.type === 'fixme');
|
|
19
19
|
su.tests.push({
|
|
20
20
|
name: test.title,
|
|
21
21
|
status,
|
|
22
|
-
error: status === 'failed' ?
|
|
22
|
+
error: status === 'failed' ? cleanError(raw) : undefined,
|
|
23
23
|
sig: status === 'failed' ? sig(raw) : undefined,
|
|
24
|
+
note: status === 'skipped' ? ((skipAnno && skipAnno.description) || 'Skipped — not run in this execution') : undefined,
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
async onEnd(result) {
|