Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ POSTGRES_DB=vrt_db_dev

# features
AUTO_APPROVE_BASED_ON_HISTORY=true
ALLOW_DIFF_DIMENSIONS=true
86 changes: 52 additions & 34 deletions src/test-runs/test-runs.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { PrismaService } from '../prisma/prisma.service';
import { StaticService } from '../shared/static/static.service';
import { PNG } from 'pngjs';
import { TestStatus, TestRun, TestVariation } from '@prisma/client';
import Pixelmatch from 'pixelmatch';
import { CreateTestRequestDto } from './dto/create-test-request.dto';
import { TestRunResultDto } from './dto/testRunResult.dto';
import { DiffResult } from './diffResult';
Expand All @@ -15,7 +16,7 @@ import { convertBaselineDataToQuery } from '../shared/dto/baseline-data.dto';
import { TestRunDto } from './dto/testRun.dto';
import { BuildsService } from '../builds/builds.service';

// jest.mock('pixelmatch');
jest.mock('pixelmatch');
jest.mock('./dto/testRunResult.dto');

const initService = async ({
Expand Down Expand Up @@ -98,22 +99,6 @@ const initService = async ({

return module.get<TestRunsService>(TestRunsService);
};

// Helper fills PNG with specified color, or fills number of pixels in png if specified
const fillPng = (png, r, g, b, pixelsCount = 0, alpha = 255) => {
for (let y = 0; y < png.height; y++) {
for (let x = 0; x < png.width; x++) {
const idx = (png.width * y + x) << 2;
if (pixelsCount === 0 || idx < pixelsCount * 4) {
png.data[idx] = r;
png.data[idx + 1] = g;
png.data[idx + 2] = b;
png.data[idx + 3] = alpha;
}
}
}
};

describe('TestRunsService', () => {
let service: TestRunsService;
const ignoreAreas = [{ x: 1, y: 2, width: 10, height: 20 }];
Expand Down Expand Up @@ -618,7 +603,8 @@ describe('TestRunsService', () => {
});
});

it('diff image dimensions mismatch ', async () => {
it('diff image dimensions mismatch', async () => {
delete process.env.ALLOW_DIFF_DIMENSIONS;
const baseline = new PNG({
width: 10,
height: 10,
Expand All @@ -627,21 +613,61 @@ describe('TestRunsService', () => {
width: 20,
height: 20,
});
fillPng(baseline, 0, 0, 0);
fillPng(image, 0, 0, 0);
service = await initService({});

const result = service.getDiff(baseline, image, baseTestRun);

expect(result).toStrictEqual({
status: TestStatus.unresolved,
diffName: null,
pixelMisMatchCount: undefined,
diffPercent: undefined,
isSameDimension: false,
});
});

it('diff image dimensions mismatch ALLOWED', async () => {
process.env.ALLOW_DIFF_DIMENSIONS = 'true';
const baseline = new PNG({
width: 20,
height: 10,
});
const image = new PNG({
width: 10,
height: 20,
});
const diffName = 'diff name';
const saveImageMock = jest.fn().mockReturnValueOnce(diffName);
mocked(Pixelmatch).mockReturnValueOnce(200);
service = await initService({ saveImageMock });

const result = service.getDiff(baseline, image, baseTestRun);

expect(mocked(Pixelmatch)).toHaveBeenCalledWith(
new PNG({
width: 20,
height: 20,
}).data,
new PNG({
width: 20,
height: 20,
}).data,
new PNG({
width: 20,
height: 20,
}).data,
20,
20,
{
includeAA: true,
}
);
expect(saveImageMock).toHaveBeenCalledTimes(1);
expect(result).toStrictEqual({
status: TestStatus.unresolved,
diffName,
pixelMisMatchCount: 20 * 20 - 10 * 10,
diffPercent: 75,
pixelMisMatchCount: 200,
diffPercent: 50,
isSameDimension: false,
});
});
Expand All @@ -655,9 +681,8 @@ describe('TestRunsService', () => {
width: 10,
height: 10,
});
fillPng(baseline, 0, 0, 0);
fillPng(image, 0, 0, 0);
service = await initService({});
mocked(Pixelmatch).mockReturnValueOnce(0);

const result = service.getDiff(baseline, image, baseTestRun);

Expand Down Expand Up @@ -685,14 +710,10 @@ describe('TestRunsService', () => {
width: 100,
height: 100,
});
fillPng(baseline, 0, 0, 0);
fillPng(image, 0, 0, 0);

const pixelMisMatchCount = 150;
fillPng(image, 255, 0, 0, pixelMisMatchCount);

const saveImageMock = jest.fn();
service = await initService({ saveImageMock });
const pixelMisMatchCount = 150;
mocked(Pixelmatch).mockReturnValueOnce(pixelMisMatchCount);

const result = service.getDiff(baseline, image, testRun);

Expand Down Expand Up @@ -721,11 +742,8 @@ describe('TestRunsService', () => {
width: 100,
height: 100,
});
fillPng(baseline, 0, 0, 0, 255);
fillPng(image, 0, 0, 0, 255);
const pixelMisMatchCount = 200;
fillPng(image, 255, 0, 0, pixelMisMatchCount);

mocked(Pixelmatch).mockReturnValueOnce(pixelMisMatchCount);
const diffName = 'diff name';
const saveImageMock = jest.fn().mockReturnValueOnce(diffName);
service = await initService({
Expand Down
82 changes: 40 additions & 42 deletions src/test-runs/test-runs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,14 +271,13 @@ export class TestRunsService {
});
}

prepareImage(image: PNG, width: number, height: number) {
private scaleImageToSize(image: PNG, width: number, height: number): PNG {
if (width > image.width || height > image.height) {
const preparedImage = new PNG({ width, height, fill: true });
PNG.bitblt(image, preparedImage, 0, 0, image.width, image.height, 0, 0);
PNG.bitblt(image, preparedImage, 0, 0, image.width, image.height);
return preparedImage;
} else {
return image;
}
return image;
}

getDiff(baseline: PNG, image: PNG, testRun: TestRun): DiffResult {
Expand All @@ -290,48 +289,47 @@ export class TestRunsService {
isSameDimension: undefined,
};

if (baseline) {
result.isSameDimension = baseline.width === image.width && baseline.height === image.height;

const width = Math.max(baseline.width, image.width);
const height = Math.max(baseline.height, image.height);

const prepearedBaseline = this.prepareImage(baseline, width, height);
const preparedImage = this.prepareImage(image, width, height);
if (!baseline) {
// no baseline
return result;
}

// if (result.isSameDimension) {
const diff = new PNG({
width,
height,
});
result.isSameDimension = baseline.width === image.width && baseline.height === image.height;

const ignoreAreas = this.getIgnoteAreas(testRun);
// compare
result.pixelMisMatchCount = Pixelmatch(
this.applyIgnoreAreas(prepearedBaseline, ignoreAreas),
this.applyIgnoreAreas(preparedImage, ignoreAreas),
diff.data,
width,
height,
{
includeAA: true,
}
);
result.diffPercent = (result.pixelMisMatchCount * 100) / (image.width * image.height);

if (result.diffPercent > testRun.diffTollerancePercent) {
// save diff
result.diffName = this.staticService.saveImage('diff', PNG.sync.write(diff));
result.status = TestStatus.unresolved;
} else {
result.status = TestStatus.ok;
}
// } else {
// // diff dimensions
// result.status = TestStatus.unresolved;
// }
if (!result.isSameDimension && !process.env.ALLOW_DIFF_DIMENSIONS) {
// diff dimensions
result.status = TestStatus.unresolved;
return result;
}
// scale image to max size
const maxWidth = Math.max(baseline.width, image.width);
const maxHeight = Math.max(baseline.height, image.height);
const scaledBaseline = this.scaleImageToSize(baseline, maxWidth, maxHeight);
const scaledImage = this.scaleImageToSize(image, maxWidth, maxHeight);

// apply ignore areas
const ignoreAreas = this.getIgnoteAreas(testRun);
const baselineData = this.applyIgnoreAreas(scaledBaseline, ignoreAreas);
const imageData = this.applyIgnoreAreas(scaledImage, ignoreAreas);

// compare
const diff = new PNG({
width: maxWidth,
height: maxHeight,
});
result.pixelMisMatchCount = Pixelmatch(baselineData, imageData, diff.data, maxWidth, maxHeight, {
includeAA: true,
});
result.diffPercent = (result.pixelMisMatchCount * 100) / (scaledImage.width * scaledImage.height);

// process result
if (result.diffPercent > testRun.diffTollerancePercent) {
// save diff
result.diffName = this.staticService.saveImage('diff', PNG.sync.write(diff));
result.status = TestStatus.unresolved;
} else {
result.status = TestStatus.ok;
}
return result;
}

Expand Down