Skip to content

Commit

Permalink
Crop scrollbar in screenshots(closes DevExpress#3292) (DevExpress#3470)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexKamaev authored and kirovboris committed Dec 18, 2019
1 parent 0fa71fb commit 1a30634
Show file tree
Hide file tree
Showing 12 changed files with 429 additions and 157 deletions.
31 changes: 8 additions & 23 deletions src/browser/provider/built-in/chrome/cdp.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import remoteChrome from 'chrome-remote-interface';
import { writeFile } from '../../../../utils/promisified-functions';
import { GET_WINDOW_DIMENSIONS_INFO_SCRIPT } from '../../utils/client-functions';


Expand Down Expand Up @@ -45,18 +44,16 @@ async function setEmulation (runtimeInfo) {
await resizeWindow({ width: config.width, height: config.height }, runtimeInfo);
}

async function getScreenshotData (client) {
const { visualViewport } = await client.Page.getLayoutMetrics();
export async function getScreenshotData ({ client }) {
const screenshotData = await client.Page.captureScreenshot();

const clipRegion = {
x: visualViewport.pageX,
y: visualViewport.pageY,
width: visualViewport.clientWidth,
height: visualViewport.clientHeight,
scale: visualViewport.scale
};
return Buffer.from(screenshotData.data, 'base64');
}

return await client.Page.captureScreenshot({ fromSurface: true, clip: clipRegion });
export async function getPageViewport ({ client }) {
const { visualViewport } = await client.Page.getLayoutMetrics();

return visualViewport;
}

export async function createClient (runtimeInfo) {
Expand Down Expand Up @@ -113,18 +110,6 @@ export async function updateMobileViewportSize (runtimeInfo) {
runtimeInfo.viewportSize.height = windowDimensions.outerHeight;
}

export async function getVideoFrameData ({ client }) {
const frameData = await getScreenshotData(client);

return Buffer.from(frameData.data, 'base64');
}

export async function takeScreenshot (path, { client }) {
const screenshotData = await getScreenshotData(client);

await writeFile(path, screenshotData.data, { encoding: 'base64' });
}

export async function resizeWindow (newDimensions, runtimeInfo) {
const { browserId, config, viewportSize, providerMethods } = runtimeInfo;

Expand Down
19 changes: 16 additions & 3 deletions src/browser/provider/built-in/chrome/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { start as startLocalChrome, stop as stopLocalChrome } from './local-chro
import * as cdp from './cdp';
import getMaximizedHeadlessWindowSize from '../../utils/get-maximized-headless-window-size';
import { GET_WINDOW_DIMENSIONS_INFO_SCRIPT } from '../../utils/client-functions';

import { cropScreenshot } from '../../../../screenshots/crop';
import { writePng } from '../../../../screenshots/utils';

const MIN_AVAILABLE_DIMENSION = 50;

Expand Down Expand Up @@ -65,8 +66,20 @@ export default {

async takeScreenshot (browserId, path) {
const runtimeInfo = this.openedBrowsers[browserId];
const viewport = await cdp.getPageViewport(runtimeInfo);
const binaryImage = await cdp.getScreenshotData(runtimeInfo);

const { clientWidth, clientHeight } = viewport;

const croppedImage = await cropScreenshot(path, false, null, {
right: clientWidth,
left: 0,
top: 0,
bottom: clientHeight
}, binaryImage);

await cdp.takeScreenshot(path, runtimeInfo);
if (croppedImage)
await writePng(path, croppedImage);
},

async resizeWindow (browserId, width, height, currentWidth, currentHeight) {
Expand All @@ -89,7 +102,7 @@ export default {
},

async getVideoFrameData (browserId) {
return await cdp.getVideoFrameData(this.openedBrowsers[browserId]);
return await cdp.getScreenshotData(this.openedBrowsers[browserId]);
},

async hasCustomActionForBrowser (browserId) {
Expand Down
25 changes: 22 additions & 3 deletions src/screenshots/capturer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { join as joinPath, dirname, basename } from 'path';
import { generateThumbnail } from 'testcafe-browser-tools';
import cropScreenshot from './crop';
import { cropScreenshot } from './crop';
import makeDir from 'make-dir';
import { isInQueue, addToQueue } from '../utils/async-queue';
import WARNING_MESSAGE from '../notifications/warning-message';
import escapeUserAgent from '../utils/escape-user-agent';
import correctFilePath from '../utils/correct-file-path';
import { stat } from '../utils/promisified-functions';
import { readFile, deleteFile, stat } from '../utils/promisified-functions';
import { writePng } from './utils';

export default class Capturer {
constructor (baseScreenshotsPath, testEntry, connection, pathPattern, warningLog) {
Expand Down Expand Up @@ -121,7 +122,25 @@ export default class Capturer {
if (!await Capturer._isScreenshotCaptured(screenshotPath))
return;

await cropScreenshot(screenshotPath, markSeed, Capturer._getClientAreaDimensions(pageDimensions), Capturer._getCropDimensions(cropDimensions, pageDimensions));
try {
const binaryImage = await readFile(screenshotPath);

const croppedImage = await cropScreenshot(
screenshotPath,
markSeed,
Capturer._getClientAreaDimensions(pageDimensions),
Capturer._getCropDimensions(cropDimensions, pageDimensions),
binaryImage
);

if (croppedImage)
await writePng(screenshotPath, croppedImage);
}
catch (err) {
await deleteFile(screenshotPath);

throw err;
}

await generateThumbnail(screenshotPath, thumbnailPath);
});
Expand Down
157 changes: 66 additions & 91 deletions src/screenshots/crop.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,11 @@
import fs from 'fs';
import Promise from 'pinkie';
import { PNG } from 'pngjs';
import promisifyEvent from 'promisify-event';
import { readPng, copyImagePart } from './utils';
import limitNumber from '../utils/limit-number';
import { deleteFile } from '../utils/promisified-functions';
import renderTemplate from '../utils/render-template';
import { InvalidElementScreenshotDimensionsError } from '../errors/test-run/';
import { MARK_LENGTH, MARK_RIGHT_MARGIN, MARK_BYTES_PER_PIXEL } from './constants';
import WARNING_MESSAGES from '../notifications/warning-message';


function readPng (filePath) {
const stream = fs.createReadStream(filePath);
const png = new PNG();

const parsedPromise = Promise.race([
promisifyEvent(png, 'parsed'),
promisifyEvent(png, 'error'),
promisifyEvent(stream, 'error')
]);

stream.pipe(png);

return parsedPromise
.then(() => png);
}

function writePng (filePath, png) {
const outStream = fs.createWriteStream(filePath);
const pngStream = png.pack();

const finishPromise = Promise.race([
promisifyEvent(outStream, 'finish'),
promisifyEvent(outStream, 'error'),
promisifyEvent(pngStream, 'error')
]);

pngStream.pipe(outStream);

return finishPromise;
}

function markSeedToId (markSeed) {
let id = 0;

Expand All @@ -50,84 +15,94 @@ function markSeedToId (markSeed) {
return id;
}

function detectClippingArea (srcImage, { markSeed, clientAreaDimensions, cropDimensions, screenshotPath } = {}) {
let clipLeft = 0;
let clipTop = 0;
let clipRight = srcImage.width;
let clipBottom = srcImage.height;
let clipWidth = srcImage.width;
let clipHeight = srcImage.height;
export function calculateMarkPosition (pngImage, markSeed) {
const mark = Buffer.from(markSeed);
const markIndex = pngImage.data.indexOf(mark);

if (markSeed && clientAreaDimensions) {
const mark = Buffer.from(markSeed);
if (markIndex < 0)
return null;

const markIndex = srcImage.data.indexOf(mark);
const endPosition = markIndex / MARK_BYTES_PER_PIXEL + MARK_LENGTH + MARK_RIGHT_MARGIN;

if (markIndex < 0)
throw new Error(renderTemplate(WARNING_MESSAGES.screenshotMarkNotFound, screenshotPath, markSeedToId(markSeed)));
const x = endPosition % pngImage.width || pngImage.width;
const y = (endPosition - x) / pngImage.width + 1;

const endPosition = markIndex / MARK_BYTES_PER_PIXEL + MARK_LENGTH + MARK_RIGHT_MARGIN;
return { x, y };
}

clipRight = endPosition % srcImage.width || srcImage.width;
clipBottom = (endPosition - clipRight) / srcImage.width + 1;
clipLeft = clipRight - clientAreaDimensions.width;
clipTop = clipBottom - clientAreaDimensions.height;
}
export function getClipInfoByMarkPosition (markPosition, { width, height }) {
const { x, y } = markPosition;

const markLineNumber = clipBottom;
const clipRight = x;
const clipBottom = y;
const clipLeft = clipRight - width;
const clipTop = clipBottom - height;

if (cropDimensions) {
clipRight = limitNumber(clipLeft + cropDimensions.right, clipLeft, clipRight);
clipBottom = limitNumber(clipTop + cropDimensions.bottom, clipTop, clipBottom);
clipLeft = limitNumber(clipLeft + cropDimensions.left, clipLeft, clipRight);
clipTop = limitNumber(clipTop + cropDimensions.top, clipTop, clipBottom);
}
return {
clipLeft,
clipTop,
clipRight,
clipBottom
};
}

if (markSeed && clipBottom === markLineNumber)
clipBottom -= 1;
export function getClipInfoByCropDimensions ({ clipRight, clipLeft, clipBottom, clipTop }, cropDimensions) {
if (cropDimensions) {
const { right, top, bottom, left } = cropDimensions;

clipWidth = clipRight - clipLeft;
clipHeight = clipBottom - clipTop;
clipRight = limitNumber(clipLeft + right, clipLeft, clipRight);
clipBottom = limitNumber(clipTop + bottom, clipTop, clipBottom);
clipLeft = limitNumber(clipLeft + left, clipLeft, clipRight);
clipTop = limitNumber(clipTop + top, clipTop, clipBottom);
}

return {
left: clipLeft,
top: clipTop,
right: clipRight,
bottom: clipBottom,
width: clipWidth,
height: clipHeight
clipLeft,
clipTop,
clipRight,
clipBottom
};
}

function copyImagePart (srcImage, { left, top, width, height }) {
const dstImage = new PNG({ width, height });
const stride = dstImage.width * MARK_BYTES_PER_PIXEL;
export function calculateClipInfo (pngImage, path, markSeed, clientAreaDimensions, cropDimensions) {
let clipInfo = {
clipRight: pngImage.width,
clipBottom: pngImage.height,
clipLeft: 0,
clipTop: 0
};

let markPosition = null;

for (let i = 0; i < height; i++) {
const srcStartIndex = (srcImage.width * (i + top) + left) * MARK_BYTES_PER_PIXEL;
if (markSeed && clientAreaDimensions) {
markPosition = calculateMarkPosition(pngImage, markSeed);

if (!markPosition)
throw new Error(renderTemplate(WARNING_MESSAGES.screenshotMarkNotFound, path, markSeedToId(markSeed)));

srcImage.data.copy(dstImage.data, stride * i, srcStartIndex, srcStartIndex + stride);
clipInfo = getClipInfoByMarkPosition(markPosition, clientAreaDimensions);
}

return dstImage;
}
clipInfo = getClipInfoByCropDimensions(clipInfo, cropDimensions);

export default async function (screenshotPath, markSeed, clientAreaDimensions, cropDimensions) {
const srcImage = await readPng(screenshotPath);
if (markPosition && markPosition.y === clipInfo.clipBottom)
clipInfo.clipBottom--;

const clippingArea = detectClippingArea(srcImage, { markSeed, clientAreaDimensions, cropDimensions, screenshotPath });
const clipWidth = clipInfo.clipRight - clipInfo.clipLeft;
const clipHeight = clipInfo.clipBottom - clipInfo.clipTop;

if (clippingArea.width <= 0 || clippingArea.height <= 0) {
await deleteFile(screenshotPath);
throw new InvalidElementScreenshotDimensionsError(clippingArea.width, clippingArea.height);
}
if (clipWidth <= 0 || clipHeight <= 0)
throw new InvalidElementScreenshotDimensionsError(clipWidth, clipHeight);

if (!markSeed && !cropDimensions)
return true;
return clipInfo;
}

const dstImage = copyImagePart(srcImage, clippingArea);
export async function cropScreenshot (path, markSeed, clientAreaDimensions, cropDimensions, binaryImage) {
if (!markSeed && !cropDimensions)
return null;

await writePng(screenshotPath, dstImage);
const pngImage = await readPng(binaryImage);
const clip = calculateClipInfo(pngImage, path, markSeed, clientAreaDimensions, cropDimensions);

return true;
return copyImagePart(pngImage, clip);
}
28 changes: 0 additions & 28 deletions src/screenshots/generate-mark.js

This file was deleted.

Loading

0 comments on commit 1a30634

Please sign in to comment.