testplane
Version:
Tests framework based on mocha and wdio
256 lines • 11.2 kB
JavaScript
;
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