 aacb45156b
			
		
	
	aacb45156b
	
	
	
		
			
			- Install Jest for unit testing with React Testing Library - Install Playwright for end-to-end testing - Configure Jest with proper TypeScript support and module mapping - Create test setup files and utilities for both unit and e2e tests Components: * Jest configuration with coverage thresholds * Playwright configuration with browser automation * Unit tests for LoginForm, AuthContext, and useSocketIO hook * E2E tests for authentication, dashboard, and agents workflows * GitHub Actions workflow for automated testing * Mock data and API utilities for consistent testing * Test documentation with best practices Testing features: - Unit tests with 70% coverage threshold - E2E tests with API mocking and user journey testing - CI/CD integration for automated test runs - Cross-browser testing support with Playwright - Authentication system testing end-to-end 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			339 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use strict";
 | |
| var __create = Object.create;
 | |
| var __defProp = Object.defineProperty;
 | |
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
 | |
| var __getOwnPropNames = Object.getOwnPropertyNames;
 | |
| var __getProtoOf = Object.getPrototypeOf;
 | |
| var __hasOwnProp = Object.prototype.hasOwnProperty;
 | |
| var __export = (target, all) => {
 | |
|   for (var name in all)
 | |
|     __defProp(target, name, { get: all[name], enumerable: true });
 | |
| };
 | |
| var __copyProps = (to, from, except, desc) => {
 | |
|   if (from && typeof from === "object" || typeof from === "function") {
 | |
|     for (let key of __getOwnPropNames(from))
 | |
|       if (!__hasOwnProp.call(to, key) && key !== except)
 | |
|         __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
 | |
|   }
 | |
|   return to;
 | |
| };
 | |
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
 | |
|   // If the importer is in node compatibility mode or this is not an ESM
 | |
|   // file that has been converted to a CommonJS file using a Babel-
 | |
|   // compatible transform (i.e. "__esModule" has not been set), then set
 | |
|   // "default" to the CommonJS "module.exports" for node compatibility.
 | |
|   isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
 | |
|   mod
 | |
| ));
 | |
| var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
 | |
| var toMatchSnapshot_exports = {};
 | |
| __export(toMatchSnapshot_exports, {
 | |
|   toHaveScreenshot: () => toHaveScreenshot,
 | |
|   toHaveScreenshotStepTitle: () => toHaveScreenshotStepTitle,
 | |
|   toMatchSnapshot: () => toMatchSnapshot
 | |
| });
 | |
| module.exports = __toCommonJS(toMatchSnapshot_exports);
 | |
| var import_fs = __toESM(require("fs"));
 | |
| var import_path = __toESM(require("path"));
 | |
| var import_utils = require("playwright-core/lib/utils");
 | |
| var import_utils2 = require("playwright-core/lib/utils");
 | |
| var import_utilsBundle = require("playwright-core/lib/utilsBundle");
 | |
| var import_util = require("../util");
 | |
| var import_matcherHint = require("./matcherHint");
 | |
| var import_globals = require("../common/globals");
 | |
| const NonConfigProperties = [
 | |
|   "clip",
 | |
|   "fullPage",
 | |
|   "mask",
 | |
|   "maskColor",
 | |
|   "omitBackground",
 | |
|   "timeout"
 | |
| ];
 | |
