diff --git a/packages/spec-dashboard/src/apis.test.ts b/packages/spec-dashboard/src/apis.test.ts index f46702a047e..97d2ed9185d 100644 --- a/packages/spec-dashboard/src/apis.test.ts +++ b/packages/spec-dashboard/src/apis.test.ts @@ -1,7 +1,10 @@ import type { ScenarioManifest } from "@typespec/spec-coverage-sdk"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; import { expect, it } from "vitest"; import type { TableDefinition } from "./apis.js"; import { splitManifestByTables } from "./apis.js"; +import { CoverageOverview } from "./components/coverage-overview.js"; const createManifest = ( packageName: string, @@ -225,6 +228,55 @@ it("should not duplicate scenarios across tables", () => { expect(defaultTable!.manifest.scenarios[0].name).toBe("unique_scenario"); }); +it("should group overview coverage by logical display name across emitter packages", () => { + const coverageSummaries = [ + { + manifest: { + packageName: "azure-test", + displayName: "Azure Test", + commit: "abc123", + version: "1.0.0", + scenarios: [ + { + name: "scenario_1", + scenarioDoc: "Doc", + location: { + path: "x", + start: { line: 1, character: 1 }, + end: { line: 2, character: 1 }, + }, + }, + ], + }, + tableName: "Azure Test", + generatorReports: { + "@azure-typespec/http-client-csharp": { + generatorMetadata: { name: "C#", version: "1.0.0" }, + results: { scenario_1: "pass" }, + }, + "@azure-typespec/http-client-csharp-mgmt": { + generatorMetadata: { name: "C#", version: "1.0.0" }, + results: { scenario_1: "pass" }, + }, + }, + }, + ] as any; + + const html = renderToStaticMarkup( + createElement(CoverageOverview, { + coverageSummaries, + emitterDisplayNames: { + "@azure-typespec/http-client-csharp": "C#", + "@azure-typespec/http-client-csharp-mgmt": "C#", + }, + }), + ); + + const cSharpMatches = html.match(/C#/g) ?? []; + expect(cSharpMatches).toHaveLength(1); + expect(html).not.toContain("@azure-typespec/http-client-csharp-mgmt"); +}); + it("should include emitterNames from table definition", () => { const manifest = createManifest("test-package", "Display Name", ["scenario1"]); const tables: TableDefinition[] = [ diff --git a/packages/spec-dashboard/src/components/coverage-overview.tsx b/packages/spec-dashboard/src/components/coverage-overview.tsx index 7e8d8d54506..59565081538 100644 --- a/packages/spec-dashboard/src/components/coverage-overview.tsx +++ b/packages/spec-dashboard/src/components/coverage-overview.tsx @@ -39,6 +39,14 @@ function getEmitterDisplayName( return emitterName; } +function getEmitterOverviewKey( + emitterName: string, + report: CoverageSummary["generatorReports"][string], + emitterDisplayNames?: Record, +): string { + return getEmitterDisplayName(emitterName, report, emitterDisplayNames); +} + /** * Gets the accent color for a coverage ratio using the same thresholds as the coverage tables. */ @@ -59,40 +67,83 @@ export const CoverageOverview: FunctionComponent = ({ emitterDisplayNames, }) => { const emitterOverviews = useMemo(() => { - // Aggregate scenarios per emitter across all summaries + // Aggregate scenarios per logical emitter language across all summaries. + // This keeps emitters that share the same display name (for example C# data-plane + // and management-plane emitters) grouped into a single overview card. const emitterMap = new Map< string, { totalScenarios: number; coveredScenarios: number; report: CoverageSummary["generatorReports"][string]; + displayName: string; } >(); for (const summary of coverageSummaries) { + const summaryGroupMap = new Map< + string, + { + scenarioNames: Set; + coveredScenarioNames: Set; + } + >(); + for (const [emitterName, report] of Object.entries(summary.generatorReports)) { - if (!emitterMap.has(emitterName)) { - emitterMap.set(emitterName, { totalScenarios: 0, coveredScenarios: 0, report }); + const groupKey = getEmitterOverviewKey(emitterName, report, emitterDisplayNames); + if (!summaryGroupMap.has(groupKey)) { + summaryGroupMap.set(groupKey, { + scenarioNames: new Set(), + coveredScenarioNames: new Set(), + }); } - const entry = emitterMap.get(emitterName)!; - const scenarios = summary.manifest.scenarios; - entry.totalScenarios += scenarios.length; - if (report) { - for (const scenario of scenarios) { - const status = report.results[scenario.name]; - if (status === "pass" || status === "not-applicable" || status === "not-supported") { - entry.coveredScenarios++; - } + + const entry = summaryGroupMap.get(groupKey)!; + for (const scenario of summary.manifest.scenarios) { + entry.scenarioNames.add(scenario.name); + const status = report?.results[scenario.name]; + if ( + report && + (status === "pass" || status === "not-applicable" || status === "not-supported") + ) { + entry.coveredScenarioNames.add(scenario.name); } } } + + for (const [groupKey, data] of summaryGroupMap) { + if (!emitterMap.has(groupKey)) { + const firstReport = Object.entries(summary.generatorReports).find( + ([emitterName, report]) => + getEmitterOverviewKey(emitterName, report, emitterDisplayNames) === groupKey, + )?.[1]; + + emitterMap.set(groupKey, { + totalScenarios: 0, + coveredScenarios: 0, + report: firstReport, + displayName: getEmitterDisplayName( + Object.entries(summary.generatorReports).find( + ([emitterName, report]) => + getEmitterOverviewKey(emitterName, report, emitterDisplayNames) === groupKey, + )?.[0] ?? groupKey, + firstReport, + emitterDisplayNames, + ), + }); + } + + const entry = emitterMap.get(groupKey)!; + entry.totalScenarios += data.scenarioNames.size; + entry.coveredScenarios += data.coveredScenarioNames.size; + } } const overviews: EmitterOverview[] = []; - for (const [emitterName, data] of emitterMap) { + for (const [groupKey, data] of emitterMap) { overviews.push({ - name: emitterName, - displayName: getEmitterDisplayName(emitterName, data.report, emitterDisplayNames), + name: groupKey, + displayName: data.displayName, coverageRatio: data.totalScenarios > 0 ? data.coveredScenarios / data.totalScenarios : 0, }); }