diff --git a/packages/core/src/lib/implementation/persist.unit.test.ts b/packages/core/src/lib/implementation/persist.unit.test.ts index e62ae19ae..f458f0bdc 100644 --- a/packages/core/src/lib/implementation/persist.unit.test.ts +++ b/packages/core/src/lib/implementation/persist.unit.test.ts @@ -71,9 +71,11 @@ describe('persistReport', () => { 'utf8', ); expect(mdReport).toContain('Code PushUp Report'); - expect(mdReport).toMatch( - /\|\s*🏷 Category\s*\|\s*⭐ Score\s*\|\s*🛡 Audits\s*\|/, - ); + expect(mdReport).toContainMarkdownTableRow([ + '🏷 Category', + '⭐ Score', + '🛡 Audits', + ]); const jsonReport: Report = JSON.parse( await readFile(path.join(MEMFS_VOLUME, 'report.json'), 'utf8'), diff --git a/packages/core/vite.config.unit.ts b/packages/core/vite.config.unit.ts index 729480d93..31e0a6262 100644 --- a/packages/core/vite.config.unit.ts +++ b/packages/core/vite.config.unit.ts @@ -29,6 +29,7 @@ export default defineConfig({ '../../testing/test-setup/src/lib/reset.mocks.ts', '../../testing/test-setup/src/lib/portal-client.mock.ts', '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', + '../../testing/test-setup/src/lib/extend/markdown-table.matcher.ts', ], }, }); diff --git a/packages/utils/src/lib/reports/generate-md-report.unit.test.ts b/packages/utils/src/lib/reports/generate-md-report.unit.test.ts index 7b7d3f7e9..bc870837f 100644 --- a/packages/utils/src/lib/reports/generate-md-report.unit.test.ts +++ b/packages/utils/src/lib/reports/generate-md-report.unit.test.ts @@ -176,7 +176,7 @@ describe('auditDetailsIssues', () => { severity: 'warning', } as Issue, ])?.toString(), - ).toMatch(/\|\s*4-7\s*\|/); + ).toContainMarkdownTableRow(['⚠️ _warning_', '', '`index.js`', '4-7']); }); }); @@ -286,8 +286,8 @@ describe('auditDetails', () => { } as AuditReport).toString(); expect(md).toMatch('
'); expect(md).toMatch('#### Elements'); - expect(md).toMatch(/\|\s*button\s*\|/); - expect(md).toMatch(/\|\s*div\s*\|/); + expect(md).toContainMarkdownTableRow(['button']); + expect(md).toContainMarkdownTableRow(['div']); expect(md).not.toMatch('#### Issues'); }); @@ -375,10 +375,13 @@ describe('auditsSection', () => { ], } as ScoredReport).toString(); expect(md).toMatch('#### Issues'); - expect(md).toMatch( - /\|\s*Severity\s*\|\s*Message\s*\|\s*Source file\s*\|\s*Line\(s\)\s*\|/, - ); - expect(md).toMatch(/\|\s*value\s*\|/); + expect(md).toContainMarkdownTableRow([ + 'Severity', + 'Message', + 'Source file', + 'Line(s)', + ]); + expect(md).toContainMarkdownTableRow(['value']); }); it('should render audit meta information', () => { @@ -472,12 +475,23 @@ describe('aboutSection', () => { ], categories: Array.from({ length: 3 }), } as ScoredReport).toString(); - expect(md).toMatch( - /\|\s*Commit\s*\|\s*Version\s*\|\s*Duration\s*\|\s*Plugins\s*\|\s*Categories\s*\|\s*Audits\s*\|/, - ); - expect(md).toMatch( - /\|\s*ci: update action \(535b8e9e557336618a764f3fa45609d224a62837\)\s*\|\s*`v1.0.0`\s*\|\s*4.20 s\s*\|\s*1\s*\|\s*3\s*\|\s*3\s*\|/, - ); + + expect(md).toContainMarkdownTableRow([ + 'Commit', + 'Version', + 'Duration', + 'Plugins', + 'Categories', + 'Audits', + ]); + expect(md).toContainMarkdownTableRow([ + 'ci: update action (535b8e9e557336618a764f3fa45609d224a62837)', + '`v1.0.0`', + '4.20 s', + '1', + '3', + '3', + ]); }); it('should return plugins section with content', () => { @@ -498,15 +512,25 @@ describe('aboutSection', () => { }, ], } as ScoredReport).toString(); - expect(md).toMatch( - /\|\s*Plugin\s*\|\s*Audits\s*\|\s*Version\s*\|\s*Duration\s*\|/, - ); - expect(md).toMatch( - /\|\s*Lighthouse\s*\|\s*78\s*\|\s*`1.0.1`\s*\|\s*15.37 s\s*\|/, - ); - expect(md).toMatch( - /\|\s*File Size\s*\|\s*2\s*\|\s*`0.3.12`\s*\|\s*260 ms\s*\|/, - ); + + expect(md).toContainMarkdownTableRow([ + 'Plugin', + 'Audits', + 'Version', + 'Duration', + ]); + expect(md).toContainMarkdownTableRow([ + 'Lighthouse', + '78', + '`1.0.1`', + '15.37 s', + ]); + expect(md).toContainMarkdownTableRow([ + 'File Size', + '2', + '`0.3.12`', + '260 ms', + ]); }); it('should return full about section', () => { @@ -534,9 +558,11 @@ describe('generateMdReport', () => { // report title expect(md).toMatch('# Code PushUp Report'); // categories section heading - expect(md).toMatch( - /\|\s*🏷 Category\s*\|\s*⭐ Score\s*\|\s*🛡 Audits\s*\|/, - ); + expect(md).toContainMarkdownTableRow([ + '🏷 Category', + '⭐ Score', + '🛡 Audits', + ]); // categories section heading expect(md).toMatch('## 🏷 Categories'); // audits heading @@ -544,9 +570,12 @@ describe('generateMdReport', () => { // about section heading expect(md).toMatch('## About'); // plugin table - expect(md).toMatch( - /\|\s*Plugin\s*\|\s*Audits\s*\|\s*Version\s*\|\s*Duration\s*\|/, - ); + expect(md).toContainMarkdownTableRow([ + 'Plugin', + 'Audits', + 'Version', + 'Duration', + ]); // made with <3 expect(md).toMatch('Made with ❤ by [Code PushUp]'); }); diff --git a/packages/utils/vite.config.unit.ts b/packages/utils/vite.config.unit.ts index 79a1203e3..e6fecfab6 100644 --- a/packages/utils/vite.config.unit.ts +++ b/packages/utils/vite.config.unit.ts @@ -27,6 +27,7 @@ export default defineConfig({ '../../testing/test-setup/src/lib/console.mock.ts', '../../testing/test-setup/src/lib/reset.mocks.ts', '../../testing/test-setup/src/lib/extend/ui-logger.matcher.ts', + '../../testing/test-setup/src/lib/extend/markdown-table.matcher.ts', ], }, }); diff --git a/testing/test-setup/src/lib/extend/markdown-table.matcher.ts b/testing/test-setup/src/lib/extend/markdown-table.matcher.ts new file mode 100644 index 000000000..ce6839e48 --- /dev/null +++ b/testing/test-setup/src/lib/extend/markdown-table.matcher.ts @@ -0,0 +1,43 @@ +import type { SyncExpectationResult } from '@vitest/expect'; +import { expect } from 'vitest'; + +export type CustomMarkdownTableMatchers = { + toContainMarkdownTableRow: (cells: string[]) => void; +}; + +expect.extend({ + toContainMarkdownTableRow: assertMarkdownTableRow, +}); + +function assertMarkdownTableRow( + actual: string, + expected: string[], +): SyncExpectationResult { + const rows = actual + .split('\n') + .map(line => line.trim()) + .filter(line => line.startsWith('|') && line.endsWith('|')) + .map(line => + line + .slice(1, -1) + .split(/(? cell.replace(/\\\|/g, '|').trim()), + ); + + const pass = rows.some( + row => + row.length === expected.length && + row.every((cell, i) => cell === expected[i]), + ); + return pass + ? { + pass, + message: () => + `Expected markdown not to contain a table row with cells: ${expected.join(', ')}`, + } + : { + pass, + message: () => + `Expected markdown to contain a table row with cells: ${expected.join(', ')}`, + }; +} diff --git a/testing/test-setup/src/lib/extend/markdown-table.matcher.unit.test.ts b/testing/test-setup/src/lib/extend/markdown-table.matcher.unit.test.ts new file mode 100644 index 000000000..727ca6289 --- /dev/null +++ b/testing/test-setup/src/lib/extend/markdown-table.matcher.unit.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest'; + +describe('markdown-table-matcher', () => { + it('should match header and data rows in a markdown table', () => { + const markdown = ` + | 🏷 Category | ⭐ Score | 🛡 Audits | + | :-------------------------- | :-------: | :-------: | + | [Security](#security) | 🟡 **81** | 2 | + | [Performance](#performance) | 🟡 **64** | 56 | + | [SEO](#seo) | 🟡 **61** | 11 | +`; + + expect(markdown).toContainMarkdownTableRow([ + '🏷 Category', + '⭐ Score', + '🛡 Audits', + ]); + expect(markdown).toContainMarkdownTableRow([ + ':--------------------------', + ':-------:', + ':-------:', + ]); + expect(markdown).toContainMarkdownTableRow([ + '[Performance](#performance)', + '🟡 **64**', + '56', + ]); + expect(markdown).not.toContainMarkdownTableRow([ + 'Non-existent cell', + 'Row cell', + 'Test cell', + ]); + }); + + it('should match table rows containing escaped pipe symbols', () => { + const markdown = ` + | Package | Versions | + | :--------- | :----------------------- | + | \`eslint\` | \`^8.0.0 \\|\\| ^9.0.0\` | + `; + expect(markdown).toContainMarkdownTableRow([ + '`eslint`', + '`^8.0.0 || ^9.0.0`', + ]); + }); + + it('should match table rows with an empty cell', () => { + const markdown = ` + | Severity | Message | Source file | Line(s) | + | :--------: | :------------------------ | :-------------------- | :-----: | + | 🚨 _error_ | File size is 20KB too big | \`list.component.ts\` | | + `; + expect(markdown).toContainMarkdownTableRow([ + '🚨 _error_', + 'File size is 20KB too big', + '`list.component.ts`', + '', + ]); + }); +}); diff --git a/testing/test-setup/src/vitest.d.ts b/testing/test-setup/src/vitest.d.ts index 2ee2fb9f5..095ad2d83 100644 --- a/testing/test-setup/src/vitest.d.ts +++ b/testing/test-setup/src/vitest.d.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import type { CustomMarkdownTableMatchers } from './lib/extend/markdown-table.matcher.js'; import type { CustomAsymmetricPathMatchers, CustomPathMatchers, @@ -6,7 +7,10 @@ import type { import type { CustomUiLoggerMatchers } from './lib/extend/ui-logger.matcher.js'; declare module 'vitest' { - interface Assertion extends CustomPathMatchers, CustomUiLoggerMatchers {} + interface Assertion + extends CustomPathMatchers, + CustomUiLoggerMatchers, + CustomMarkdownTableMatchers {} // eslint-disable-next-line @typescript-eslint/no-empty-object-type interface AsymmetricMatchersContaining extends CustomAsymmetricPathMatchers {} } diff --git a/testing/test-setup/vite.config.unit.ts b/testing/test-setup/vite.config.unit.ts index 45688aac5..9a8966612 100644 --- a/testing/test-setup/vite.config.unit.ts +++ b/testing/test-setup/vite.config.unit.ts @@ -23,6 +23,7 @@ export default defineConfig({ setupFiles: [ '../test-setup/src/lib/reset.mocks.ts', '../test-setup/src/lib/extend/path.matcher.ts', + '../test-setup/src/lib/extend/markdown-table.matcher.ts', ], }, });