diff --git a/packages/plugin-api/src/report.ts b/packages/plugin-api/src/report.ts index 2cfec224ec6..c284b91331b 100644 --- a/packages/plugin-api/src/report.ts +++ b/packages/plugin-api/src/report.ts @@ -53,6 +53,7 @@ export type ReportOptions = { ci?: CiDescriptor; executor?: ReportExecutorInfo; runSummary?: ReportRunSummary; + runSummaryByEnv?: Record; stepTreeExpansion?: StepTreeExpansion; defaultSortBy?: string; }; diff --git a/packages/plugin-awesome/src/generators.ts b/packages/plugin-awesome/src/generators.ts index 1d185c7d971..4a38769e275 100644 --- a/packages/plugin-awesome/src/generators.ts +++ b/packages/plugin-awesome/src/generators.ts @@ -749,6 +749,7 @@ export const generateStaticFiles = async ( reportName: string; executor?: ReportExecutorInfo; runSummary?: ReportRunSummary; + runSummaryByEnv?: Record; }, ) => { const { @@ -767,6 +768,7 @@ export const generateStaticFiles = async ( ci, executor, runSummary, + runSummaryByEnv, stepTreeExpansion, defaultSortBy, } = payload; @@ -823,6 +825,7 @@ export const generateStaticFiles = async ( ci, executor, runSummary, + runSummaryByEnv, layout, allureVersion, sections, diff --git a/packages/plugin-awesome/src/plugin.ts b/packages/plugin-awesome/src/plugin.ts index f9246e0a174..71e923e769e 100644 --- a/packages/plugin-awesome/src/plugin.ts +++ b/packages/plugin-awesome/src/plugin.ts @@ -8,6 +8,7 @@ import { import { type AllureStore, type ReportExecutorInfo, + type ReportRunSummary, type Plugin, type PluginContext, type PluginSummary, @@ -150,6 +151,16 @@ export class AwesomePlugin implements Plugin { }), ); + const runSummaryByEnv: Record = {}; + + for (const { id } of environments) { + const envRunSummary = getRunSummary(trsByEnvId.get(id) ?? []); + + if (envRunSummary) { + runSummaryByEnv[id] = envRunSummary; + } + } + await generateStatistic(this.#writer!, { stats: statistics, statsByEnv: envStatistics, @@ -262,6 +273,7 @@ export class AwesomePlugin implements Plugin { ci: context.ci, executor, runSummary, + runSummaryByEnv, reportDataFiles, }); }; diff --git a/packages/plugin-awesome/test/plugin.test.ts b/packages/plugin-awesome/test/plugin.test.ts index 4d8900911cd..01476306d28 100644 --- a/packages/plugin-awesome/test/plugin.test.ts +++ b/packages/plugin-awesome/test/plugin.test.ts @@ -867,20 +867,32 @@ describe("plugin", () => { }); describe("report assets", () => { + const environmentIdOf = (tr: TestResult) => tr.environment ?? "default"; + const makeSingleFileStore = (testResults: TestResult[], metadata: Record = {}): AllureStore => ({ metadataByKey: vi.fn(async (key: string) => metadata[key]), - allEnvironments: vi.fn().mockResolvedValue(["default"]), - allEnvironmentIdentities: vi - .fn() - .mockResolvedValue([{ id: "default", name: "default" } satisfies EnvironmentIdentity]), + allEnvironments: vi.fn(async () => [...new Set(testResults.map(environmentIdOf))]), + allEnvironmentIdentities: vi.fn( + async () => + [...new Set(testResults.map(environmentIdOf))].map((id) => ({ + id, + name: id, + })) satisfies EnvironmentIdentity[], + ), allAttachments: vi.fn().mockResolvedValue([]), allTestResults: vi.fn(async (options?: { includeRetries?: boolean; filter?: (tr: TestResult) => boolean }) => { const trs = options?.filter ? testResults.filter(options.filter) : testResults; return trs; }), - testResultsByEnvironmentId: vi.fn().mockResolvedValue(testResults), - environmentIdByTrId: vi.fn().mockResolvedValue("default"), + testResultsByEnvironmentId: vi.fn(async (envId: string) => + testResults.filter((tr) => environmentIdOf(tr) === envId), + ), + environmentIdByTrId: vi.fn(async (trId: string) => { + const tr = testResults.find(({ id }) => id === trId); + + return tr ? environmentIdOf(tr) : undefined; + }), testsStatistic: vi.fn(async (filter: (tr: TestResult) => boolean) => getTestResultsStats(testResults, filter)), allTestEnvGroups: vi.fn().mockResolvedValue([]), allGlobalAttachments: vi.fn().mockResolvedValue([]), @@ -1149,5 +1161,58 @@ describe("plugin", () => { }); expect(reportOptions.executor).toEqual(executor); }); + + it("should include a separate launch interval for every environment", async () => { + const testResults: TestResult[] = [ + { + id: "tr-staging", + name: "staging test", + status: "passed", + environment: "staging", + start: 1000, + stop: 3000, + labels: [], + }, + { + id: "tr-staging-retry", + name: "staging test", + status: "failed", + environment: "staging", + isRetry: true, + start: 500, + stop: 900, + labels: [], + }, + { + id: "tr-prod", + name: "prod test", + status: "passed", + environment: "prod", + start: 10_000, + stop: 12_500, + labels: [], + }, + ] as unknown as TestResult[]; + const addedFiles = new Map(); + const reportFiles: ReportFiles = { + addFile: vi.fn(async (path: string, data: Buffer) => { + addedFiles.set(path, data); + return path; + }), + }; + const plugin = new AwesomePlugin({ singleFile: true }); + + await plugin.start(makeSingleFileContext(reportFiles)); + await plugin.done(makeSingleFileContext(reportFiles), makeSingleFileStore(testResults)); + + const reportOptions = extractReportOptions(addedFiles.get("index.html")?.toString("utf-8") ?? ""); + + // The report-wide summary still spans everything, the per-environment ones must not. + expect(reportOptions.runSummary).toEqual({ start: 500, stop: 12_500, duration: 12_000 }); + expect(reportOptions.runSummaryByEnv).toEqual({ + staging: { start: 500, stop: 3000, duration: 2500 }, + prod: { start: 10_000, stop: 12_500, duration: 2500 }, + }); + }); }); }); diff --git a/packages/web-awesome/src/components/ReportHeader/index.tsx b/packages/web-awesome/src/components/ReportHeader/index.tsx index e343fd5059c..f822aa9f1d8 100644 --- a/packages/web-awesome/src/components/ReportHeader/index.tsx +++ b/packages/web-awesome/src/components/ReportHeader/index.tsx @@ -7,6 +7,7 @@ import { ReportHeaderLogo } from "@/components/ReportHeader/ReportHeaderLogo"; import { ReportHeaderPie } from "@/components/ReportHeader/ReportHeaderPie"; import { TrStatus } from "@/components/TestResult/TrStatus"; import { useI18n } from "@/stores"; +import { currentEnvironment } from "@/stores/env"; import { globalsStore } from "@/stores/globals"; import { timestampToDate } from "@/utils/time"; @@ -22,11 +23,15 @@ const reportDateOptions: Intl.DateTimeFormatOptions = { }; export const ReportHeader = () => { - const { reportName, createdAt, runSummary } = getReportOptions() ?? {}; + const { reportName, createdAt, runSummary, runSummaryByEnv } = getReportOptions() ?? {}; const { t } = useI18n("ui"); + const environmentId = currentEnvironment.value; + // With a single environment selected, show that environment's own launch interval instead of the + // report-wide one, which spans the earliest start and the latest stop across all environments. + const selectedRunSummary = environmentId ? runSummaryByEnv?.[environmentId] : runSummary; const formattedCreatedAt = timestampToDate(createdAt as number, reportDateOptions); - const formattedReportTime = runSummary - ? `${timestampToDate(runSummary.start, reportDateOptions)} (${formatDuration(runSummary.duration)})` + const formattedReportTime = selectedRunSummary + ? `${timestampToDate(selectedRunSummary.start, reportDateOptions)} (${formatDuration(selectedRunSummary.duration)})` : formattedCreatedAt; return ( diff --git a/packages/web-awesome/test/components/ReportHeader.test.tsx b/packages/web-awesome/test/components/ReportHeader.test.tsx index 6ae89f86915..e50b6fbd20c 100644 --- a/packages/web-awesome/test/components/ReportHeader.test.tsx +++ b/packages/web-awesome/test/components/ReportHeader.test.tsx @@ -36,6 +36,10 @@ vi.mock("@/stores", () => ({ }), })); +const { currentEnvironment } = vi.hoisted(() => ({ currentEnvironment: { value: "" } })); + +vi.mock("@/stores/env", () => ({ currentEnvironment })); + vi.mock("@/utils/time", () => ({ timestampToDate: (value: number, options?: Intl.DateTimeFormatOptions) => `${options?.month === "long" ? "long" : "default"}:${value}`, @@ -43,6 +47,7 @@ vi.mock("@/utils/time", () => ({ beforeEach(() => { vi.clearAllMocks(); + currentEnvironment.value = ""; }); describe("components > ReportHeader", () => { @@ -74,4 +79,38 @@ describe("components > ReportHeader", () => { expect(screen.getByTestId("report-data")).toHaveTextContent("long:10"); }); + + it("should render the selected environment's launch start time instead of the report-wide one", () => { + (getReportOptions as Mock).mockReturnValue({ + reportName: "Wrike report", + createdAt: 42, + runSummary: { start: 1000, stop: 12_500, duration: 11_500 }, + runSummaryByEnv: { + staging: { start: 1000, stop: 3000, duration: 2000 }, + prod: { start: 10_000, stop: 12_500, duration: 2500 }, + }, + }); + currentEnvironment.value = "prod"; + + render(); + + // "long:1000" (the report-wide start) is not a substring of the rendered "long:10000". + expect(screen.getByTestId("report-data")).toHaveTextContent("long:10000"); + }); + + it("should fall back to generated time when the selected environment has no launch interval", () => { + (getReportOptions as Mock).mockReturnValue({ + reportName: "Wrike report", + createdAt: 42, + runSummary: { start: 1000, stop: 12_500, duration: 11_500 }, + runSummaryByEnv: { + staging: { start: 1000, stop: 3000, duration: 2000 }, + }, + }); + currentEnvironment.value = "prod"; + + render(); + + expect(screen.getByTestId("report-data")).toHaveTextContent("long:42"); + }); }); diff --git a/packages/web-awesome/test/components/TestResult/PwTraceButton.test.tsx b/packages/web-awesome/test/components/TestResult/PwTraceButton.test.tsx index 169fe305bef..47e495c6031 100644 --- a/packages/web-awesome/test/components/TestResult/PwTraceButton.test.tsx +++ b/packages/web-awesome/test/components/TestResult/PwTraceButton.test.tsx @@ -85,7 +85,7 @@ describe("components > TestResult > PwTraceButton", () => { expect(fetchFromUrl).toHaveBeenCalledTimes(1); expect(openModal).not.toHaveBeenCalled(); - }); + }, 15000); it("shows popup-blocked modal when new tab cannot be opened", async () => { const { fetchFromUrl, openModal, openPlaywrightTraceInNewTab } = await setup({ @@ -103,7 +103,7 @@ describe("components > TestResult > PwTraceButton", () => { title: "Playwright Trace Viewer | trace.zip", }), ); - }); + }, 15000); it("starts loading trace attachment after opening the popup", async () => { const { fetchFromUrl, openModal, openPlaywrightTraceInNewTab } = await setup({ @@ -117,5 +117,5 @@ describe("components > TestResult > PwTraceButton", () => { expect(openPlaywrightTraceInNewTab).toHaveBeenCalledTimes(1); expect(openModal).not.toHaveBeenCalled(); - }); + }, 15000); }); diff --git a/packages/web-awesome/test/components/Timeline.test.tsx b/packages/web-awesome/test/components/Timeline.test.tsx index 42ab616348c..cfc356791f9 100644 --- a/packages/web-awesome/test/components/Timeline.test.tsx +++ b/packages/web-awesome/test/components/Timeline.test.tsx @@ -108,5 +108,5 @@ describe("components > Timeline", () => { expect(screen.getByTestId("timeline-shared-host")).toHaveTextContent("tr-qa-a"); expect(screen.getByTestId("timeline-shared-host")).not.toHaveTextContent("tr-qa-b"); - }); + }, 15000); });