From 9f24cff9dd44dd35deff61febeebc1cc1e08e1a8 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:46:12 +0800 Subject: [PATCH] chore(web): enhance frontend tests (#29859) --- .github/workflows/web-tests.yml | 165 +++++++++++++++++- .../view-annotation-modal/index.spec.tsx | 35 +++- 2 files changed, 191 insertions(+), 9 deletions(-) diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index a22d0a9d1d..dd311701b5 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -84,6 +84,13 @@ jobs: process.exit(0); } + const summary = hasSummary + ? JSON.parse(fs.readFileSync(summaryPath, 'utf8')) + : null; + const coverage = hasFinal + ? JSON.parse(fs.readFileSync(finalPath, 'utf8')) + : null; + const totals = { lines: { covered: 0, total: 0 }, statements: { covered: 0, total: 0 }, @@ -92,15 +99,14 @@ jobs: }; const fileSummaries = []; - if (hasSummary) { - const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')); + if (summary) { const totalEntry = summary.total ?? {}; ['lines', 'statements', 'branches', 'functions'].forEach((key) => { if (totalEntry[key]) { totals[key].covered = totalEntry[key].covered ?? 0; totals[key].total = totalEntry[key].total ?? 0; } - }); + }); Object.entries(summary) .filter(([file]) => file !== 'total') @@ -114,9 +120,7 @@ jobs: }, }); }); - } else if (hasFinal) { - const coverage = JSON.parse(fs.readFileSync(finalPath, 'utf8')); - + } else if (coverage) { Object.entries(coverage).forEach(([file, entry]) => { const lineHits = entry.l ?? {}; const statementHits = entry.s ?? {}; @@ -183,6 +187,155 @@ jobs: }); console.log('```'); console.log(''); + + if (coverage) { + const pctValue = (covered, tot) => { + if (tot === 0) { + return '0'; + } + return ((covered / tot) * 100) + .toFixed(2) + .replace(/\.?0+$/, ''); + }; + + const formatLineRanges = (lines) => { + if (lines.length === 0) { + return ''; + } + const ranges = []; + let start = lines[0]; + let end = lines[0]; + + for (let i = 1; i < lines.length; i += 1) { + const current = lines[i]; + if (current === end + 1) { + end = current; + continue; + } + ranges.push(start === end ? `${start}` : `${start}-${end}`); + start = current; + end = current; + } + ranges.push(start === end ? `${start}` : `${start}-${end}`); + return ranges.join(','); + }; + + const tableTotals = { + statements: { covered: 0, total: 0 }, + branches: { covered: 0, total: 0 }, + functions: { covered: 0, total: 0 }, + lines: { covered: 0, total: 0 }, + }; + const tableRows = Object.entries(coverage) + .map(([file, entry]) => { + const lineHits = entry.l ?? {}; + const statementHits = entry.s ?? {}; + const branchHits = entry.b ?? {}; + const functionHits = entry.f ?? {}; + + const lineTotal = Object.keys(lineHits).length; + const lineCovered = Object.values(lineHits).filter((n) => n > 0).length; + const statementTotal = Object.keys(statementHits).length; + const statementCovered = Object.values(statementHits).filter((n) => n > 0).length; + const branchTotal = Object.values(branchHits).reduce((acc, branches) => acc + branches.length, 0); + const branchCovered = Object.values(branchHits).reduce( + (acc, branches) => acc + branches.filter((n) => n > 0).length, + 0, + ); + const functionTotal = Object.keys(functionHits).length; + const functionCovered = Object.values(functionHits).filter((n) => n > 0).length; + + tableTotals.lines.total += lineTotal; + tableTotals.lines.covered += lineCovered; + tableTotals.statements.total += statementTotal; + tableTotals.statements.covered += statementCovered; + tableTotals.branches.total += branchTotal; + tableTotals.branches.covered += branchCovered; + tableTotals.functions.total += functionTotal; + tableTotals.functions.covered += functionCovered; + + const uncoveredLines = Object.entries(lineHits) + .filter(([, count]) => count === 0) + .map(([line]) => Number(line)) + .sort((a, b) => a - b); + + const filePath = entry.path ?? file; + const relativePath = path.isAbsolute(filePath) + ? path.relative(process.cwd(), filePath) + : filePath; + + return { + file: relativePath || file, + statements: pctValue(statementCovered, statementTotal), + branches: pctValue(branchCovered, branchTotal), + functions: pctValue(functionCovered, functionTotal), + lines: pctValue(lineCovered, lineTotal), + uncovered: formatLineRanges(uncoveredLines), + }; + }) + .sort((a, b) => a.file.localeCompare(b.file)); + + const columns = [ + { key: 'file', header: 'File', align: 'left' }, + { key: 'statements', header: '% Stmts', align: 'right' }, + { key: 'branches', header: '% Branch', align: 'right' }, + { key: 'functions', header: '% Funcs', align: 'right' }, + { key: 'lines', header: '% Lines', align: 'right' }, + { key: 'uncovered', header: 'Uncovered Line #s', align: 'left' }, + ]; + + const allFilesRow = { + file: 'All files', + statements: pctValue(tableTotals.statements.covered, tableTotals.statements.total), + branches: pctValue(tableTotals.branches.covered, tableTotals.branches.total), + functions: pctValue(tableTotals.functions.covered, tableTotals.functions.total), + lines: pctValue(tableTotals.lines.covered, tableTotals.lines.total), + uncovered: '', + }; + + const rowsForOutput = [allFilesRow, ...tableRows]; + const columnWidths = Object.fromEntries( + columns.map(({ key, header }) => [key, header.length]), + ); + + rowsForOutput.forEach((row) => { + columns.forEach(({ key }) => { + const value = String(row[key] ?? ''); + columnWidths[key] = Math.max(columnWidths[key], value.length); + }); + }); + + const formatRow = (row) => columns + .map(({ key, align }) => { + const value = String(row[key] ?? ''); + const width = columnWidths[key]; + return align === 'right' ? value.padStart(width) : value.padEnd(width); + }) + .join(' | '); + + const headerRow = columns + .map(({ header, key, align }) => { + const width = columnWidths[key]; + return align === 'right' ? header.padStart(width) : header.padEnd(width); + }) + .join(' | '); + + const dividerRow = columns + .map(({ key }) => '-'.repeat(columnWidths[key])) + .join('|'); + + console.log(''); + console.log('
Jest coverage table'); + console.log(''); + console.log('```'); + console.log(dividerRow); + console.log(headerRow); + console.log(dividerRow); + rowsForOutput.forEach((row) => console.log(formatRow(row))); + console.log(dividerRow); + console.log('```'); + console.log('
'); + } NODE - name: Upload Coverage Artifact diff --git a/web/app/components/app/annotation/view-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/view-annotation-modal/index.spec.tsx index b5e3241fff..dec0ad0c01 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.spec.tsx @@ -77,24 +77,53 @@ describe('ViewAnnotationModal', () => { fetchHitHistoryListMock.mockResolvedValue({ data: [], total: 0 }) }) - it('should render annotation tab and allow saving updated content', async () => { + it('should render annotation tab and allow saving updated query', async () => { + // Arrange const { props } = renderComponent() await waitFor(() => { expect(fetchHitHistoryListMock).toHaveBeenCalled() }) + // Act fireEvent.click(screen.getByTestId('edit-query')) + + // Assert await waitFor(() => { expect(props.onSave).toHaveBeenCalledWith('query-updated', props.item.answer) }) + }) + + it('should render annotation tab and allow saving updated answer', async () => { + // Arrange + const { props } = renderComponent() - fireEvent.click(screen.getByTestId('edit-answer')) await waitFor(() => { - expect(props.onSave).toHaveBeenCalledWith(props.item.question, 'answer-updated') + expect(fetchHitHistoryListMock).toHaveBeenCalled() }) + // Act + fireEvent.click(screen.getByTestId('edit-answer')) + + // Assert + await waitFor(() => { + expect(props.onSave).toHaveBeenCalledWith(props.item.question, 'answer-updated') + }, + ) + }) + + it('should switch to hit history tab and show no data message', async () => { + // Arrange + const { props } = renderComponent() + + await waitFor(() => { + expect(fetchHitHistoryListMock).toHaveBeenCalled() + }) + + // Act fireEvent.click(screen.getByText('appAnnotation.viewModal.hitHistory')) + + // Assert expect(await screen.findByText('appAnnotation.viewModal.noHitHistory')).toBeInTheDocument() expect(mockFormatTime).toHaveBeenCalledWith(props.item.created_at, 'appLog.dateTimeFormat') })