diff --git a/.pnp.cjs b/.pnp.cjs index f1d9a1ad787..adad8d6307f 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -26236,6 +26236,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "./packages/sandbox/",\ "packageDependencies": [\ ["sandbox", "workspace:packages/sandbox"],\ + ["@allurereport/plugin-awesome", "workspace:packages/plugin-awesome"],\ ["@allurereport/plugin-csv", "workspace:packages/plugin-csv"],\ ["@stylistic/eslint-plugin", "virtual:e545774f2ccef2393aca5c009a358532c03f065393263f9cbb3ab67366c2879624d5c1730fe5313387f3f2857386a5904c6581787f320ce570367a0ce47cf7b2#npm:2.11.0"],\ ["@types/eslint", "npm:8.56.12"],\ diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index 7656a91b5c8..ae96a976039 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -46,6 +46,5 @@ export interface FullConfig { url?: string; project?: string; accessToken?: string; - publish?: boolean; }; } diff --git a/packages/core/src/report.ts b/packages/core/src/report.ts index f3fc1041bef..c19aead4368 100644 --- a/packages/core/src/report.ts +++ b/packages/core/src/report.ts @@ -7,7 +7,6 @@ import type { ReportFiles, ResultFile, } from "@allurereport/plugin-api"; -import AwesomePlugin from "@allurereport/plugin-awesome"; import { allure1, allure2, attachments, cucumberjson, junitXml, readXcResultBundle } from "@allurereport/reader"; import { PathResultFile, type ResultsReader } from "@allurereport/reader-api"; import { AllureRemoteHistory, AllureServiceClient, KnownError, UnknownError } from "@allurereport/service"; @@ -43,7 +42,7 @@ export class AllureReport { readonly #output: string; readonly #history: AllureHistory | undefined; readonly #allureServiceClient: AllureServiceClient | undefined; - readonly #publish: boolean; + #reportUrl?: string; #state?: Record; #stage: "init" | "running" | "done" = "init"; @@ -66,7 +65,6 @@ export class AllureReport { } = opts; this.#allureServiceClient = allureServiceConfig?.url ? new AllureServiceClient(allureServiceConfig) : undefined; - this.#publish = allureServiceConfig?.publish ?? false; this.#reportUuid = randomUUID(); this.#reportName = name; this.#eventEmitter = new EventEmitter(); @@ -111,6 +109,10 @@ export class AllureReport { return this.#qualityGate.result; } + get #publish() { + return this.#plugins.some(({ enabled, options }) => enabled && options.publish); + } + readDirectory = async (resultsDir: string) => { if (this.#stage !== "running") { throw new Error(initRequired); @@ -216,28 +218,28 @@ export class AllureReport { // closing it early, to prevent future reads this.#stage = "done"; - await this.#eachPlugin(false, async (plugin, context, id) => { - const pluginFiles = (await context.state.get("files")) ?? {}; - + await this.#eachPlugin(false, async (plugin, context, { id, publish }) => { await plugin.done?.(context, this.#store); - // publish only Allure Awesome reports - if ( - plugin instanceof AwesomePlugin && - this.#history && - this.#allureServiceClient && - this.#publish && - Object.keys(pluginFiles).length - ) { - await Promise.all( - Object.entries(pluginFiles).map(([key, filepath]) => - this.#allureServiceClient?.addReportFile({ + if (this.#allureServiceClient && publish) { + const pluginFiles = (await context.state.get("files")) ?? {}; + + for (const [filename, filepath] of Object.entries(pluginFiles)) { + // publish data-files separately + if (/^(data|widgets|index\.html$)/.test(filename)) { + this.#allureServiceClient.addReportFile({ reportUuid: this.#reportUuid, - key, + pluginId: id, + filename, + filepath, + }); + } else { + this.#allureServiceClient.addReportAsset({ + filename, filepath, - }), - ), - ); + }); + } + } } const summary = await plugin?.info?.(context, this.#store); @@ -313,20 +315,18 @@ export class AllureReport { #eachPlugin = async ( initState: boolean, - consumer: (plugin: Plugin, context: PluginContext, id: string) => Promise, + consumer: (plugin: Plugin, context: PluginContext, options: { id: string; publish: boolean }) => Promise, ) => { if (initState) { // reset state on start; this.#state = {}; } - for (const descriptor of this.#plugins) { - if (!descriptor.enabled) { + for (const { enabled, id, plugin, options } of this.#plugins) { + if (!enabled) { continue; } - const id = descriptor.id; - const plugin = descriptor.plugin; const pluginState = this.#getPluginState(initState, id); if (!pluginState) { @@ -357,7 +357,7 @@ export class AllureReport { }; try { - await consumer.call(this, plugin, pluginContext, id); + await consumer.call(this, plugin, pluginContext, { id, publish: !!options?.publish }); if (initState) { this.#state![id] = pluginState; diff --git a/packages/e2e/test/utils/index.ts b/packages/e2e/test/utils/index.ts index 68663bbd2e6..f2de434bdc3 100644 --- a/packages/e2e/test/utils/index.ts +++ b/packages/e2e/test/utils/index.ts @@ -4,11 +4,11 @@ import AwesomePlugin from "@allurereport/plugin-awesome"; import { serve } from "@allurereport/static-server"; import type { TestResult } from "allure-js-commons"; import { FileSystemWriter, ReporterRuntime } from "allure-js-commons/sdk/reporter"; +import { randomUUID } from "node:crypto"; +import { existsSync } from "node:fs"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { resolve } from "node:path"; -import { existsSync } from "node:fs"; -import { randomUUID } from "node:crypto"; export type GeneratorParams = { history?: HistoryDataPoint[]; @@ -37,13 +37,15 @@ export const randomNumber = (min: number, max: number): number => { export const generateReport = async (payload: GeneratorParams) => { const { reportConfig, rootDir, reportDir, resultsDir, testResults, attachments = [], history = [] } = payload; + + const hasHistory = history.length > 0; const historyPath = resolve(rootDir, `history-${randomUUID()}.jsonl`); if (!existsSync(historyPath)) { await writeFile(historyPath, ""); } - if (history.length > 0) { + if (hasHistory) { await writeFile(historyPath, history.map((item) => JSON.stringify(item)).join("\n"), { encoding: "utf-8" }); } @@ -51,7 +53,7 @@ export const generateReport = async (payload: GeneratorParams) => { ...reportConfig, output: reportDir, reportFiles: new FileSystemReportFiles(reportDir), - historyPath, + historyPath: hasHistory ? historyPath : undefined, }); const runtime = new ReporterRuntime({ writer: new FileSystemWriter({ diff --git a/packages/plugin-api/src/config.ts b/packages/plugin-api/src/config.ts index 18d670dc1b3..9a611193a78 100644 --- a/packages/plugin-api/src/config.ts +++ b/packages/plugin-api/src/config.ts @@ -43,7 +43,6 @@ export interface Config { url?: string; project?: string; accessToken?: string; - publish?: boolean; }; } diff --git a/packages/plugin-awesome/src/model.ts b/packages/plugin-awesome/src/model.ts index 18dbe454f9f..5eabe8ead74 100644 --- a/packages/plugin-awesome/src/model.ts +++ b/packages/plugin-awesome/src/model.ts @@ -19,6 +19,7 @@ export type AwesomeOptions = { charts?: ChartOptions[]; sections?: string[]; defaultSection?: string; + publish?: boolean; }; export type TemplateManifest = Record; diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 964e27538e7..13db0f160f5 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -12,6 +12,7 @@ "t": "yarn allure run -- vitest run" }, "devDependencies": { + "@allurereport/plugin-awesome": "workspace:*", "@allurereport/plugin-csv": "workspace:*", "@stylistic/eslint-plugin": "^2.6.1", "@types/eslint": "^8.56.11", diff --git a/packages/service/src/index.ts b/packages/service/src/index.ts index 4427cbd3ad4..12c76627861 100644 --- a/packages/service/src/index.ts +++ b/packages/service/src/index.ts @@ -1,3 +1,4 @@ +export type * from "./model.js"; export * from "./service.js"; export * from "./history.js"; export { KnownError, UnknownError } from "./utils/http.js"; diff --git a/packages/service/src/service.ts b/packages/service/src/service.ts index 62a68ada27b..40c0150c54a 100644 --- a/packages/service/src/service.ts +++ b/packages/service/src/service.ts @@ -161,19 +161,74 @@ export class AllureServiceClient { return this.#client.post<{ url: string }>("/api/reports/create", { body: { project: this.project, - name: reportName, + reportName, + reportUuid, + }, + }); + } + + /** + * Marks report as a completed one + * Use when all report files have been uploaded + * @param payload + */ + async completeReport(payload: { reportUuid: string }) { + const { reportUuid } = payload; + + if (!this.project) { + throw new Error("Project is not set"); + } + + return this.#client.post("/api/reports/complete", { + body: { id: reportUuid, }, }); } + /** + * Uploads report asset which can be shared between multiple reports at once + * @param payload + */ + async addReportAsset(payload: { filename: string; file?: Buffer; filepath?: string }) { + const { filename, file, filepath } = payload; + + if (!file && !filepath) { + throw new Error("File or filepath is required"); + } + + let content = file; + + if (!content) { + content = await readFile(filepath!); + } + + const form = new FormData(); + + form.set("filename", filename); + form.set("file", content); + + return this.#client.post("/api/assets/upload", { + body: form, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + } + /** * Adds a file to an existing report * If the report doesn't exist, it will be created * @param payload */ - async addReportFile(payload: { reportUuid: string; key: string; file?: Buffer; filepath?: string }) { - const { reportUuid, key, file, filepath } = payload; + async addReportFile(payload: { + reportUuid: string; + pluginId: string; + filename: string; + file?: Buffer; + filepath?: string; + }) { + const { reportUuid, filename, file, filepath, pluginId } = payload; if (!file && !filepath) { throw new Error("File or filepath is required"); @@ -187,8 +242,8 @@ export class AllureServiceClient { const form = new FormData(); - form.set("report", reportUuid); - form.set("filename", key); + form.set("reportUuid", reportUuid); + form.set("filename", joinPosix(pluginId, filename)); form.set("file", content); await this.#client.post("/api/reports/upload", { @@ -198,6 +253,6 @@ export class AllureServiceClient { }, }); - return joinPosix(this.#url, reportUuid, key); + return joinPosix(this.#url, reportUuid, filename); } } diff --git a/packages/service/test/service.test.ts b/packages/service/test/service.test.ts index c85b61c0c9f..7bee88e00e5 100644 --- a/packages/service/test/service.test.ts +++ b/packages/service/test/service.test.ts @@ -24,7 +24,8 @@ const fixtures = { name: "test", } as HistoryDataPoint, report: "report", - key: "key", + filename: "filename", + pluginId: "sample", }; const open = await import("open"); @@ -217,9 +218,9 @@ describe("AllureServiceClient", () => { expect(HttpClientMock.prototype.post).toHaveBeenCalledWith("/api/reports/create", { body: { - id: fixtures.report, project: fixtures.project, - name: fixtures.report, + reportUuid: fixtures.report, + reportName: fixtures.report, }, }); expect(res).toEqual({ id: fixtures.report, name: fixtures.report }); @@ -228,21 +229,13 @@ describe("AllureServiceClient", () => { describe("addReportFile", () => { it("should throw an error unless a file or filepath is provided", async () => { - await expect(serviceClient.addReportFile({ reportUuid: fixtures.report, key: fixtures.key })).rejects.toThrow( - "File or filepath is required", - ); - }); - - it("should throw an error if the provided filename points to a non-existing file", async () => { - (readFile as MockedFunction).mockRejectedValue(new Error("File not found")); - await expect( serviceClient.addReportFile({ reportUuid: fixtures.report, - key: fixtures.key, - filepath: "not-existing-file.txt", + pluginId: fixtures.pluginId, + filename: fixtures.filename, }), - ).rejects.toThrow("File not found"); + ).rejects.toThrow("File or filepath is required"); }); it("should upload a given file", async () => { @@ -250,14 +243,41 @@ describe("AllureServiceClient", () => { const res = await serviceClient.addReportFile({ reportUuid: fixtures.report, - key: fixtures.key, + pluginId: fixtures.pluginId, + filename: fixtures.filename, file: Buffer.from("test"), }); const form = new FormData(); - form.set("report", fixtures.report); - form.set("filename", fixtures.key); + form.set("reportUuid", fixtures.report); + form.set("filename", joinPosix(fixtures.pluginId, fixtures.filename)); + form.set("file", Buffer.from("test") as unknown as Blob); + + expect(HttpClientMock.prototype.post).toHaveBeenCalledWith("/api/reports/upload", { + body: form, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + expect(res).toEqual(joinPosix(fixtures.url, fixtures.report, fixtures.filename)); + }); + + it("should upload a file from a filepath", async () => { + (readFile as MockedFunction).mockResolvedValue(Buffer.from("test")); + HttpClientMock.prototype.post.mockResolvedValue({}); + + const res = await serviceClient.addReportFile({ + reportUuid: fixtures.report, + pluginId: fixtures.pluginId, + filename: fixtures.filename, + filepath: "test.txt", + }); + + const form = new FormData(); + + form.set("reportUuid", fixtures.report); + form.set("filename", joinPosix(fixtures.pluginId, fixtures.filename)); form.set("file", Buffer.from("test") as unknown as Blob); expect(HttpClientMock.prototype.post).toHaveBeenCalledWith("/api/reports/upload", { @@ -266,32 +286,58 @@ describe("AllureServiceClient", () => { "Content-Type": "multipart/form-data", }, }); - expect(res).toEqual(joinPosix(fixtures.url, fixtures.report, fixtures.key)); + expect(res).toEqual(joinPosix(fixtures.url, fixtures.report, fixtures.filename)); }); }); - it("should upload a file from a filepath", async () => { - (readFile as MockedFunction).mockResolvedValue(Buffer.from("test")); - HttpClientMock.prototype.post.mockResolvedValue({}); + describe("addReportAsset", () => { + it("should throw an error unless a file or filepath is provided", async () => { + await expect(serviceClient.addReportAsset({ filename: fixtures.filename })).rejects.toThrow( + "File or filepath is required", + ); + }); + + it("should upload a given file", async () => { + HttpClientMock.prototype.post.mockResolvedValue({}); - const res = await serviceClient.addReportFile({ - reportUuid: fixtures.report, - key: fixtures.key, - filepath: "test.txt", + const res = await serviceClient.addReportAsset({ + filename: fixtures.filename, + file: Buffer.from("test"), + }); + + const form = new FormData(); + form.set("filename", fixtures.filename); + form.set("file", Buffer.from("test") as unknown as Blob); + + expect(HttpClientMock.prototype.post).toHaveBeenCalledWith("/api/assets/upload", { + body: form, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + expect(res).toEqual({}); }); - const form = new FormData(); + it("should upload a file from a filepath", async () => { + (readFile as MockedFunction).mockResolvedValue(Buffer.from("test")); + HttpClientMock.prototype.post.mockResolvedValue({}); - form.set("report", fixtures.report); - form.set("filename", fixtures.key); - form.set("file", Buffer.from("test") as unknown as Blob); + const res = await serviceClient.addReportAsset({ + filename: fixtures.filename, + filepath: "test.txt", + }); - expect(HttpClientMock.prototype.post).toHaveBeenCalledWith("/api/reports/upload", { - body: form, - headers: { - "Content-Type": "multipart/form-data", - }, + const form = new FormData(); + form.set("filename", fixtures.filename); + form.set("file", Buffer.from("test") as unknown as Blob); + + expect(HttpClientMock.prototype.post).toHaveBeenCalledWith("/api/assets/upload", { + body: form, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + expect(res).toEqual({}); }); - expect(res).toEqual(joinPosix(fixtures.url, fixtures.report, fixtures.key)); }); }); diff --git a/packages/web-allure2/webpack.config.js b/packages/web-allure2/webpack.config.js index 99c77a5a376..c05937a2736 100644 --- a/packages/web-allure2/webpack.config.js +++ b/packages/web-allure2/webpack.config.js @@ -11,12 +11,13 @@ const { SINGLE_FILE_MODE } = env; const __dirname = dirname(fileURLToPath(import.meta.url)); export default (env, argv) => { + const devMode = argv?.mode === "development"; const config = { entry: "./src/index.js", output: { path: join(__dirname, SINGLE_FILE_MODE ? "dist/single" : "dist/multi"), - filename: "app-[fullhash:8].js", - assetModuleFilename: `[name]-[fullhash:8][ext]`, + filename: devMode ? "app.js" : "app-[fullhash].js", + assetModuleFilename: `[name][ext]`, }, module: { rules: [ @@ -61,12 +62,6 @@ export default (env, argv) => { test: /translations\/\D+\.json$/, type: "asset/source", }, - // FIXME: how can we solve the problem with svg in css? - // { - // test: /\.svg$/, - // type: "asset/inline", - // resourceQuery: /inline/, - // }, { test: /\.svg$/, loader: "svg-sprite-loader", @@ -93,7 +88,7 @@ export default (env, argv) => { }, }), new MiniCssExtractPlugin({ - filename: "styles-[fullhash:8].css", + filename: devMode ? "styles.css" : "styles-[contenthash].css", }), new SpriteLoaderPlugin(), new WebpackManifestPlugin({ diff --git a/packages/web-awesome/webpack.config.js b/packages/web-awesome/webpack.config.js index 4811c61efb2..47713374504 100644 --- a/packages/web-awesome/webpack.config.js +++ b/packages/web-awesome/webpack.config.js @@ -19,7 +19,7 @@ export default (env, argv) => { output: { path: join(baseDir, SINGLE_FILE_MODE ? "dist/single" : "dist/multi"), filename: devMode ? "app.js" : "app-[fullhash].js", - assetModuleFilename: devMode ? `[name].[ext]` : `[name]-[contenthash].[ext]`, + assetModuleFilename: `[name][ext]`, }, devtool: devMode ? "inline-source-map" : false, optimization: { diff --git a/packages/web-classic/webpack.config.js b/packages/web-classic/webpack.config.js index c80bf1a564f..c096f375ea8 100644 --- a/packages/web-classic/webpack.config.js +++ b/packages/web-classic/webpack.config.js @@ -16,8 +16,8 @@ export default (env, argv) => { entry: "./src/index.tsx", output: { path: join(baseDir, SINGLE_FILE_MODE ? "dist/single" : "dist/multi"), - filename: devMode ? "app.js" : "app-[fullhash:8].js", - assetModuleFilename: devMode ? `[name].[ext]` : `[name]-[fullhash:8].[ext]`, + filename: devMode ? "app.js" : "app-[fullhash].js", + assetModuleFilename: `[name][ext]`, }, devtool: devMode ? "inline-source-map" : false, module: { @@ -72,7 +72,7 @@ export default (env, argv) => { DEVELOPMENT: devMode, }), new MiniCssExtractPlugin({ - filename: devMode ? "styles.css" : "styles-[fullhash:8].css", + filename: devMode ? "styles.css" : "styles-[contenthash].css", }), new SpriteLoaderPlugin(), new WebpackManifestPlugin({ diff --git a/packages/web-dashboard/webpack.config.js b/packages/web-dashboard/webpack.config.js index 3027d3bb070..cd3a0fd1f84 100644 --- a/packages/web-dashboard/webpack.config.js +++ b/packages/web-dashboard/webpack.config.js @@ -17,8 +17,8 @@ export default (env, argv) => { entry: "./src/index.tsx", output: { path: join(baseDir, SINGLE_FILE_MODE ? "dist/single" : "dist/multi"), - filename: devMode ? "app.js" : "app-[fullhash:8].js", - assetModuleFilename: devMode ? `[name].[ext]` : `[name]-[fullhash:8].[ext]`, + filename: devMode ? "app.js" : "app-[fullhash].js", + assetModuleFilename: `[name][ext]`, }, devtool: devMode ? "inline-source-map" : false, module: { @@ -74,7 +74,7 @@ export default (env, argv) => { DEVELOPMENT: devMode, }), new MiniCssExtractPlugin({ - filename: devMode ? "styles.css" : "styles-[fullhash:8].css", + filename: devMode ? "styles.css" : "styles-[contenthash].css", }), new SpriteLoaderPlugin(), new WebpackManifestPlugin({ diff --git a/packages/web-summary/webpack.config.js b/packages/web-summary/webpack.config.js index 9fe8ff50c55..1c84b05b1aa 100644 --- a/packages/web-summary/webpack.config.js +++ b/packages/web-summary/webpack.config.js @@ -2,7 +2,6 @@ import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"; import HtmlWebpackPlugin from "html-webpack-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import { dirname, join } from "node:path"; -import { env } from "node:process"; import { fileURLToPath } from "node:url"; import SpriteLoaderPlugin from "svg-sprite-loader/plugin.js"; import webpack from "webpack"; @@ -16,8 +15,8 @@ export default (env, argv) => { entry: "./src/index.tsx", output: { path: join(baseDir, "dist"), - filename: devMode ? "app.js" : "app-[fullhash:8].js", - assetModuleFilename: devMode ? `[name].[ext]` : `[name]-[fullhash:8].[ext]`, + filename: devMode ? "app.js" : "app-[fullhash].js", + assetModuleFilename: `[name][ext]`, }, devtool: devMode ? "inline-source-map" : false, module: { @@ -73,7 +72,7 @@ export default (env, argv) => { DEVELOPMENT: devMode, }), new MiniCssExtractPlugin({ - filename: devMode ? "styles.css" : "styles-[fullhash:8].css", + filename: devMode ? "styles.css" : "styles-[contenthash].css", }), new SpriteLoaderPlugin(), new WebpackManifestPlugin({ diff --git a/yarn.lock b/yarn.lock index 915e01d1c38..853cd3f4d15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17733,6 +17733,7 @@ __metadata: version: 0.0.0-use.local resolution: "sandbox@workspace:packages/sandbox" dependencies: + "@allurereport/plugin-awesome": "workspace:*" "@allurereport/plugin-csv": "workspace:*" "@stylistic/eslint-plugin": "npm:^2.6.1" "@types/eslint": "npm:^8.56.11"