Implement comprehensive frontend integration testing with Playwright
- Add Playwright E2E testing framework with cross-browser support (Chrome, Firefox) - Create authentication helpers for CalDAV server integration - Implement calendar interaction helpers with event creation, drag-and-drop, and view switching - Add comprehensive drag-and-drop test suite with event cleanup - Configure CI/CD integration with Gitea Actions for headless testing - Support both local development and CI environments with proper dependency management - Include video recording, screenshots, and HTML reporting for test debugging - Handle Firefox-specific timing and interaction challenges with force clicks and timeouts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										338
									
								
								frontend/e2e/node_modules/playwright/lib/matchers/toMatchSnapshot.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								frontend/e2e/node_modules/playwright/lib/matchers/toMatchSnapshot.js
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,338 @@ | ||||
| "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, 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 | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone