From d063d235ca6575f07b9a369615cd41309ab1b877 Mon Sep 17 00:00:00 2001 From: Thomas Winkler Date: Tue, 29 Oct 2024 22:02:01 +0100 Subject: [PATCH] New cli option clear. Overwrite implemented also for run. Refactoring. --- src/lib/screenshots/types.ts | 1 + src/plugin/index.spec.ts | 53 ++++++++++++- src/plugin/index.ts | 43 ++++++++--- src/screenshot/config.ts | 12 ++- src/screenshot/helper.spec.ts | 120 +++++++++++++++++++++++++++++- src/screenshot/helper.ts | 73 +++++++++++++++++- src/screenshot/runner.ts | 4 +- src/screenshot/startup.ts | 65 ++++++---------- src/shared/c8ypact/fileadapter.ts | 2 +- 9 files changed, 315 insertions(+), 58 deletions(-) diff --git a/src/lib/screenshots/types.ts b/src/lib/screenshots/types.ts index d4c2942..7cdd334 100644 --- a/src/lib/screenshots/types.ts +++ b/src/lib/screenshots/types.ts @@ -371,6 +371,7 @@ export interface C8yScreenshotOptions { quiet: boolean; setup: ScreenshotSetup; init: boolean; + clear: boolean; } export type C8yScreenshotActionHandler = ( diff --git a/src/plugin/index.spec.ts b/src/plugin/index.spec.ts index e2164fe..d6dc610 100644 --- a/src/plugin/index.spec.ts +++ b/src/plugin/index.spec.ts @@ -1,9 +1,16 @@ /// -import { C8yPactDefaultFileAdapter, configureC8yPlugin } from "./index"; +import { + appendCountIfPathExists, + C8yPactDefaultFileAdapter, + configureC8yPlugin, +} from "./index"; import path from "path"; +import { vol } from "memfs"; jest.spyOn(process, "cwd").mockReturnValue("/home/user/test"); +// eslint-disable-next-line @typescript-eslint/no-var-requires +jest.mock("fs", () => require("memfs").fs); describe("plugin", () => { describe("configurePlugin ", () => { @@ -61,4 +68,48 @@ describe("plugin", () => { expect((config.env as any).C8Y_PLUGIN_LOADED).toBe("true"); }); }); + + describe("appendCountIfPathExists", () => { + beforeEach(() => { + vol.fromNestedJSON({ + "/home/user/test/cypress/screenshots": { + "my-screenshot-05.png": Buffer.from([8, 6, 7, 5, 3, 0, 9]), + "my-screenshot-04.png": Buffer.from([8, 6, 7, 5, 3, 0, 9]), + "my-screenshot-04 (2).png": Buffer.from([8, 6, 7, 5, 3, 0, 9]), + "my-screenshot-04 (3).png": Buffer.from([8, 6, 7, 5, 3, 0, 9]), + }, + }); + }); + + afterEach(() => { + vol.reset(); + }); + + it("should append count if path exists", () => { + const result = appendCountIfPathExists( + "/home/user/test/cypress/screenshots/my-screenshot-05.png" + ); + expect(result).toBe( + "/home/user/test/cypress/screenshots/my-screenshot-05 (2).png" + ); + }); + + it("should not append count if path does not exists", () => { + const result = appendCountIfPathExists( + "/home/user/test/cypress/screenshots/my-screenshot-01.png" + ); + expect(result).toBe( + "/home/user/test/cypress/screenshots/my-screenshot-01.png" + ); + }); + + it("should increase count", () => { + const result = appendCountIfPathExists( + "/home/user/test/cypress/screenshots/my-screenshot-04.png" + ); + expect(result).toBe( + "/home/user/test/cypress/screenshots/my-screenshot-04 (4).png" + ); + }); + }); }); diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 24dc30e..9362f5d 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -45,7 +45,7 @@ export function configureC8yPlugin( config: Cypress.PluginConfigOptions, options: C8yPluginConfig = {} ) { - const log = debug("c8y:c8yscrn:plugin"); + const log = debug("c8y:plugin"); let adapter = options.pactAdapter; if (!adapter) { @@ -122,7 +122,7 @@ export function configureC8yPlugin( "c8ypact:save": savePact, "c8ypact:get": getPact, "c8ypact:remove": removePact, - "c8ypact:oauthLogin": login, + "c8ypact:oauthLogin": login }); } } @@ -141,9 +141,9 @@ export function configureC8yScreenshotPlugin( ) { const log = debug("c8y:scrn:plugin"); let configData: string | ScreenshotSetup | undefined = setup; - if (config.env._c8yscrnyaml != null) { - log(`Using config from _c8yscrnyaml`); - configData = config.env._c8yscrnyaml; + if (config.env._c8yscrnConfigYaml != null) { + log(`Using config from _c8yscrnConfigYaml`); + configData = config.env._c8yscrnConfigYaml; } let lookupPaths: string[] = []; @@ -186,6 +186,9 @@ export function configureC8yScreenshotPlugin( ); } + const ajv = new C8yAjvSchemaMatcher(); + ajv.match(configData, schema, true); + if (configData.global?.timeouts?.default) { config.defaultCommandTimeout = configData.global.timeouts.default; log(`Setting default command timeout to ${config.defaultCommandTimeout}`); @@ -199,17 +202,20 @@ export function configureC8yScreenshotPlugin( log(`Setting screenshot timeout to ${config.responseTimeout}`); } - const ajv = new C8yAjvSchemaMatcher(); - ajv.match(configData, schema, true); log( `Config validated. ${configData.screenshots?.length} screenshots configured.` ); - config.env._c8yscrnyaml = configData; + const overwrite = configData.global?.overwrite ?? false; + + config.env._c8yscrnConfigYaml = configData; config.baseUrl = config.baseUrl ?? configData?.baseUrl ?? "http://localhost:8080"; log(`Using baseUrl ${config.baseUrl}`); + const screenshotsFolder = config.env._c8yscrnBrowserLaunchArgs ?? "c8yscrn"; + log(`Using screenshotsFolder to ${screenshotsFolder}`); + // https://www.cypress.io/blog/generate-high-resolution-videos-and-screenshots // https://github.com/cypress-io/cypress/issues/27260 on("before:browser:launch", (browser, launchOptions) => { @@ -271,8 +277,12 @@ export function configureC8yScreenshotPlugin( dimensions: details.dimensions, }); } - log(`Moving screenshot ${details.path} to ${newPath}`); - fs.rename(details.path, newPath, (err) => { + + // for Module API run(), overwrite option of the screenshot is not working + const targetPath = overwrite === true ? newPath : appendCountIfPathExists(newPath); + log(`Moving screenshot ${details.path} to ${targetPath} (overwrite: ${overwrite})`); + + fs.rename(details.path, targetPath, (err) => { if (err) return reject(err); resolve({ path: newPath, @@ -286,6 +296,19 @@ export function configureC8yScreenshotPlugin( return config; } +export function appendCountIfPathExists(newPath: string): string { + let count = 2; + let adjustedPath = newPath; + + while (fs.existsSync(adjustedPath)) { + const parsedPath = path.parse(newPath); + adjustedPath = path.join(parsedPath.dir, `${parsedPath.name} (${count})${parsedPath.ext}`); + count++; + } + + return adjustedPath; +} + function getVersion() { try { let currentDir = __dirname; diff --git a/src/screenshot/config.ts b/src/screenshot/config.ts index 3d8cf1d..6fbbe02 100644 --- a/src/screenshot/config.ts +++ b/src/screenshot/config.ts @@ -1,15 +1,23 @@ import { defineConfig } from "cypress"; import { configureC8yScreenshotPlugin } from "../plugin"; +import debug from "debug"; +const log = debug("c8y:scrn:cypress"); + export default defineConfig({ e2e: { baseUrl: "http://localhost:4200", supportFile: false, video: false, - videosFolder: "videos", - screenshotsFolder: "screenshots", setupNodeEvents(on, config) { configureC8yScreenshotPlugin(on, config); + on("task", { + "debug": (message: string) => { + log(message); + return null; + } + }); + return config; }, }, diff --git a/src/screenshot/helper.spec.ts b/src/screenshot/helper.spec.ts index 62029a7..5d7b31f 100644 --- a/src/screenshot/helper.spec.ts +++ b/src/screenshot/helper.spec.ts @@ -3,7 +3,11 @@ import * as yaml from "yaml"; import { C8yAjvSchemaMatcher } from "../contrib/ajv"; -import { createInitConfig } from "./helper"; +import { + createInitConfig, + resolveConfigOptions, + resolveScreenshotFolder, +} from "./helper"; import schema from "./../screenshot/schema.json"; jest.spyOn(process, "cwd").mockReturnValue("/home/user/test"); @@ -20,4 +24,118 @@ describe("startup", () => { }).not.toThrow(); }); }); + + describe("resolveScreenshotFolder", () => { + it("should throw error for current working directory", () => { + expect(() => { + resolveScreenshotFolder({ + folder: ".", + }); + }).toThrow( + `Please provide a screenshot folder path that does not resolve to the current working directory.` + ); + }); + + it("should throw error for current working directory with absolute path", () => { + expect(() => { + resolveScreenshotFolder({ + // use trailing slash + folder: "/home/user/test/", + }); + }).toThrow( + `Please provide a screenshot folder path that does not resolve to the current working directory.` + ); + }); + + it("should throw error for current working directory with path operations", () => { + expect(() => { + resolveScreenshotFolder({ + // use trailing slash + folder: "/home/../home/user/test/", + }); + }).toThrow( + `Please provide a screenshot folder path that does not resolve to the current working directory.` + ); + }); + + it("should return folder for current working directory", () => { + expect(resolveScreenshotFolder({ folder: "c8yscrn" })).toBe( + "/home/user/test/c8yscrn" + ); + }); + + it("should return absolute folder", () => { + expect( + resolveScreenshotFolder({ folder: "/my/path/to/c8yscrn/folder" }) + ).toBe("/my/path/to/c8yscrn/folder"); + }); + + it("should return folder for folder hierarchy", () => { + expect(resolveScreenshotFolder({ folder: "my/c8yscrn/folder" })).toBe( + "/home/user/test/my/c8yscrn/folder" + ); + }); + }); + + describe("resolveConfigOptions", () => { + it("should return default options", () => { + const options = resolveConfigOptions({}); + expect(options.browser).toBe("chrome"); + expect(options.quiet).toBe(true); + expect(options.testingType).toBe("e2e"); + expect(options.config.e2e.baseUrl).toBe("http://localhost:8080"); + expect(options.config.e2e.screenshotsFolder).toBe( + "/home/user/test/c8yscrn" + ); + expect(options.config.e2e.trashAssetsBeforeRuns).toBe(false); + expect(options.config.e2e.specPattern.endsWith(".ts")).toBe(true); + expect(options.configFile.endsWith(".ts")).toBe(true); + }); + + it("should return custom options", () => { + const options = resolveConfigOptions({ + browser: "firefox", + clear: true, + folder: "my/c8yscrn/folder", + tags: ["tag1", "tag2"], + baseUrl: "http://localhost:4200", + }); + expect(options.browser).toBe("firefox"); + expect(options.quiet).toBe(true); + expect(options.testingType).toBe("e2e"); + expect(options.config.e2e.baseUrl).toBe("http://localhost:4200"); + expect(options.config.e2e.screenshotsFolder).toBe( + "/home/user/test/my/c8yscrn/folder" + ); + expect(options.config.e2e.trashAssetsBeforeRuns).toBe(true); + expect(options.config.e2e.specPattern.endsWith(".ts")).toBe(true); + expect(options.configFile.endsWith(".ts")).toBe(true); + }); + }); + + describe("resolveBaseUrl", () => { + const originalEnv = process.env; + beforeAll(() => { + process.env = { + ...originalEnv, + C8Y_BASEURL: "http://localhost:4200", + }; + }); + afterAll(() => { + process.env = originalEnv; + }); + + it("should return custom base url", () => { + expect( + resolveConfigOptions({ baseUrl: "http://localhost:4200" }).config.e2e + .baseUrl + ).toBe("http://localhost:4200"); + }); + + it("should return base url from env", () => { + expect(resolveConfigOptions({}).config.e2e.baseUrl).toBe( + "http://localhost:4200" + ); + }); + }); }); diff --git a/src/screenshot/helper.ts b/src/screenshot/helper.ts index a8a5ee7..0a79e8d 100644 --- a/src/screenshot/helper.ts +++ b/src/screenshot/helper.ts @@ -1,5 +1,8 @@ import * as yaml from "yaml"; import * as fs from "fs"; +import * as path from "path"; + +import { C8yScreenshotOptions } from "cumulocity-cypress/lib/screenshots/types"; export function readYamlFile(filePath: string): any { const fileContent = fs.readFileSync(filePath, "utf-8"); @@ -29,4 +32,72 @@ screenshots: tags: - cockpit `; -} \ No newline at end of file +} + +export function resolveScreenshotFolder( + args: Partial +): string { + const screenshotsFolder = path.resolve( + process.cwd(), + args.folder ?? "c8yscrn" + ); + if (screenshotsFolder == process.cwd()) { + throw new Error( + `Please provide a screenshot folder path that does not resolve to the current working directory.` + ); + } + + return screenshotsFolder; +} + +export function resolveConfigOptions(args: Partial): any { + const browser = (args.browser ?? process.env.C8Y_BROWSER ?? "chrome") + .toLowerCase() + .trim(); + if (!["chrome", "firefox", "electron"].includes(browser)) { + throw new Error( + `Invalid browser ${browser}. Supported browsers are chrome, firefox, electron.` + ); + } + + // might run in different environments, so we need to find the correct extension + // this is required when running in development mode from ts files + const fileExtension = resolveFileExtension(); + const cypressConfigFile = path.resolve( + path.dirname(__filename), + `config.${fileExtension}` + ); + + const screenshotsFolder = resolveScreenshotFolder(args); + const baseUrl = resolveBaseUrl(args); + + return { + configFile: cypressConfigFile, + browser, + testingType: "e2e" as const, + quiet: args.quiet ?? true, + config: { + e2e: { + baseUrl, + screenshotsFolder, + trashAssetsBeforeRuns: args.clear ?? false, + specPattern: path.join( + path.dirname(__filename), + `*.cy.${fileExtension}` + ), + }, + }, + }; +} + +export function resolveFileExtension(): string { + let fileExtension = __filename?.split(".")?.pop(); + if (!fileExtension || !["js", "ts", "mjs", "cjs"].includes(fileExtension)) { + fileExtension = "js"; + } + return fileExtension; +} + +export function resolveBaseUrl(args: Partial): string { + return args.baseUrl ?? process.env.C8Y_BASEURL ?? "http://localhost:8080"; +} diff --git a/src/screenshot/runner.ts b/src/screenshot/runner.ts index 11ab1a6..5b45786 100644 --- a/src/screenshot/runner.ts +++ b/src/screenshot/runner.ts @@ -69,7 +69,7 @@ export class C8yScreenshotRunner { _.isNil ), { - overwrite: true, + overwrite: false, scale: false, disableTimersAndAnimations: true, } @@ -195,6 +195,8 @@ export class C8yScreenshotRunner { !lastAction || !isScreenshotAction(lastAction) ) { + cy.task("debug", `Taking screenshot ${item.image}`); + cy.task("debug", `Options: ${JSON.stringify(options)}`); cy.screenshot(item.image, options); } }, diff --git a/src/screenshot/startup.ts b/src/screenshot/startup.ts index 9d3285a..7b34422 100644 --- a/src/screenshot/startup.ts +++ b/src/screenshot/startup.ts @@ -10,14 +10,19 @@ import { config as dotenv } from "dotenv"; import { C8yAjvSchemaMatcher } from "../contrib/ajv"; import schema from "./../screenshot/schema.json"; -import { createInitConfig, readYamlFile } from "./helper"; +import { + createInitConfig, + readYamlFile, + resolveConfigOptions, + resolveFileExtension, +} from "./helper"; import { C8yScreenshotOptions, ScreenshotSetup, } from "./../lib/screenshots/types"; import debug from "debug"; -const log = debug("c8y:c8yscrn:startup"); +const log = debug("c8y:scrn:startup"); (async () => { try { @@ -27,9 +32,8 @@ const log = debug("c8y:c8yscrn:startup"); "No config file provided. Use --config option to provide the config file." ); } - - const baseUrl = - args.baseUrl ?? process.env.C8Y_BASEURL ?? "http://localhost:8080"; + const resolvedCypressConfig = resolveConfigOptions(args); + const baseUrl = resolvedCypressConfig.config.e2e.baseUrl; const yamlFile = path.resolve(process.cwd(), args.config); if (args.init === true) { @@ -69,57 +73,30 @@ const log = debug("c8y:c8yscrn:startup"); throw new Error(`Invalid config file. ${error.message}`); } - // might run in different environments, so we need to find the correct extension - // this is required when running in development mode from ts files - let fileExtension = __filename?.split(".")?.pop(); - if (!fileExtension || !["js", "ts", "mjs", "cjs"].includes(fileExtension)) { - fileExtension = "js"; - } log(`Using baseUrl ${baseUrl}`); - const screenshotsFolder = path.resolve( - process.cwd(), - args.folder ?? "c8yscrn" - ); + const screenshotsFolder = + resolvedCypressConfig.config.e2e.screenshotsFolder; log(`Using screenshots folder ${screenshotsFolder}`); + + const fileExtension = resolveFileExtension(); const cypressConfigFile = path.resolve( path.dirname(__filename), `config.${fileExtension}` ); log(`Using cypress config file ${cypressConfigFile}`); - const browser = (args.browser ?? process.env.C8Y_BROWSER ?? "chrome") - .toLowerCase() - .trim(); - log(`Using browser ${args.browser}`); - if (!["chrome", "firefox", "electron"].includes(browser)) { - throw new Error( - `Invalid browser ${browser}. Supported browsers are chrome, firefox, electron.` - ); - } + const browser = resolvedCypressConfig.browser; + log(`Using browser ${browser}`); const browserLaunchArgs = process.env[`C8Y_${browser.toUpperCase()}_LAUNCH_ARGS`] ?? process.env.C8Y_BROWSER_LAUNCH_ARGS ?? ""; + // https://docs.cypress.io/guides/guides/module-api const config = { - ...{ - configFile: cypressConfigFile, - browser, - testingType: "e2e" as const, - quiet: args.quiet ?? true, - config: { - e2e: { - baseUrl, - screenshotsFolder, - specPattern: path.join( - path.dirname(__filename), - `*.cy.${fileExtension}` - ), - }, - }, - }, + ...resolvedCypressConfig, ...{ env: { ...envs, @@ -200,6 +177,12 @@ function runOptions(yargs: Argv) { requiresArg: true, description: "The target folder for the screenshots", }) + .option("clear", { + type: "boolean", + requiresArg: true, + description: "Clear the target folder and remove all data", + default: false, + }) .option("browser", { alias: "b", type: "string", @@ -210,7 +193,7 @@ function runOptions(yargs: Argv) { .option("quiet", { type: "boolean", default: true, - requiresArg: false, + requiresArg: true, hidden: true, }) .option("tags", { diff --git a/src/shared/c8ypact/fileadapter.ts b/src/shared/c8ypact/fileadapter.ts index 846be21..e4d32cd 100644 --- a/src/shared/c8ypact/fileadapter.ts +++ b/src/shared/c8ypact/fileadapter.ts @@ -38,7 +38,7 @@ export interface C8yPactFileAdapter { getFolder: () => string; } -const log = debug("c8y:plugin:fileadapter"); +const log = debug("c8y:fileadapter"); /** * Default implementation of C8yPactFileAdapter which loads and saves pact objects from/to