| class SnapshotHelper {
 | |
|   constructor(testInfo, matcherName, locator, anonymousSnapshotExtension, configOptions, nameOrOptions, optOptions) {
 | |
|     let name;
 | |
|     if (Array.isArray(nameOrOptions) || typeof nameOrOptions === "string") {
 | |
|       name = nameOrOptions;
 | |
|       this.options = { ...optOptions };
 | |
|     } else {
 | |
|       const { name: nameFromOptions, ...options } = nameOrOptions;
 | |
|       this.options = options;
 | |
|       name = nameFromOptions;
 | |
|     }
 | |
|     this.name = Array.isArray(name) ? name.join(import_path.default.sep) : name || "";
 | |
|     const resolvedPaths = testInfo._resolveSnapshotPaths(matcherName === "toHaveScreenshot" ? "screenshot" : "snapshot", name, "updateSnapshotIndex", anonymousSnapshotExtension);
 | |
|     this.expectedPath = resolvedPaths.absoluteSnapshotPath;
 | |
|     this.attachmentBaseName = resolvedPaths.relativeOutputPath;
 | |
|     const outputBasePath = testInfo._getOutputPath(resolvedPaths.relativeOutputPath);
 | |
|     this.legacyExpectedPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-expected");
 | |
|     this.previousPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-previous");
 | |
|     this.actualPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-actual");
 | |
|     this.diffPath = (0, import_util.addSuffixToFilePath)(outputBasePath, "-diff");
 | |
|     const filteredConfigOptions = { ...configOptions };
 | |
|     for (const prop of NonConfigProperties)
 | |
|       delete filteredConfigOptions[prop];
 | |
|     this.options = {
 | |
|       ...filteredConfigOptions,
 | |
|       ...this.options
 | |
|     };
 | |
|     if (this.options._comparator) {
 | |
|       this.options.comparator = this.options._comparator;
 | |
|       delete this.options._comparator;
 | |
|     }
 | |
|     if (this.options.maxDiffPixels !== void 0 && this.options.maxDiffPixels < 0)
 | |
|       throw new Error("`maxDiffPixels` option value must be non-negative integer");
 | |
|     if (this.options.maxDiffPixelRatio !== void 0 && (this.options.maxDiffPixelRatio < 0 || this.options.maxDiffPixelRatio > 1))
 | |
|       throw new Error("`maxDiffPixelRatio` option value must be between 0 and 1");
 | |
|     this.matcherName = matcherName;
 | |
|     this.locator = locator;
 | |
|     this.updateSnapshots = testInfo.config.updateSnapshots;
 | |
|     this.mimeType = import_utilsBundle.mime.getType(import_path.default.basename(this.expectedPath)) ?? "application/octet-stream";
 | |
|     this.comparator = (0, import_utils.getComparator)(this.mimeType);
 | |
|     this.testInfo = testInfo;
 | |
|     this.kind = this.mimeType.startsWith("image/") ? "Screenshot" : "Snapshot";
 | |
|   }
 | |
|   createMatcherResult(message, pass, log) {
 | |
|     const unfiltered = {
 | |
|       name: this.matcherName,
 | |
|       expected: this.expectedPath,
 | |
|       actual: this.actualPath,
 | |
|       diff: this.diffPath,
 | |
|       pass,
 | |
|       message: () => message,
 | |
|       log
 | |
|     };
 | |
|     return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== void 0));
 | |
|   }
 | |
|   handleMissingNegated() {
 | |
|     const isWriteMissingMode = this.updateSnapshots !== "none";
 | |
|     const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? `, matchers using ".not" won't write them automatically.` : "."}`;
 | |
|     return this.createMatcherResult(message, true);
 | |
|   }
 | |
|   handleDifferentNegated() {
 | |
|     return this.createMatcherResult("", false);
 | |
|   }
 | |
|   handleMatchingNegated() {
 | |
|     const message = [
 | |
|       import_utils2.colors.red(`${this.kind} comparison failed:`),
 | |
|       "",
 | |
|       indent("Expected result should be different from the actual one.", "  ")
 | |
|     ].join("\n");
 | |
|     return this.createMatcherResult(message, true);
 | |
|   }
 | |
|   handleMissing(actual, step) {
 | |
|     const isWriteMissingMode = this.updateSnapshots !== "none";
 | |
|     if (isWriteMissingMode)
 | |
|       writeFileSync(this.expectedPath, actual);
 | |
|     step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-expected"), contentType: this.mimeType, path: this.expectedPath });
 | |
|     writeFileSync(this.actualPath, actual);
 | |
|     step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-actual"), contentType: this.mimeType, path: this.actualPath });
 | |
|     const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ", writing actual." : "."}`;
 | |
|     if (this.updateSnapshots === "all" || this.updateSnapshots === "changed") {
 | |
|       console.log(message);
 | |
|       return this.createMatcherResult(message, true);
 | |
|     }
 | |
|     if (this.updateSnapshots === "missing") {
 | |
|       this.testInfo._hasNonRetriableError = true;
 | |
|       this.testInfo._failWithError(new Error(message));
 | |
|       return this.createMatcherResult("", true);
 | |
|     }
 | |
