Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/spec-dashboard/src/apis.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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[] = [
Expand Down
81 changes: 66 additions & 15 deletions packages/spec-dashboard/src/components/coverage-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ function getEmitterDisplayName(
return emitterName;
}

function getEmitterOverviewKey(
emitterName: string,
report: CoverageSummary["generatorReports"][string],
emitterDisplayNames?: Record<string, string>,
): string {
return getEmitterDisplayName(emitterName, report, emitterDisplayNames);
}

/**
* Gets the accent color for a coverage ratio using the same thresholds as the coverage tables.
*/
Expand All @@ -59,40 +67,83 @@ export const CoverageOverview: FunctionComponent<CoverageOverviewProps> = ({
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.
Comment thread
live1206 marked this conversation as resolved.
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<string>;
coveredScenarioNames: Set<string>;
}
>();

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, {
Comment thread
live1206 marked this conversation as resolved.
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,
});
}
Expand Down
Loading