From c13174b8aa85acdc33ecb4d802faa047187a3194 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 25 Aug 2026 00:53:59 -0300 Subject: [PATCH] =?UTF-8?q?feat(ops96):=20motor=20de=20m=C3=A9tricas=20bar?= =?UTF-8?q?atas=20+=20@vitest/coverage-v8=20atr=C3=A1s=20de=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- knip.json | 2 +- package.json | 1 + pnpm-lock.yaml | 93 ++++++++++++++++ scripts/lib/testing-audit-metrics-core.mjs | 105 ++++++++++++++++++ scripts/testing-audit-metrics.mjs | 96 ++++++++++++++++ .../unit/testingAuditMetricsCore.unit.spec.ts | 101 +++++++++++++++++ 6 files changed, 397 insertions(+), 1 deletion(-) create mode 100644 scripts/lib/testing-audit-metrics-core.mjs create mode 100644 scripts/testing-audit-metrics.mjs create mode 100644 tests/unit/testingAuditMetricsCore.unit.spec.ts diff --git a/knip.json b/knip.json index aa6920e3..6c792908 100644 --- a/knip.json +++ b/knip.json @@ -19,7 +19,7 @@ "server-only": ["tests/helpers/serverOnly.ts"] }, "ignoreBinaries": [], - "ignoreDependencies": ["tailwindcss-animate", "tw-animate-css", "shadcn", "eslint-config-next"], + "ignoreDependencies": ["tailwindcss-animate", "tw-animate-css", "shadcn", "eslint-config-next", "@vitest/coverage-v8"], "rules": { "exports": "error", "types": "error", diff --git a/package.json b/package.json index cc3c3d18..ff71b37c 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "@types/web-push": "^3.6.4", "@types/yauzl": "^3.4.0", "@vitejs/plugin-react": "4.5.2", + "@vitest/coverage-v8": "4.0.18", "csv-parse": "^7.0.1", "eslint": "^9.39.2", "eslint-config-next": "15.4.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c897bc8d..7cd32ccc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,9 @@ importers: '@vitejs/plugin-react': specifier: 4.5.2 version: 4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.7.0)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/coverage-v8': + specifier: 4.0.18 + version: 4.0.18(vitest@4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.7.0)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0)) csv-parse: specifier: ^7.0.1 version: 7.0.1 @@ -630,6 +633,10 @@ packages: '@types/react': optional: true + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@borewit/text-codec@0.2.1': resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} @@ -3794,6 +3801,15 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + '@vitest/coverage-v8@4.0.18': + resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} + peerDependencies: + '@vitest/browser': 4.0.18 + vitest: 4.0.18 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} @@ -3992,6 +4008,9 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -5376,6 +5395,9 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -5699,6 +5721,18 @@ packages: isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -5721,6 +5755,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5963,6 +6000,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.4: + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -8583,6 +8627,8 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@bcoe/v8-coverage@1.0.2': {} + '@borewit/text-codec@0.2.1': {} '@csstools/color-helpers@6.0.1': {} @@ -11656,6 +11702,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.7.0)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0))': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.18 + ast-v8-to-istanbul: 0.3.12 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.4 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.7.0)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(@typescript/typescript6@6.0.2))(sass@1.77.4)(tsx@4.21.0)(yaml@2.9.0) + '@vitest/expect@4.0.18': dependencies: '@standard-schema/spec': 1.1.0 @@ -11909,6 +11969,12 @@ snapshots: dependencies: tslib: 2.8.1 + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + async-function@1.0.0: {} atomic-sleep@1.0.0: {} @@ -13435,6 +13501,8 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + html-escaper@2.0.2: {} + html-url-attributes@3.0.1: {} http-errors@2.0.1: @@ -13699,6 +13767,19 @@ snapshots: isomorphic.js@0.2.5: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -13718,6 +13799,8 @@ snapshots: joycon@3.1.1: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -13967,6 +14050,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.4: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + markdown-table@3.0.4: {} marked@14.0.0: {} diff --git a/scripts/lib/testing-audit-metrics-core.mjs b/scripts/lib/testing-audit-metrics-core.mjs new file mode 100644 index 00000000..4c23e558 --- /dev/null +++ b/scripts/lib/testing-audit-metrics-core.mjs @@ -0,0 +1,105 @@ +/** + * Pure core for testing-audit metrics (OPS96) — normalization of native + * runner reports into the audit's per-layer portrait. No I/O here: the CLI + * (scripts/testing-audit-metrics.mjs) spawns the suites and feeds these + * functions; the unit spec pins their behavior against fixture payloads. + */ + +const DEFAULT_TOP = 15 + +const ms = (value) => `${(value / 1000).toFixed(1)}s` + +/** @returns {number} */ +const fileDurationMs = (entry) => { + if (typeof entry.endTime === 'number' && typeof entry.startTime === 'number') { + return Math.max(0, entry.endTime - entry.startTime) + } + return entry.assertionResults?.reduce((sum, result) => sum + (result.duration ?? 0), 0) ?? 0 +} + +const slowestAssertion = (entry) => + (entry.assertionResults ?? []) + .filter((result) => typeof result.duration === 'number') + .reduce( + (slowest, result) => (slowest === null || result.duration > slowest.duration ? result : slowest), + null, + ) + +/** + * Normalize a Vitest JSON reporter payload (jest-compatible shape) into a + * layer summary: totals plus per-file rows sorted by duration desc. + * + * @param {unknown} report parsed JSON from `vitest run --reporter=json` + * @returns {{ + * totals: { files: number, tests: number, failedTests: number, durationMs: number }, + * rows: Array<{ file: string, tests: number, durationMs: number, slowestTest: string | null, slowestMs: number | null }>, + * }} + */ +export const summarizeVitestReport = (report) => { + if (!report || !Array.isArray(report.testResults)) { + throw new Error('invalid vitest JSON report: missing testResults') + } + const rows = report.testResults.map((entry) => { + const slowest = slowestAssertion(entry) + return { + file: String(entry.name ?? '(desconhecido)').replace(/^.*tests\//, 'tests/'), + tests: entry.assertionResults?.length ?? 0, + durationMs: fileDurationMs(entry), + slowestTest: slowest ? slowest.title : null, + slowestMs: slowest ? slowest.duration : null, + } + }) + rows.sort((a, b) => b.durationMs - a.durationMs) + return { + totals: { + files: rows.length, + tests: Number(report.numTotalTests ?? rows.reduce((sum, row) => sum + row.tests, 0)), + failedTests: Number(report.numFailedTests ?? 0), + durationMs: rows.reduce((sum, row) => sum + row.durationMs, 0), + }, + rows, + } +} + +/** + * Markdown table of the slowest spec files of a layer. + * + * @param {ReturnType} summary + * @param {{ top?: number }} [options] + */ +export const renderSlowestFilesTable = (summary, { top = DEFAULT_TOP } = {}) => { + const lines = [ + '| arquivo | testes | duração | teste mais lento |', + '|---|---:|---:|---|', + ] + for (const row of summary.rows.slice(0, Math.max(0, top))) { + const slowest = + row.slowestTest === null ? '—' : `${row.slowestTest} (${ms(row.slowestMs ?? 0)})` + lines.push(`| ${row.file} | ${row.tests} | ${ms(row.durationMs)} | ${slowest} |`) + } + return lines.join('\n') +} + +/** + * Static inventory of an e2e spec source — counting only, never execution. + * + * @param {{ file: string, content: string }} sources + * @returns {Array<{ file: string, describes: number, tests: number }>} + */ +export const inventoryE2ESource = ({ file, content }) => ({ + file, + describes: (content.match(/\bdescribe\s*\(/g) ?? []).length, + tests: (content.match(/\btest\s*\(/g) ?? []).length, +}) + +/** Markdown table of the static e2e inventory (durations deliberately absent). */ +export const renderE2EInventoryTable = (rows) => { + const lines = [ + '| arquivo | describes | testes | duração |', + '|---|---:|---:|---|', + ...rows.map( + (row) => `| ${row.file} | ${row.describes} | ${row.tests} | não medido — execução noturna proibida |`, + ), + ] + return lines.join('\n') +} diff --git a/scripts/testing-audit-metrics.mjs b/scripts/testing-audit-metrics.mjs new file mode 100644 index 00000000..9f4be9ee --- /dev/null +++ b/scripts/testing-audit-metrics.mjs @@ -0,0 +1,96 @@ +#!/usr/bin/env node +/** + * testing-audit-metrics (OPS96) — the cheap-metrics tool behind /testing-audit: + * per-layer portrait of the suite from native reporters, nothing third-party. + * + * node scripts/testing-audit-metrics.mjs unit|int [--top N] + * node scripts/testing-audit-metrics.mjs e2e-inventory + * + * unit/int: spawns `pnpm test: --reporter=json` (the package scripts + * carry their own env guards — invalid DATABASE_URL for unit, .env.test.local + * worktree DB for int), parses the JSON payload and prints totals plus a + * slowest-files markdown table for the audit report. The runner's exit code + * propagates: a red suite still prints its portrait, then fails. + * e2e-inventory: static count of describe/test blocks per spec file — durations + * are deliberately absent because running Playwright overnight is forbidden. + */ +import { readdirSync, readFileSync } from 'node:fs' +import { spawnSync } from 'node:child_process' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { + inventoryE2ESource, + renderE2EInventoryTable, + renderSlowestFilesTable, + summarizeVitestReport, +} from './lib/testing-audit-metrics-core.mjs' + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const E2E_DIR = 'tests/e2e' +const DEFAULT_TOP = 15 + +const usage = () => { + console.error('usage: node scripts/testing-audit-metrics.mjs [--top N]') + process.exit(2) +} + +const args = process.argv.slice(2) +const mode = args.find((arg) => !arg.startsWith('--')) +if (mode !== 'unit' && mode !== 'int' && mode !== 'e2e-inventory') usage() +const topFlag = args.indexOf('--top') +const top = topFlag === -1 ? DEFAULT_TOP : Number(args[topFlag + 1]) +if (!Number.isInteger(top) || top <= 0) usage() + +/** Vitest's jest-compatible JSON payload starts with this key. */ +const VITEST_JSON_ANCHOR = '{"numTotal' + +const runVitestLayer = async (layer) => { + console.log(`[metrics] rodando pnpm test:${layer} --reporter=json …`) + const result = spawnSync('pnpm', [`test:${layer}`, '--reporter=json'], { + cwd: repoRoot, + encoding: 'utf8', + maxBuffer: 256 * 1024 * 1024, + }) + const jsonStart = (result.stdout ?? '').indexOf(VITEST_JSON_ANCHOR) + if (jsonStart === -1) { + console.error(`[metrics] vitest não produziu relatório JSON no stdout (exit ${result.status})`) + console.error((result.stderr ?? '').slice(-4000)) + process.exit(result.status ?? 2) + } + let report + try { + report = JSON.parse(result.stdout.slice(jsonStart)) + } catch (error) { + console.error(`[metrics] relatório JSON inválido: ${error instanceof Error ? error.message : error}`) + process.exit(2) + } + const summary = summarizeVitestReport(report) + console.log( + `\n## ${layer} — ${summary.totals.files} arquivos / ${summary.totals.tests} testes` + + ` (${summary.totals.failedTests} falhas) / parede dos arquivos ${ms(summary.totals.durationMs)}\n`, + ) + console.log(renderSlowestFilesTable(summary, { top })) + if (result.status !== 0) process.exit(result.status) +} + +const ms = (value) => `${(value / 1000).toFixed(1)}s` + +const runE2EInventory = () => { + const dir = path.join(repoRoot, E2E_DIR) + const rows = readdirSync(dir) + .filter((name) => name.endsWith('.spec.ts')) + .sort() + .map((name) => + inventoryE2ESource({ + file: `${E2E_DIR}/${name}`, + content: readFileSync(path.join(dir, name), 'utf8'), + }), + ) + const totalTests = rows.reduce((sum, row) => sum + row.tests, 0) + console.log(`\n## e2e (inventário estático) — ${rows.length} arquivos / ${totalTests} testes\n`) + console.log(renderE2EInventoryTable(rows)) +} + +if (mode === 'e2e-inventory') runE2EInventory() +else await runVitestLayer(mode) diff --git a/tests/unit/testingAuditMetricsCore.unit.spec.ts b/tests/unit/testingAuditMetricsCore.unit.spec.ts new file mode 100644 index 00000000..5aaa787f --- /dev/null +++ b/tests/unit/testingAuditMetricsCore.unit.spec.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest' + +import { + inventoryE2ESource, + renderE2EInventoryTable, + renderSlowestFilesTable, + summarizeVitestReport, +} from '../../scripts/lib/testing-audit-metrics-core.mjs' + +const reportFixture = () => ({ + numTotalTests: 3, + numFailedTests: 0, + testResults: [ + { + name: '/abs/repo/tests/unit/b.unit.spec.ts', + endTime: 1600, + startTime: 500, + assertionResults: [ + { title: 'slow one', status: 'passed', duration: 400 }, + { title: 'fast one', status: 'passed', duration: 10 }, + ], + }, + { + name: '/abs/repo/tests/unit/a.unit.spec.ts', + endTime: 2000, + startTime: 1000, + assertionResults: [{ title: 'only one', status: 'passed', duration: 50 }], + }, + ], +}) + +describe('summarizeVitestReport', () => { + it('totals files and tests and sorts rows by duration desc', () => { + const summary = summarizeVitestReport(reportFixture()) + + expect(summary.totals).toEqual({ files: 2, tests: 3, failedTests: 0, durationMs: 2100 }) + expect(summary.rows.map((row) => row.file)).toEqual([ + 'tests/unit/b.unit.spec.ts', + 'tests/unit/a.unit.spec.ts', + ]) + }) + + it('keeps the slowest assertion per file', () => { + const summary = summarizeVitestReport(reportFixture()) + + expect(summary.rows[0].slowestTest).toBe('slow one') + expect(summary.rows[0].slowestMs).toBe(400) + }) + + it('falls back to summed durations when the payload lacks timestamps', () => { + const payload = { + testResults: [{ name: '/x/y.unit.spec.ts', assertionResults: [{ title: 't', duration: 120 }] }], + } + + expect(summarizeVitestReport(payload).rows[0].durationMs).toBe(120) + }) + + it('rejects payloads without testResults', () => { + expect(() => summarizeVitestReport({})).toThrow('missing testResults') + }) +}) + +describe('renderSlowestFilesTable', () => { + it('renders a markdown table limited to top N rows', () => { + const table = renderSlowestFilesTable(summarizeVitestReport(reportFixture()), { top: 1 }) + + expect(table.split('\n')).toHaveLength(3) + expect(table).toContain('| tests/unit/b.unit.spec.ts | 2 | 1.1s | slow one (0.4s) |') + }) +}) + +describe('inventoryE2ESource', () => { + it('counts describe and test blocks statically', () => { + const row = inventoryE2ESource({ + file: 'tests/e2e/x.e2e.spec.ts', + content: "test('a', () => {})\n describe('d', () => { test('b', () => {}) })", + }) + + expect(row).toEqual({ file: 'tests/e2e/x.e2e.spec.ts', describes: 1, tests: 2 }) + }) + + it('does not count words merely containing the keywords', () => { + const row = inventoryE2ESource({ + file: 'tests/e2e/x.e2e.spec.ts', + content: '// contest( and described( are not blocks', + }) + + expect(row.tests).toBe(0) + expect(row.describes).toBe(0) + }) +}) + +describe('renderE2EInventoryTable', () => { + it('marks durations as not measured overnight', () => { + const table = renderE2EInventoryTable([ + { file: 'tests/e2e/x.e2e.spec.ts', describes: 1, tests: 4 }, + ]) + + expect(table).toContain('| tests/e2e/x.e2e.spec.ts | 1 | 4 | não medido — execução noturna proibida |') + }) +})