|     return this.createMatcherResult(message, false);
 | |
|   }
 | |
|   handleDifferent(actual, expected, previous, diff, header, diffError, log, step) {
 | |
|     const output = [`${header}${indent(diffError, "  ")}`];
 | |
|     if (this.name) {
 | |
|       output.push("");
 | |
|       output.push(`  Snapshot: ${this.name}`);
 | |
|     }
 | |
|     if (expected !== void 0) {
 | |
|       writeFileSync(this.legacyExpectedPath, expected);
 | |
|       step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-expected"), contentType: this.mimeType, path: this.expectedPath });
 | |
|     }
 | |
|     if (previous !== void 0) {
 | |
|       writeFileSync(this.previousPath, previous);
 | |
|       step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-previous"), contentType: this.mimeType, path: this.previousPath });
 | |
|     }
 | |
|     if (actual !== void 0) {
 | |
|       writeFileSync(this.actualPath, actual);
 | |
|       step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-actual"), contentType: this.mimeType, path: this.actualPath });
 | |
|     }
 | |
|     if (diff !== void 0) {
 | |
|       writeFileSync(this.diffPath, diff);
 | |
|       step?._attachToStep({ name: (0, import_util.addSuffixToFilePath)(this.attachmentBaseName, "-diff"), contentType: this.mimeType, path: this.diffPath });
 | |
|     }
 | |
|     if (log?.length)
 | |
|       output.push((0, import_util.callLogText)(log));
 | |
|     else
 | |
|       output.push("");
 | |
|     return this.createMatcherResult(output.join("\n"), false, log);
 | |
|   }
 | |
|   handleMatching() {
 | |
|     return this.createMatcherResult("", true);
 | |
|   }
 | |
| }
 | |
| function toMatchSnapshot(received, nameOrOptions = {}, optOptions = {}) {
 | |
|   const testInfo = (0, import_globals.currentTestInfo)();
 | |
|   if (!testInfo)
 | |
|     throw new Error(`toMatchSnapshot() must be called during the test`);
 | |
|   if (received instanceof Promise)
 | |
|     throw new Error("An unresolved Promise was passed to toMatchSnapshot(), make sure to resolve it by adding await to it.");
 | |
|   if (testInfo._projectInternal.ignoreSnapshots)
 | |
|     return { pass: !this.isNot, message: () => "", name: "toMatchSnapshot", expected: nameOrOptions };
 | |
|   const configOptions = testInfo._projectInternal.expect?.toMatchSnapshot || {};
 | |
|   const helper = new SnapshotHelper(
 | |
|     testInfo,
 | |
|     "toMatchSnapshot",
 | |
|     void 0,
 | |
|     "." + determineFileExtension(received),
 | |
|     configOptions,
 | |
|     nameOrOptions,
 | |
|     optOptions
 | |
|   );
 | |
|   if (this.isNot) {
 | |
|     if (!import_fs.default.existsSync(helper.expectedPath))
 | |
|       return helper.handleMissingNegated();
 | |
|     const isDifferent = !!helper.comparator(received, import_fs.default.readFileSync(helper.expectedPath), helper.options);
 | |
|     return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
 | |
|   }
 | |
|   if (!import_fs.default.existsSync(helper.expectedPath))
 | |
|     return helper.handleMissing(received, this._stepInfo);
 | |
|   const expected = import_fs.default.readFileSync(helper.expectedPath);
 | |
|   if (helper.updateSnapshots === "all") {
 | |
|     if (!(0, import_utils.compareBuffersOrStrings)(received, expected))
 | |
|       return helper.handleMatching();
 | |
|     writeFileSync(helper.expectedPath, received);
 | |
|     console.log(helper.expectedPath + " is not the same, writing actual.");
 | |
|     return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
 | |
|   }
 | |
|   if (helper.updateSnapshots === "changed") {
 | |
|     const result2 = helper.comparator(received, expected, helper.options);
 | |
|     if (!result2)
 | |
|       return helper.handleMatching();
 | |
|     writeFileSync(helper.expectedPath, received);
 | |
|     console.log(helper.expectedPath + " does not match, writing actual.");
 | |
|     return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
 | |
|   }
 | |
|   const result = helper.comparator(received, expected, helper.options);
 | |
|   if (!result)
 | |
|     return helper.handleMatching();
 | |
|   const receiver = (0, import_utils.isString)(received) ? "string" : "Buffer";
 | |
|   const header = (0, import_matcherHint.matcherHint)(this, void 0, "toMatchSnapshot", receiver, void 0, void 0);
 | |
|   return helper.handleDifferent(received, expected, void 0, result.diff, header, result.errorMessage, void 0, this._stepInfo);
 | |
| }
 | |
