diff --git a/packages/core/src/report.ts b/packages/core/src/report.ts index 51421ccebeb..32d2d4b0f2a 100644 --- a/packages/core/src/report.ts +++ b/packages/core/src/report.ts @@ -95,6 +95,16 @@ const remoteReportParams = (ci: CiDescriptor | undefined): { repo?: string; bran const errorDetails = (err: unknown): string => (err instanceof Error ? (err.stack ?? err.message) : String(err)); +const getExecutorReportUrl = (executor: unknown): string | undefined => { + if (!executor || typeof executor !== "object" || !("reportUrl" in executor)) { + return undefined; + } + + const { reportUrl } = executor as { reportUrl?: unknown }; + + return typeof reportUrl === "string" && reportUrl.length > 0 ? reportUrl : undefined; +}; + const closeReadStream = async (stream: ReadStream): Promise => { if (stream.closed) { return; @@ -254,16 +264,32 @@ export class AllureReport { return this.#realtimeChannel.dispatcher; } + #resolveHistoryReportUrl = async (): Promise => { + if (this.reportUrl) { + return this.reportUrl; + } + + const executorReportUrl = getExecutorReportUrl(await this.#store.metadataByKey("allure2_executor")); + + if (executorReportUrl) { + this.reportUrl = executorReportUrl; + return executorReportUrl; + } + + return ""; + }; + #createHistoryDataPoint = async (): Promise => { const allTrs = await this.#store.allTestResults(); const allTcs = await this.#store.allTestCases(); + const historyReportUrl = await this.#resolveHistoryReportUrl(); return createHistory( this.reportUuid, this.reportName, allTcs, allTrs, - this.reportUrl, + historyReportUrl, await this.#store.allMetrics(), ); }; @@ -1109,6 +1135,8 @@ export class AllureReport { return; } + await this.#resolveHistoryReportUrl(); + // isolate logs of different reports dumps: done and summary await measurePerf(PERF_METRIC_NAMES.generatePluginsDone, async () => { await this.#eachPlugin(false, async (plugin, context) => { diff --git a/packages/core/test/report.test.ts b/packages/core/test/report.test.ts index 9c82c4840be..9b479c6bb0e 100644 --- a/packages/core/test/report.test.ts +++ b/packages/core/test/report.test.ts @@ -71,6 +71,12 @@ const createSignal = () => { return { promise, resolve }; }; +const readHistoryEntries = async (historyPath: string) => + (await readFile(historyPath, "utf-8")) + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line)); + const readPerfMetrics = async (output: string, reportUuid: string) => JSON.parse(await readFile(join(output, perfMetricsFileName(reportUuid)), "utf8")); @@ -362,6 +368,121 @@ describe("report", () => { expect(historyContent.startsWith(initialHistoryContent)).toBe(true); }); + it("should populate appended history urls from allure2 executor reportUrl", async () => { + const output = await mkdtemp(join(tmpdir(), "allure3-executor-history-url-")); + const historyPath = join(await mkdtemp(join(tmpdir(), "allure3-executor-history-url-data-")), "history.jsonl"); + const reportUrl = "https://jenkins.example/job/demo/42/allure"; + const config = await resolveConfig({ + name: "Allure Report", + output, + historyPath, + appendHistory: true, + }); + + const allureReport = new AllureReport(config); + + await allureReport.start(); + await allureReport.store.visitMetadata({ + allure2_executor: { + reportUrl, + }, + }); + await allureReport.store.visitTestResult( + { + uuid: "executor-history-url-result", + name: "AdditionWorks", + testId: "addition-works", + status: "passed", + }, + { readerId: "test" }, + ); + await allureReport.done(); + + const [historyPoint] = await readHistoryEntries(historyPath); + const [historyTestResult] = Object.values(historyPoint.testResults); + + expect(allureReport.reportUrl).toBe(reportUrl); + expect(historyPoint.url).toBe(reportUrl); + expect(historyTestResult).toEqual(expect.objectContaining({ url: reportUrl })); + }); + + it("should prefer plugin reportUrl over allure2 executor reportUrl for appended history", async () => { + const output = await mkdtemp(join(tmpdir(), "allure3-plugin-history-url-")); + const historyPath = join(await mkdtemp(join(tmpdir(), "allure3-plugin-history-url-data-")), "history.jsonl"); + const pluginReportUrl = "https://allure.example/reports/plugin"; + const executorReportUrl = "https://jenkins.example/job/demo/42/allure"; + const p1 = createPlugin("p1"); + const config = await resolveConfig({ + name: "Allure Report", + output, + historyPath, + appendHistory: true, + }); + + (p1.plugin.start as Mock).mockImplementation(async (context) => { + context.reportUrl = pluginReportUrl; + }); + (p1.plugin.done as Mock).mockImplementation(async (context) => { + await context.reportFiles.addFile("index.html", Buffer.from("index")); + }); + config.plugins = [p1]; + + const allureReport = new AllureReport(config); + + await allureReport.start(); + await allureReport.store.visitMetadata({ + allure2_executor: { + reportUrl: executorReportUrl, + }, + }); + await allureReport.store.visitTestResult( + { + uuid: "plugin-history-url-result", + name: "AdditionWorks", + testId: "addition-works", + status: "passed", + }, + { readerId: "test" }, + ); + await allureReport.done(); + + const [historyPoint] = await readHistoryEntries(historyPath); + const [historyTestResult] = Object.values(historyPoint.testResults); + + expect(allureReport.reportUrl).toBe(pluginReportUrl); + expect(historyPoint.url).toBe(pluginReportUrl); + expect(historyTestResult).toEqual(expect.objectContaining({ url: pluginReportUrl })); + }); + + it("should expose allure2 executor reportUrl to plugin done hooks when no plugin overrides it", async () => { + const output = await mkdtemp(join(tmpdir(), "allure3-plugin-context-executor-url-")); + const reportUrl = "https://jenkins.example/job/demo/42/allure"; + const p1 = createPlugin("p1"); + const config = await resolveConfig({ + name: "Allure Report", + output, + }); + let pluginDoneReportUrl: string | undefined; + + (p1.plugin.done as Mock).mockImplementation(async (context) => { + pluginDoneReportUrl = context.reportUrl; + }); + config.plugins = [p1]; + + const allureReport = new AllureReport(config); + + await allureReport.start(); + await allureReport.store.visitMetadata({ + allure2_executor: { + reportUrl, + }, + }); + await allureReport.done(); + + expect(pluginDoneReportUrl).toBe(reportUrl); + expect(allureReport.reportUrl).toBe(reportUrl); + }); + it("should read result directory files with bounded concurrency", async () => { const previousConcurrency = process.env.ALLURE_READ_CONCURRENCY; const resultsDir = await mkdtemp(join(tmpdir(), "allure3-read-directory-")); diff --git a/packages/web-awesome/src/components/TestResult/TrHistory/TrHistoryItem.tsx b/packages/web-awesome/src/components/TestResult/TrHistory/TrHistoryItem.tsx index a44ac2845a4..4dbe76c0f34 100644 --- a/packages/web-awesome/src/components/TestResult/TrHistory/TrHistoryItem.tsx +++ b/packages/web-awesome/src/components/TestResult/TrHistory/TrHistoryItem.tsx @@ -5,6 +5,7 @@ import { type FunctionalComponent } from "preact"; import { useMemo, useState } from "preact/hooks"; import type { ReportOptions } from "types"; +import { getHistoryNavigationUrl } from "@/components/TestResult/historyNavigation"; import { TrError } from "@/components/TestResult/TrError"; import { useI18n } from "@/stores"; import { timestampToDate } from "@/utils/time"; @@ -51,17 +52,8 @@ export const TrHistoryItem: FunctionalComponent = (props) => { const { t } = useI18n("controls"); const navigateUrl = useMemo(() => { - if (!url) { - return undefined; - } - - const { origin, pathname } = new URL(url); - const navUrl = new URL([pathname, reportOptions.id].join("/"), origin); - - navUrl.hash = id; - - return navUrl.toString(); - }, [id, url]); + return getHistoryNavigationUrl(url, reportOptions.id, id); + }, [id, reportOptions.id, url]); const renderExternalLink = () => { if (!navigateUrl) { @@ -71,7 +63,7 @@ export const TrHistoryItem: FunctionalComponent = (props) => { return ( = (props) => {
{Boolean(error) && ( - setIsOpen(!isOpened)}> - - + setIsOpen((value) => !value)} + /> )} {navigateUrl ? ( diff --git a/packages/web-awesome/src/components/TestResult/TrPrevStatuses/index.tsx b/packages/web-awesome/src/components/TestResult/TrPrevStatuses/index.tsx index e9dc96fb75f..6302ff5929b 100644 --- a/packages/web-awesome/src/components/TestResult/TrPrevStatuses/index.tsx +++ b/packages/web-awesome/src/components/TestResult/TrPrevStatuses/index.tsx @@ -4,6 +4,7 @@ import { SvgIcon, Text, TooltipWrapper, allureIcons } from "@allurereport/web-co import type { FunctionalComponent } from "preact"; import type { ReportOptions, ReportTestResult } from "types"; +import { getHistoryNavigationUrl } from "@/components/TestResult/historyNavigation"; import { useI18n } from "@/stores"; import { timestampToDate } from "@/utils/time"; @@ -11,8 +12,9 @@ import * as styles from "./styles.scss"; const TrPrevStatus: FunctionalComponent<{ item: HistoryTestResult }> = ({ item }) => { const reportOptions = getReportOptions(); + const navigateUrl = getHistoryNavigationUrl(item.url, reportOptions.id, item.id); - if (!item.url) { + if (!navigateUrl) { return (
@@ -20,13 +22,8 @@ const TrPrevStatus: FunctionalComponent<{ item: HistoryTestResult }> = ({ item } ); } - const { origin, pathname } = new URL(item.url); - const navigateUrl = new URL([pathname, reportOptions.id].join("/"), origin); - - navigateUrl.hash = item.id; - return ( - + ); diff --git a/packages/web-awesome/src/components/TestResult/historyNavigation.ts b/packages/web-awesome/src/components/TestResult/historyNavigation.ts new file mode 100644 index 00000000000..a62c1519e86 --- /dev/null +++ b/packages/web-awesome/src/components/TestResult/historyNavigation.ts @@ -0,0 +1,22 @@ +export const getHistoryNavigationUrl = ( + url: string | undefined, + reportId: string, + testResultId: string, +): string | undefined => { + if (!url) { + return undefined; + } + + try { + const navUrl = new URL(url); + const pathname = navUrl.pathname.endsWith("/") ? navUrl.pathname : `${navUrl.pathname}/`; + const lastSegment = pathname.slice(0, -1).split("/").pop(); + + navUrl.pathname = lastSegment === reportId ? pathname : `${pathname}${reportId}/`; + navUrl.hash = testResultId; + + return navUrl.toString(); + } catch { + return undefined; + } +}; diff --git a/packages/web-awesome/test/components/TestResult/TrHistoryItem.test.tsx b/packages/web-awesome/test/components/TestResult/TrHistoryItem.test.tsx new file mode 100644 index 00000000000..4e08f8406da --- /dev/null +++ b/packages/web-awesome/test/components/TestResult/TrHistoryItem.test.tsx @@ -0,0 +1,79 @@ +import type { HistoryTestResult } from "@allurereport/core-api"; +import { getReportOptions } from "@allurereport/web-commons"; +import { cleanup, render, screen } from "@testing-library/preact"; +import type { Mock } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { TrHistoryItem } from "@/components/TestResult/TrHistory/TrHistoryItem"; + +vi.mock("@allurereport/web-commons", async (importOriginal) => ({ + ...(await importOriginal()), + getReportOptions: vi.fn(), +})); + +vi.mock("@allurereport/web-components", () => ({ + ArrowButton: () => ( +