UNPKG

testplane

Version:

Tests framework based on mocha and wdio

256 lines 11.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Image = exports.extractBase64PngSize = void 0; const fs_1 = __importDefault(require("fs")); const looks_same_1 = __importDefault(require("looks-same")); const preload_utils_1 = require("./utils/preload-utils"); const eight_bit_rgba_to_png_1 = require("./utils/eight-bit-rgba-to-png"); const png_1 = require("./constants/png"); const initJsquashPromise = new Promise(resolve => { const wasmLocation = require.resolve("@jsquash/png/codec/pkg/squoosh_png_bg.wasm"); Promise.all([ (0, preload_utils_1.loadEsm)("@jsquash/png/decode.js"), fs_1.default.promises.readFile(wasmLocation), ]) .then(([mod, wasmBytes]) => mod.init(wasmBytes)) .then(resolve); }); const jsquashDecode = (buffer) => { return Promise.all([ (0, preload_utils_1.loadEsm)("@jsquash/png/decode.js"), initJsquashPromise, ]).then(([mod]) => mod.decode(buffer, { bitDepth: png_1.BITS_IN_BYTE })); }; const extractBase64PngSize = (base64EncodedPng) => { // Strips data URI prefix if it exists const base64Data = base64EncodedPng.includes(";base64,") ? base64EncodedPng.split(";base64,").pop() : base64EncodedPng; if (base64Data.length <= png_1.PNG_MIN_ASSIST_BYTES) { throw new Error("Invalid base64 encoded png: too short"); } const headerBytesToRead = Math.max(png_1.PNG_WIDTH_OFFSET, png_1.PNG_HEIGHT_OFFSET) + 4; const headerCharsToRead = Math.ceil(headerBytesToRead / 3) * 4; const pngHeader = Buffer.from(base64Data.slice(0, headerCharsToRead), "base64"); if (!pngHeader.subarray(0, png_1.PNG_SIGNATURE.byteLength).equals(png_1.PNG_SIGNATURE)) { throw new Error("Invalid base64 encoded png: signature missmatch"); } return { width: pngHeader.readUInt32BE(png_1.PNG_WIDTH_OFFSET), height: pngHeader.readUInt32BE(png_1.PNG_HEIGHT_OFFSET), }; }; exports.extractBase64PngSize = extractBase64PngSize; const hasICCPChunk = (buffer) => { const auxChunkSizeBytes = 12; // 4 bytes for length, 4 bytes for type, 4 bytes for crc const iCCPChunkType = Buffer.from("iCCP", "ascii").readUInt32BE(0); const IDATChunkType = Buffer.from("IDAT", "ascii").readUInt32BE(0); const PLTEChunkType = Buffer.from("PLTE", "ascii").readUInt32BE(0); for (let nextChunkPointer = png_1.PNG_SIGNATURE.byteLength; nextChunkPointer <= buffer.length - auxChunkSizeBytes;) { const chunkLength = buffer.readUInt32BE(nextChunkPointer); const chunkType = buffer.readUInt32BE(nextChunkPointer + 4); if (chunkType === iCCPChunkType) { return true; } // If the iCCP chunk appears, it must precede the first IDAT chunk, and it must also precede the PLTE chunk if present // https://libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.iCCP if (chunkType === IDATChunkType || chunkType === PLTEChunkType) { return false; } nextChunkPointer += chunkLength + auxChunkSizeBytes; } return false; }; class Image { static create(buffer) { return new this(buffer); } constructor(bufferOrSize) { this._imgData = null; this._composeImages = []; this._clearAreas = []; this._decodeError = null; this._hasICCPChunk = false; if (Buffer.isBuffer(bufferOrSize)) { this._width = bufferOrSize.readUInt32BE(png_1.PNG_WIDTH_OFFSET); this._height = bufferOrSize.readUInt32BE(png_1.PNG_HEIGHT_OFFSET); this._hasICCPChunk = hasICCPChunk(bufferOrSize); this._imgDataPromise = jsquashDecode(bufferOrSize) .then(({ data }) => { return Buffer.from(data.buffer, data.byteOffset, data.byteLength); }) .catch(error => { this._decodeError = error; return null; }); return; } this._width = Math.max(0, Math.floor(bufferOrSize.width)); this._height = Math.max(0, Math.floor(bufferOrSize.height)); const blackRgbaPixel = Buffer.from([0, 0, 0, 255]); this._imgData = Buffer.alloc(this._width * this._height * png_1.RGBA_CHANNELS); this._imgData.fill(blackRgbaPixel); this._imgDataPromise = Promise.resolve(this._imgData); } get hasICCPChunk() { return this._hasICCPChunk; } async _getImgData() { if (this._imgData) { return this._imgData; } this._imgData = await this._imgDataPromise; if (!this._imgData) { throw new Error("Failed to decode image", { cause: this._decodeError }); } return this._imgData; } _ensureImagesHaveSameWidth() { for (const image of this._composeImages) { if (image._width !== this._width) { throw new Error([ `It looks like viewport width changed while performing long page screenshot (${this._width}px -> ${image._width}px)`, "Please make sure page is fully loaded before making screenshot", ].join("\n")); } } } getSize() { this._ensureImagesHaveSameWidth(); const height = this._composeImages.reduce((acc, img) => acc + img._height, this._height); return { height, width: this._width }; } async clone() { const imgData = await this._getImgData(); const image = new Image({ width: this._width, height: this._height }); image._imgData = Buffer.from(imgData); image._imgDataPromise = Promise.resolve(image._imgData); image._clearAreas = this._clearAreas.slice(); image._composeImages = this._composeImages.slice(); return image; } async crop(rect) { const imgData = await this._getImgData(); let bufferPointer = 0; let sourceOffset = (rect.top * this._width + rect.left) * png_1.RGBA_CHANNELS; const actualWidth = Math.min(rect.width, this._width - rect.left); const actualHeight = Math.min(rect.height, this._height - rect.top); const bytesToCopy = actualWidth * png_1.RGBA_CHANNELS; const bytesToIterate = this._width * png_1.RGBA_CHANNELS; for (let i = 0; i < actualHeight; i++) { imgData.copy(imgData, bufferPointer, sourceOffset, sourceOffset + bytesToCopy); bufferPointer += bytesToCopy; sourceOffset += bytesToIterate; } this._imgData = imgData.subarray(0, bufferPointer); this._width = actualWidth; this._height = actualHeight; } addJoin(attachedImages) { this._composeImages = this._composeImages.concat(attachedImages); } async applyJoin() { if (this._composeImages.length) { this._ensureImagesHaveSameWidth(); const resultHeight = this._composeImages.reduce((acc, img) => acc + img._height, this._height); const imageBuffers = await Promise.all([this, ...this._composeImages].map(img => img._getImgData())); this._imgData = Buffer.concat(imageBuffers); this._height = resultHeight; this._composeImages = []; } for (const rect of this._clearAreas) { await this.clearArea(rect); } this._clearAreas = []; } async clearArea(rect) { const clippedRect = this._clipRect(rect); if (!clippedRect) { return; } const imgData = await this._getImgData(); let sourceOffset = (clippedRect.top * this._width + clippedRect.left) * png_1.RGBA_CHANNELS; const bytesToCopyAmount = clippedRect.width * png_1.RGBA_CHANNELS; const bytesToIterate = this._width * png_1.RGBA_CHANNELS; const bytesToFill = Buffer.from([0, 0, 0, 255]); // black RGBA for (let i = 0; i < clippedRect.height; i++) { imgData.fill(bytesToFill, sourceOffset, sourceOffset + bytesToCopyAmount); sourceOffset += bytesToIterate; } } async addClear(rect) { this._clearAreas.push(rect); } _clipRect(rect) { const left = Math.max(0, Math.floor(rect.left)); const top = Math.max(0, Math.floor(rect.top)); const right = Math.min(this._width, Math.ceil(rect.left + rect.width)); const bottom = Math.min(this._height, Math.ceil(rect.top + rect.height)); if (right <= left || bottom <= top) { return null; } return { left, top, width: right - left, height: bottom - top, }; } async getRGB(x, y) { const imgData = await this._getImgData(); const idx = (this._width * y + x) * png_1.RGBA_CHANNELS; return { R: imgData[idx], G: imgData[idx + 1], B: imgData[idx + 2], }; } async _getPngBuffer() { const imageData = await this._getImgData(); try { return (0, eight_bit_rgba_to_png_1.convertRgbaToPng)(imageData, this._width, this._height); } catch (e) { const baseMessage = `Failed to convert image buffer to PNG.\n` + `Expected image size (formatted as height x width): ${this._height} x ${this._width}.\n` + `Actual data present in buffer (formatted as height x width): ${this._height} x ${imageData.length / (this._height * png_1.RGBA_CHANNELS)} or ${imageData.length / (this._width * png_1.RGBA_CHANNELS)} x ${this._width}.\n` + `This means the data is malformed or image size doesn't match actual image dimensions.\n`; throw new Error(baseMessage, { cause: e }); } } async save(file) { const data = await this._getPngBuffer(); await fs_1.default.promises.writeFile(file, data); } static fromBase64(base64) { return new this(Buffer.from(base64, "base64")); } async toPngBuffer(opts = { resolveWithObject: true }) { const data = await this._getPngBuffer(); return opts.resolveWithObject ? { data, size: { width: this._width, height: this._height } } : data; } static compare(path1, path2, opts = {}) { const compareOptions = { ignoreCaret: opts.canHaveCaret, pixelRatio: opts.pixelRatio, ...opts.compareOpts, }; if (opts.tolerance) { compareOptions.tolerance = opts.tolerance; } if (opts.antialiasingTolerance) { compareOptions.antialiasingTolerance = opts.antialiasingTolerance; } return (0, looks_same_1.default)(path1, path2, { ...compareOptions, createDiffImage: true }); } static buildDiff(opts) { const { diffColor: highlightColor, ...otherOpts } = opts; const diffOptions = { highlightColor, ...otherOpts }; return looks_same_1.default.createDiff(diffOptions); } } exports.Image = Image; //# sourceMappingURL=image.js.map