| function toHaveScreenshotStepTitle(nameOrOptions = {}, optOptions = {}) {
 | |
|   let name;
 | |
|   if (typeof nameOrOptions === "object" && !Array.isArray(nameOrOptions))
 | |
|     name = nameOrOptions.name;
 | |
|   else
 | |
|     name = nameOrOptions;
 | |
|   return Array.isArray(name) ? name.join(import_path.default.sep) : name || "";
 | |
| }
 | |
| async function toHaveScreenshot(pageOrLocator, nameOrOptions = {}, optOptions = {}) {
 | |
|   const testInfo = (0, import_globals.currentTestInfo)();
 | |
|   if (!testInfo)
 | |
|     throw new Error(`toHaveScreenshot() must be called during the test`);
 | |
|   if (testInfo._projectInternal.ignoreSnapshots)
 | |
|     return { pass: !this.isNot, message: () => "", name: "toHaveScreenshot", expected: nameOrOptions };
 | |
|   (0, import_util.expectTypes)(pageOrLocator, ["Page", "Locator"], "toHaveScreenshot");
 | |
|   const [page, locator] = pageOrLocator.constructor.name === "Page" ? [pageOrLocator, void 0] : [pageOrLocator.page(), pageOrLocator];
 | |
|   const configOptions = testInfo._projectInternal.expect?.toHaveScreenshot || {};
 | |
|   const helper = new SnapshotHelper(testInfo, "toHaveScreenshot", locator, void 0, configOptions, nameOrOptions, optOptions);
 | |
|   if (!helper.expectedPath.toLowerCase().endsWith(".png"))
 | |
|     throw new Error(`Screenshot name "${import_path.default.basename(helper.expectedPath)}" must have '.png' extension`);
 | |
|   (0, import_util.expectTypes)(pageOrLocator, ["Page", "Locator"], "toHaveScreenshot");
 | |
|   const style = await loadScreenshotStyles(helper.options.stylePath);
 | |
|   const timeout = helper.options.timeout ?? this.timeout;
 | |
|   const expectScreenshotOptions = {
 | |
|     locator,
 | |
|     animations: helper.options.animations ?? "disabled",
 | |
|     caret: helper.options.caret ?? "hide",
 | |
|     clip: helper.options.clip,
 | |
|     fullPage: helper.options.fullPage,
 | |
|     mask: helper.options.mask,
 | |
|     maskColor: helper.options.maskColor,
 | |
|     omitBackground: helper.options.omitBackground,
 | |
|     scale: helper.options.scale ?? "css",
 | |
|     style,
 | |
|     isNot: !!this.isNot,
 | |
|     timeout,
 | |
|     comparator: helper.options.comparator,
 | |
|     maxDiffPixels: helper.options.maxDiffPixels,
 | |
|     maxDiffPixelRatio: helper.options.maxDiffPixelRatio,
 | |
|     threshold: helper.options.threshold
 | |
|   };
 | |
|   const hasSnapshot = import_fs.default.existsSync(helper.expectedPath);
 | |
|   if (this.isNot) {
 | |
|     if (!hasSnapshot)
 | |
|       return helper.handleMissingNegated();
 | |
|     expectScreenshotOptions.expected = await import_fs.default.promises.readFile(helper.expectedPath);
 | |
|     const isDifferent = !(await page._expectScreenshot(expectScreenshotOptions)).errorMessage;
 | |
|     return isDifferent ? helper.handleDifferentNegated() : helper.handleMatchingNegated();
 | |
|   }
 | |
|   if (helper.updateSnapshots === "none" && !hasSnapshot)
 | |
|     return helper.createMatcherResult(`A snapshot doesn't exist at ${helper.expectedPath}.`, false);
 | |
|   const receiver = locator ? "locator" : "page";
 | |
|   if (!hasSnapshot) {
 | |
|     const { actual: actual2, previous: previous2, diff: diff2, errorMessage: errorMessage2, log: log2, timedOut: timedOut2 } = await page._expectScreenshot(expectScreenshotOptions);
 | |
|     if (errorMessage2) {
 | |
|       const header2 = (0, import_matcherHint.matcherHint)(this, locator, "toHaveScreenshot", receiver, void 0, void 0, timedOut2 ? timeout : void 0);
 | |
|       return helper.handleDifferent(actual2, void 0, previous2, diff2, header2, errorMessage2, log2, this._stepInfo);
 | |
|     }
 | |
|     return helper.handleMissing(actual2, this._stepInfo);
 | |
|   }
 | |
|   const expected = await import_fs.default.promises.readFile(helper.expectedPath);
 | |
|   expectScreenshotOptions.expected = helper.updateSnapshots === "all" ? void 0 : expected;
 | |
|   const { actual, previous, diff, errorMessage, log, timedOut } = await page._expectScreenshot(expectScreenshotOptions);
 | |
|   const writeFiles = () => {
 | |
|     writeFileSync(helper.expectedPath, actual);
 | |
|     writeFileSync(helper.actualPath, actual);
 | |
|     console.log(helper.expectedPath + " is re-generated, writing actual.");
 | |
|     return helper.createMatcherResult(helper.expectedPath + " running with --update-snapshots, writing actual.", true);
 | |
|   };
 | |
|   if (!errorMessage) {
 | |
|     if (helper.updateSnapshots === "all" && actual && (0, import_utils.compareBuffersOrStrings)(actual, expected)) {
 | |
|       console.log(helper.expectedPath + " is re-generated, writing actual.");
 | |
|       return writeFiles();
 | |
|     }
 | |
|     return helper.handleMatching();
 | |
|   }
 | |
|   if (helper.updateSnapshots === "changed" || helper.updateSnapshots === "all")
 | |
|     return writeFiles();
 | |
|   const header = (0, import_matcherHint.matcherHint)(this, void 0, "toHaveScreenshot", receiver, void 0, void 0, timedOut ? timeout : void 0);
 | |
|   return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo);
 | |
