diff --git a/src/contractReport.js b/src/contractReport.js index 818c0fa..d4f2104 100644 --- a/src/contractReport.js +++ b/src/contractReport.js @@ -33,7 +33,11 @@ export function printContractReport(output) { bySource.get(key).push(result); } + // Passing mocks and skips are counted, not listed: run.json and index.html hold the detail. for (const [source, sourceResults] of bySource) { + const noisy = sourceResults.filter((r) => !r.validation.valid || r.validation.warnings.length > 0); + if (noisy.length === 0) continue; + const mode = sourceResults[0]?.mode || 'warn'; const modeLabel = mode === 'error' ? bgRed(` ${mode.toUpperCase()} `) @@ -41,7 +45,7 @@ export function printContractReport(output) { console.log(`${cyan('Source:')} ${source} ${modeLabel}`); console.log(''); - for (const result of sourceResults) { + for (const result of noisy) { const failColor = result.mode === 'error' ? boldRed : boldYellow; const detailColor = result.mode === 'error' ? red : yellow; @@ -55,8 +59,6 @@ export function printContractReport(output) { if (result.mode === 'error') { hasContractErrors = true; } - } else if (result.validation.warnings.length === 0) { - console.log(green(` MOCK ✓ ${result.method} ${result.matchedPath} (${result.status}) — ${formatMockLabel(result)}`)); } for (const warning of result.validation.warnings) { @@ -68,15 +70,6 @@ export function printContractReport(output) { } } - if (skipped.length > 0) { - console.log(dim('Skipped:')); - for (const skip of skipped) { - console.log(dim(` MOCK ℹ "${skip.alias}" — ${skip.url}`)); - console.log(dim(` ${skip.reason === 'urlRegex mock' ? 'Regex URL pattern' : 'No matching path in any spec'}`)); - } - console.log(''); - } - const validatedCount = results.length; if (errorCount === 0 && warningCount === 0) { diff --git a/tests/contractReport.test.js b/tests/contractReport.test.js index 1d98ed4..29cb4ef 100644 --- a/tests/contractReport.test.js +++ b/tests/contractReport.test.js @@ -37,6 +37,38 @@ describe('printContractReport', () => { const logs = stripAnsi(consoleSpy.mock.calls.map(c => c[0]).join('\n')); expect(logs).toContain('All mocks match'); expect(logs).toContain('Mocks validated: 1'); + expect(logs).not.toContain('✓'); + expect(logs).not.toContain('Source:'); + }); + + it('prints a source header only for sources with errors or warnings', () => { + const pass = (specSource) => ({ + alias: 'getPets', method: 'GET', status: 200, specSource, matchedPath: '/v1/pets', mode: 'warn', + validation: { valid: true, errors: [], warnings: [] }, + }); + const output = { + results: [ + pass('./clean.json'), + pass('./noisy.json'), + { + alias: 'createPet', method: 'POST', status: 201, specSource: './noisy.json', matchedPath: '/v1/pets', mode: 'warn', + validation: { + valid: false, + errors: [{ path: 'response.id', message: 'expected integer, got string', keyword: 'type' }], + warnings: [], + }, + }, + ], + skipped: [], + }; + + printContractReport(output); + + const logs = stripAnsi(consoleSpy.mock.calls.map(c => c[0]).join('\n')); + expect(logs).toContain('Source: ./noisy.json'); + expect(logs).not.toContain('./clean.json'); + expect(logs).not.toContain('GET /v1/pets'); + expect(logs).toContain('Mocks validated: 3'); }); it('prints errors with ✗ symbol', () => { @@ -98,7 +130,7 @@ describe('printContractReport', () => { expect(logs).toContain('Warnings: 1'); }); - it('prints skipped mocks with ℹ symbol', () => { + it('counts skipped mocks without listing them', () => { const output = { results: [], skipped: [ @@ -109,8 +141,9 @@ describe('printContractReport', () => { printContractReport(output); const logs = stripAnsi(consoleSpy.mock.calls.map(c => c[0]).join('\n')); - expect(logs).toContain('ℹ'); - expect(logs).toContain('adyenSetup'); + expect(logs).not.toContain('ℹ'); + expect(logs).not.toContain('adyenSetup'); + expect(logs).not.toContain('Skipped:\n'); expect(logs).toContain('Skipped: 1'); }); @@ -198,7 +231,7 @@ describe('printContractReport', () => { mode: 'warn', testName: 'Cart > should load items', occurrence: 1, - validation: { valid: true, errors: [], warnings: [] }, + validation: { valid: true, errors: [], warnings: [{ type: 'UNMATCHED_STATUS', message: 'Status 200 not documented' }] }, }, ], skipped: [], @@ -264,7 +297,7 @@ describe('printContractReport', () => { const lines = consoleSpy.mock.calls.map((c) => stripAnsi(c[0])); const glyphLines = lines.filter((l) => /^\s*(MOCK\s+)?[✓✗⚠ℹ]/.test(l)); - expect(glyphLines.length).toBeGreaterThanOrEqual(4); + expect(glyphLines.length).toBe(2); for (const line of glyphLines) { expect(line).toMatch(/^\s*MOCK [✓✗⚠ℹ]/); } @@ -283,7 +316,7 @@ describe('printContractReport', () => { mode: 'warn', testName: 'Cart > should load items', occurrence: 2, - validation: { valid: true, errors: [], warnings: [] }, + validation: { valid: true, errors: [], warnings: [{ type: 'UNMATCHED_STATUS', message: 'Status 200 not documented' }] }, }, ], skipped: [],