| }
 | |
| function writeFileSync(aPath, content) {
 | |
|   import_fs.default.mkdirSync(import_path.default.dirname(aPath), { recursive: true });
 | |
|   import_fs.default.writeFileSync(aPath, content);
 | |
| }
 | |
| function indent(lines, tab) {
 | |
|   return lines.replace(/^(?=.+$)/gm, tab);
 | |
| }
 | |
| function determineFileExtension(file) {
 | |
|   if (typeof file === "string")
 | |
|     return "txt";
 | |
|   if (compareMagicBytes(file, [137, 80, 78, 71, 13, 10, 26, 10]))
 | |
|     return "png";
 | |
|   if (compareMagicBytes(file, [255, 216, 255]))
 | |
|     return "jpg";
 | |
|   return "dat";
 | |
| }
 | |
| function compareMagicBytes(file, magicBytes) {
 | |
|   return Buffer.compare(Buffer.from(magicBytes), file.slice(0, magicBytes.length)) === 0;
 | |
| }
 | |
| async function loadScreenshotStyles(stylePath) {
 | |
|   if (!stylePath)
 | |
|     return;
 | |
|   const stylePaths = Array.isArray(stylePath) ? stylePath : [stylePath];
 | |
|   const styles = await Promise.all(stylePaths.map(async (stylePath2) => {
 | |
|     const text = await import_fs.default.promises.readFile(stylePath2, "utf8");
 | |
|     return text.trim();
 | |
|   }));
 | |
|   return styles.join("\n").trim() || void 0;
 | |
| }
 | |
| // Annotate the CommonJS export names for ESM import in node:
 | |
| 0 && (module.exports = {
 | |
|   toHaveScreenshot,
 | |
|   toHaveScreenshotStepTitle,
 | |
|   toMatchSnapshot
 | |
| });
 |