creevey 0.9.1 → 0.10.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/chromatic.config.json +5 -0
- package/dist/client/addon/components/Addon.d.ts +1 -0
- package/dist/client/addon/components/Addon.js.map +1 -1
- package/dist/client/addon/components/Icons.d.ts +1 -0
- package/dist/client/addon/components/Icons.js.map +1 -1
- package/dist/client/addon/components/Panel.d.ts +1 -0
- package/dist/client/addon/components/Panel.js.map +1 -1
- package/dist/client/addon/components/TestSelect.d.ts +1 -0
- package/dist/client/addon/components/TestSelect.js +4 -3
- package/dist/client/addon/components/TestSelect.js.map +1 -1
- package/dist/client/addon/components/Tools.d.ts +1 -0
- package/dist/client/addon/components/Tools.js +7 -8
- package/dist/client/addon/components/Tools.js.map +1 -1
- package/dist/client/addon/controller.d.ts +1 -1
- package/dist/client/addon/controller.js.map +1 -1
- package/dist/client/addon/decorator.d.ts +1 -1
- package/dist/client/addon/manager.js +3 -2
- package/dist/client/addon/manager.js.map +1 -1
- package/dist/client/addon/preview.d.ts +1 -1
- package/dist/client/addon/withCreevey.d.ts +6 -8
- package/dist/client/addon/withCreevey.js +21 -19
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/ImagesView.d.ts +1 -0
- package/dist/client/shared/components/ImagesView/ImagesView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
- package/dist/client/shared/components/PageFooter/PageFooter.d.ts +1 -0
- package/dist/client/shared/components/PageFooter/PageFooter.js +1 -1
- package/dist/client/shared/components/PageFooter/PageFooter.js.map +1 -1
- package/dist/client/shared/components/PageFooter/Paging.d.ts +2 -2
- package/dist/client/shared/components/PageFooter/Paging.js +8 -6
- package/dist/client/shared/components/PageFooter/Paging.js.map +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.d.ts +1 -0
- package/dist/client/shared/components/PageHeader/PageHeader.js +2 -1
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.d.ts +2 -2
- package/dist/client/shared/components/ResultsPage.js.map +1 -1
- package/dist/client/web/CreeveyApp.d.ts +1 -0
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyLoader.d.ts +1 -0
- package/dist/client/web/CreeveyLoader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Checkbox.d.ts +1 -1
- package/dist/client/web/CreeveyView/SideBar/Checkbox.js +4 -4
- package/dist/client/web/CreeveyView/SideBar/Checkbox.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Search.d.ts +1 -0
- package/dist/client/web/CreeveyView/SideBar/Search.js +4 -4
- package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBar.d.ts +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBar.js +1 -7
- package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.d.ts +1 -0
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +5 -4
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.d.ts +1 -0
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +4 -3
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +3 -7
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +6 -5
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.d.ts +1 -0
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +5 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js +15 -8
- package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +5 -4
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Toggle.d.ts +1 -0
- package/dist/client/web/CreeveyView/SideBar/Toggle.js.map +1 -1
- package/dist/client/web/KeyboardEventsContext.d.ts +3 -4
- package/dist/client/web/KeyboardEventsContext.js.map +1 -1
- package/dist/client/web/assets/index-DkmZfG9C.js +591 -0
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +5 -6
- package/dist/client/web/index.js.map +1 -1
- package/dist/creevey.js +21 -9
- package/dist/creevey.js.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/server/config.d.ts +1 -1
- package/dist/server/config.js +9 -5
- package/dist/server/config.js.map +1 -1
- package/dist/server/docker.d.ts +2 -2
- package/dist/server/docker.js +46 -40
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +54 -15
- package/dist/server/index.js.map +1 -1
- package/dist/server/master/master.d.ts +1 -5
- package/dist/server/master/master.js +3 -3
- package/dist/server/master/master.js.map +1 -1
- package/dist/server/master/pool.d.ts +2 -1
- package/dist/server/master/pool.js +13 -7
- package/dist/server/master/pool.js.map +1 -1
- package/dist/server/master/runner.d.ts +1 -1
- package/dist/server/master/runner.js +4 -2
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +1 -0
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.d.ts +3 -0
- package/dist/server/master/{index.js → start.js} +6 -9
- package/dist/server/master/start.js.map +1 -0
- package/dist/server/messages.d.ts +4 -10
- package/dist/server/messages.js +4 -58
- package/dist/server/messages.js.map +1 -1
- package/dist/server/playwright/docker-file.d.ts +1 -0
- package/dist/server/playwright/docker-file.js +26 -0
- package/dist/server/playwright/docker-file.js.map +1 -0
- package/dist/server/playwright/docker.d.ts +1 -0
- package/dist/server/playwright/docker.js +31 -0
- package/dist/server/playwright/docker.js.map +1 -0
- package/dist/server/playwright/internal.d.ts +25 -0
- package/dist/server/playwright/internal.js +319 -0
- package/dist/server/playwright/internal.js.map +1 -0
- package/dist/server/playwright/webdriver.d.ts +16 -0
- package/dist/server/playwright/webdriver.js +105 -0
- package/dist/server/playwright/webdriver.js.map +1 -0
- package/dist/server/providers/browser.d.ts +2 -0
- package/dist/server/{storybook/providers → providers}/browser.js +6 -7
- package/dist/server/providers/browser.js.map +1 -0
- package/dist/server/providers/hybrid.d.ts +2 -0
- package/dist/server/{storybook/providers → providers}/hybrid.js +8 -8
- package/dist/server/providers/hybrid.js.map +1 -0
- package/dist/server/reporter.d.ts +26 -0
- package/dist/server/{worker/reporter.js → reporter.js} +34 -56
- package/dist/server/reporter.js.map +1 -0
- package/dist/server/selenium/internal.d.ts +31 -0
- package/dist/server/selenium/internal.js +606 -0
- package/dist/server/selenium/internal.js.map +1 -0
- package/dist/server/selenium/selenoid.js +6 -13
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/selenium/webdriver.d.ts +24 -0
- package/dist/server/selenium/webdriver.js +106 -0
- package/dist/server/selenium/webdriver.js.map +1 -0
- package/dist/server/stories.js +16 -9
- package/dist/server/stories.js.map +1 -1
- package/dist/server/telemetry.d.ts +1 -1
- package/dist/server/telemetry.js +4 -4
- package/dist/server/telemetry.js.map +1 -1
- package/dist/server/utils.d.ts +3 -4
- package/dist/server/utils.js +10 -9
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +19 -0
- package/dist/server/webdriver.js +79 -0
- package/dist/server/webdriver.js.map +1 -0
- package/dist/server/worker/chai-image.d.ts +2 -5
- package/dist/server/worker/chai-image.js +14 -102
- package/dist/server/worker/chai-image.js.map +1 -1
- package/dist/server/worker/match-image.d.ts +14 -0
- package/dist/server/worker/match-image.js +231 -0
- package/dist/server/worker/match-image.js.map +1 -0
- package/dist/server/worker/start.d.ts +2 -0
- package/dist/server/worker/start.js +258 -0
- package/dist/server/worker/start.js.map +1 -0
- package/dist/types.d.ts +127 -64
- package/dist/types.js +15 -9
- package/dist/types.js.map +1 -1
- package/package.json +108 -110
- package/src/client/addon/components/Addon.tsx +1 -1
- package/src/client/addon/components/Icons.tsx +1 -1
- package/src/client/addon/components/Panel.tsx +1 -1
- package/src/client/addon/components/TestSelect.tsx +5 -5
- package/src/client/addon/components/Tools.tsx +9 -9
- package/src/client/addon/controller.ts +1 -1
- package/src/client/addon/manager.ts +4 -4
- package/src/client/addon/withCreevey.ts +26 -28
- package/src/client/shared/components/ImagesView/BlendView.tsx +1 -1
- package/src/client/shared/components/ImagesView/ImagesView.tsx +2 -2
- package/src/client/shared/components/ImagesView/SideBySideView.tsx +1 -1
- package/src/client/shared/components/ImagesView/SlideView.tsx +1 -1
- package/src/client/shared/components/ImagesView/SwapView.tsx +1 -1
- package/src/client/shared/components/PageFooter/PageFooter.tsx +2 -2
- package/src/client/shared/components/PageFooter/Paging.tsx +13 -13
- package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -1
- package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -3
- package/src/client/shared/components/ResultsPage.tsx +1 -1
- package/src/client/web/CreeveyApp.tsx +1 -1
- package/src/client/web/CreeveyLoader.tsx +1 -1
- package/src/client/web/CreeveyView/SideBar/Checkbox.tsx +6 -7
- package/src/client/web/CreeveyView/SideBar/Search.tsx +4 -4
- package/src/client/web/CreeveyView/SideBar/SideBar.tsx +3 -10
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +7 -6
- package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +7 -6
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +8 -6
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -3
- package/src/client/web/CreeveyView/SideBar/TestStatusIcon.tsx +18 -10
- package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +7 -10
- package/src/client/web/CreeveyView/SideBar/Toggle.tsx +1 -2
- package/src/client/web/KeyboardEventsContext.tsx +3 -4
- package/src/client/web/index.html +1 -1
- package/src/client/web/index.tsx +4 -3
- package/src/creevey.ts +25 -8
- package/src/index.ts +4 -2
- package/src/server/config.ts +12 -8
- package/src/server/docker.ts +58 -44
- package/src/server/index.ts +57 -18
- package/src/server/master/master.ts +3 -6
- package/src/server/master/pool.ts +25 -9
- package/src/server/master/runner.ts +4 -2
- package/src/server/master/server.ts +1 -0
- package/src/server/master/{index.ts → start.ts} +13 -11
- package/src/server/messages.ts +11 -75
- package/src/server/playwright/docker-file.ts +21 -0
- package/src/server/playwright/docker.ts +41 -0
- package/src/server/playwright/internal.ts +387 -0
- package/src/server/playwright/webdriver.ts +126 -0
- package/src/server/{storybook/providers → providers}/browser.ts +7 -8
- package/src/server/{storybook/providers → providers}/hybrid.ts +19 -19
- package/src/server/{worker/reporter.ts → reporter.ts} +40 -72
- package/src/server/selenium/internal.ts +785 -0
- package/src/server/selenium/selenoid.ts +12 -17
- package/src/server/selenium/webdriver.ts +136 -0
- package/src/server/stories.ts +18 -11
- package/src/server/telemetry.ts +2 -2
- package/src/server/utils.ts +9 -9
- package/src/server/webdriver.ts +127 -0
- package/src/server/worker/chai-image.ts +21 -133
- package/src/server/worker/match-image.ts +303 -0
- package/src/server/worker/start.ts +303 -0
- package/src/types.ts +162 -60
- package/dist/client/web/202.js +0 -1
- package/dist/client/web/270.js +0 -43
- package/dist/client/web/752.js +0 -1
- package/dist/client/web/main.js +0 -79
- package/dist/client/web/main.js.LICENSE.txt +0 -34
- package/dist/server/master/index.d.ts +0 -3
- package/dist/server/master/index.js.map +0 -1
- package/dist/server/selenium/browser.d.ts +0 -19
- package/dist/server/selenium/browser.js +0 -640
- package/dist/server/selenium/browser.js.map +0 -1
- package/dist/server/selenium/index.d.ts +0 -2
- package/dist/server/selenium/index.js +0 -19
- package/dist/server/selenium/index.js.map +0 -1
- package/dist/server/storybook/providers/browser.d.ts +0 -2
- package/dist/server/storybook/providers/browser.js.map +0 -1
- package/dist/server/storybook/providers/hybrid.d.ts +0 -2
- package/dist/server/storybook/providers/hybrid.js.map +0 -1
- package/dist/server/worker/helpers.d.ts +0 -8
- package/dist/server/worker/helpers.js +0 -57
- package/dist/server/worker/helpers.js.map +0 -1
- package/dist/server/worker/index.d.ts +0 -1
- package/dist/server/worker/index.js +0 -6
- package/dist/server/worker/index.js.map +0 -1
- package/dist/server/worker/reporter.d.ts +0 -8
- package/dist/server/worker/reporter.js.map +0 -1
- package/dist/server/worker/worker.d.ts +0 -4
- package/dist/server/worker/worker.js +0 -212
- package/dist/server/worker/worker.js.map +0 -1
- package/src/server/selenium/browser.ts +0 -840
- package/src/server/selenium/index.ts +0 -2
- package/src/server/worker/helpers.ts +0 -61
- package/src/server/worker/index.ts +0 -1
- package/src/server/worker/worker.ts +0 -240
- package/types/mocha.d.ts +0 -20
@@ -0,0 +1,303 @@
|
|
1
|
+
import path from 'path';
|
2
|
+
import { Stats } from 'fs';
|
3
|
+
import { mkdir, readdir, readFile, stat, writeFile } from 'fs/promises';
|
4
|
+
import { compare } from 'odiff-bin';
|
5
|
+
import { PNG } from 'pngjs';
|
6
|
+
import { Config, DiffOptions, Images, ImagesError } from '../../types.js';
|
7
|
+
import assert from 'assert';
|
8
|
+
|
9
|
+
export interface ImageContext {
|
10
|
+
attachments: string[];
|
11
|
+
testFullPath: string[];
|
12
|
+
images: Partial<Record<string, Images>>;
|
13
|
+
}
|
14
|
+
|
15
|
+
interface ImagePaths {
|
16
|
+
imageName: string;
|
17
|
+
actualImageName: string;
|
18
|
+
expectImageName: string;
|
19
|
+
diffImageName: string;
|
20
|
+
expectImageDir: string;
|
21
|
+
reportImageDir: string;
|
22
|
+
}
|
23
|
+
|
24
|
+
async function getStat(filePath: string): Promise<Stats | null> {
|
25
|
+
try {
|
26
|
+
return await stat(filePath);
|
27
|
+
} catch (error) {
|
28
|
+
if (typeof error == 'object' && error && (error as { code?: unknown }).code === 'ENOENT') {
|
29
|
+
return null;
|
30
|
+
}
|
31
|
+
throw error;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
async function getLastImageNumber(imageDir: string, imageName: string): Promise<number> {
|
36
|
+
const actualImagesRegexp = new RegExp(`${imageName}-actual-(\\d+)\\.png`);
|
37
|
+
|
38
|
+
try {
|
39
|
+
return (
|
40
|
+
(await readdir(imageDir))
|
41
|
+
.map((filename) => filename.replace(actualImagesRegexp, '$1'))
|
42
|
+
.map(Number)
|
43
|
+
.filter((x) => !isNaN(x))
|
44
|
+
.sort((a, b) => b - a)[0] ?? 0
|
45
|
+
);
|
46
|
+
} catch (_error) {
|
47
|
+
return 0;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
async function readExpected(expectImageDir: string, imageName: string): Promise<Buffer> {
|
52
|
+
const expected = await readFile(path.join(expectImageDir, `${imageName}.png`));
|
53
|
+
|
54
|
+
return expected;
|
55
|
+
}
|
56
|
+
|
57
|
+
async function saveImages(imageDir: string, images: { name: string; data: Buffer }[]): Promise<string[]> {
|
58
|
+
const files: string[] = [];
|
59
|
+
await mkdir(imageDir, { recursive: true });
|
60
|
+
for (const { name, data } of images) {
|
61
|
+
const filePath = path.join(imageDir, name);
|
62
|
+
await writeFile(filePath, data);
|
63
|
+
files.push(filePath);
|
64
|
+
}
|
65
|
+
return files;
|
66
|
+
}
|
67
|
+
|
68
|
+
async function getImagePaths(config: Config, testFullPath: string[], assertImageName?: string): Promise<ImagePaths> {
|
69
|
+
const testPath = [...testFullPath];
|
70
|
+
const imageName = assertImageName ?? testPath.pop();
|
71
|
+
|
72
|
+
assert(typeof imageName === 'string', `Can't get image name from empty test scope`);
|
73
|
+
|
74
|
+
const expectImageDir = path.join(config.screenDir, ...testPath);
|
75
|
+
const reportImageDir = path.join(config.reportDir, ...testPath);
|
76
|
+
const imageNumber = (await getLastImageNumber(reportImageDir, imageName)) + 1;
|
77
|
+
const actualImageName = `${imageName}-actual-${imageNumber}.png`;
|
78
|
+
const expectImageName = `${imageName}-expect-${imageNumber}.png`;
|
79
|
+
const diffImageName = `${imageName}-diff-${imageNumber}.png`;
|
80
|
+
|
81
|
+
return { imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir };
|
82
|
+
}
|
83
|
+
|
84
|
+
async function getExpected(
|
85
|
+
ctx: ImageContext,
|
86
|
+
{ imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir }: ImagePaths,
|
87
|
+
): Promise<{
|
88
|
+
expected: Buffer | null;
|
89
|
+
onCompare: (actual: Buffer, expect?: Buffer, diff?: Buffer) => Promise<void>;
|
90
|
+
}> {
|
91
|
+
const onCompare = async (actual: Buffer, expect?: Buffer, diff?: Buffer): Promise<void> => {
|
92
|
+
const imagesMeta: { name: string; data: Buffer }[] = [];
|
93
|
+
const image = (ctx.images[imageName] = ctx.images[imageName] ?? { actual: actualImageName });
|
94
|
+
|
95
|
+
imagesMeta.push({ name: image.actual, data: actual });
|
96
|
+
|
97
|
+
if (diff && expect) {
|
98
|
+
image.expect = expectImageName;
|
99
|
+
image.diff = diffImageName;
|
100
|
+
imagesMeta.push({ name: image.expect, data: expect });
|
101
|
+
imagesMeta.push({ name: image.diff, data: diff });
|
102
|
+
}
|
103
|
+
ctx.attachments = await saveImages(reportImageDir, imagesMeta);
|
104
|
+
};
|
105
|
+
|
106
|
+
const expectImageStat = await getStat(path.join(expectImageDir, `${imageName}.png`));
|
107
|
+
if (!expectImageStat) return { expected: null, onCompare };
|
108
|
+
|
109
|
+
const expected = await readExpected(expectImageDir, imageName);
|
110
|
+
|
111
|
+
return { expected, onCompare };
|
112
|
+
}
|
113
|
+
|
114
|
+
async function getOdiffExpected(
|
115
|
+
ctx: ImageContext,
|
116
|
+
actual: Buffer,
|
117
|
+
{ imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir }: ImagePaths,
|
118
|
+
): Promise<{ actual: string; expect: string; diff: string }> {
|
119
|
+
const expected = await readExpected(expectImageDir, imageName);
|
120
|
+
|
121
|
+
const image = (ctx.images[imageName] = ctx.images[imageName] ?? { actual: actualImageName });
|
122
|
+
image.expect = expectImageName;
|
123
|
+
image.diff = diffImageName;
|
124
|
+
|
125
|
+
const imagesMeta = [
|
126
|
+
{ name: image.actual, data: actual },
|
127
|
+
{ name: expectImageName, data: expected },
|
128
|
+
];
|
129
|
+
|
130
|
+
ctx.attachments = await saveImages(reportImageDir, imagesMeta);
|
131
|
+
|
132
|
+
return {
|
133
|
+
actual: path.join(reportImageDir, actualImageName),
|
134
|
+
expect: path.join(reportImageDir, expectImageName),
|
135
|
+
diff: path.join(reportImageDir, diffImageName),
|
136
|
+
};
|
137
|
+
}
|
138
|
+
|
139
|
+
function normalizeImageSize(image: PNG, width: number, height: number): Buffer {
|
140
|
+
const normalizedImage = Buffer.alloc(4 * width * height);
|
141
|
+
|
142
|
+
for (let y = 0; y < height; y++) {
|
143
|
+
for (let x = 0; x < width; x++) {
|
144
|
+
const i = (y * width + x) * 4;
|
145
|
+
if (x < image.width && y < image.height) {
|
146
|
+
const j = (y * image.width + x) * 4;
|
147
|
+
normalizedImage[i + 0] = image.data[j + 0];
|
148
|
+
normalizedImage[i + 1] = image.data[j + 1];
|
149
|
+
normalizedImage[i + 2] = image.data[j + 2];
|
150
|
+
normalizedImage[i + 3] = image.data[j + 3];
|
151
|
+
} else {
|
152
|
+
normalizedImage[i + 0] = 0;
|
153
|
+
normalizedImage[i + 1] = 0;
|
154
|
+
normalizedImage[i + 2] = 0;
|
155
|
+
normalizedImage[i + 3] = 0;
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
return normalizedImage;
|
160
|
+
}
|
161
|
+
|
162
|
+
function hasDiffPixels(diff: Buffer): boolean {
|
163
|
+
for (let i = 0; i < diff.length; i += 4) {
|
164
|
+
if (diff[i + 0] == 255 && diff[i + 1] == 0 && diff[i + 2] == 0 && diff[i + 3] == 255) return true;
|
165
|
+
}
|
166
|
+
return false;
|
167
|
+
}
|
168
|
+
|
169
|
+
function compareImages(
|
170
|
+
expect: Buffer,
|
171
|
+
actual: Buffer,
|
172
|
+
pixelmatch: typeof import('pixelmatch'),
|
173
|
+
diffOptions: DiffOptions,
|
174
|
+
): { isEqual: boolean; diff: Buffer } {
|
175
|
+
const expectImage = PNG.sync.read(expect);
|
176
|
+
const actualImage = PNG.sync.read(actual);
|
177
|
+
|
178
|
+
const width = Math.max(actualImage.width, expectImage.width);
|
179
|
+
const height = Math.max(actualImage.height, expectImage.height);
|
180
|
+
|
181
|
+
const diffImage = new PNG({ width, height });
|
182
|
+
|
183
|
+
let actualImageData = actualImage.data;
|
184
|
+
if (actualImage.width < width || actualImage.height < height) {
|
185
|
+
actualImageData = normalizeImageSize(actualImage, width, height);
|
186
|
+
}
|
187
|
+
|
188
|
+
let expectImageData = expectImage.data;
|
189
|
+
if (expectImage.width < width || expectImage.height < height) {
|
190
|
+
expectImageData = normalizeImageSize(expectImage, width, height);
|
191
|
+
}
|
192
|
+
|
193
|
+
pixelmatch(expectImageData, actualImageData, diffImage.data, width, height, diffOptions);
|
194
|
+
|
195
|
+
return {
|
196
|
+
isEqual: !hasDiffPixels(diffImage.data),
|
197
|
+
diff: PNG.sync.write(diffImage),
|
198
|
+
};
|
199
|
+
}
|
200
|
+
|
201
|
+
export async function getMatchers(ctx: ImageContext, config: Config) {
|
202
|
+
// TODO Replace with `import from`
|
203
|
+
const { default: pixelmatch } = await import('pixelmatch');
|
204
|
+
|
205
|
+
async function assertImage(actual: Buffer, imageName?: string): Promise<string | undefined> {
|
206
|
+
const { expected, onCompare } = await getExpected(ctx, await getImagePaths(config, ctx.testFullPath, imageName));
|
207
|
+
|
208
|
+
if (expected == null) {
|
209
|
+
await onCompare(actual);
|
210
|
+
return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
|
211
|
+
}
|
212
|
+
|
213
|
+
if (actual.equals(expected)) {
|
214
|
+
await onCompare(actual);
|
215
|
+
return;
|
216
|
+
}
|
217
|
+
|
218
|
+
const { isEqual, diff } = compareImages(expected, actual, pixelmatch, config.diffOptions);
|
219
|
+
|
220
|
+
if (isEqual) {
|
221
|
+
await onCompare(actual);
|
222
|
+
return;
|
223
|
+
}
|
224
|
+
|
225
|
+
await onCompare(actual, expected, diff);
|
226
|
+
|
227
|
+
return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
|
228
|
+
}
|
229
|
+
|
230
|
+
return {
|
231
|
+
matchImage: async (image: Buffer, imageName?: string) => {
|
232
|
+
const errorMessage = await assertImage(image, imageName);
|
233
|
+
if (errorMessage) {
|
234
|
+
throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
|
235
|
+
}
|
236
|
+
},
|
237
|
+
matchImages: async (images: Record<string, Buffer>) => {
|
238
|
+
const errors: Record<string, string> = {};
|
239
|
+
await Promise.all(
|
240
|
+
Object.entries(images).map(async ([imageName, image]) => {
|
241
|
+
const errorMessage = await assertImage(image, imageName);
|
242
|
+
if (errorMessage) {
|
243
|
+
errors[imageName] = errorMessage;
|
244
|
+
}
|
245
|
+
}),
|
246
|
+
);
|
247
|
+
if (Object.keys(errors).length > 0) {
|
248
|
+
throw createImageError(errors);
|
249
|
+
}
|
250
|
+
},
|
251
|
+
};
|
252
|
+
}
|
253
|
+
|
254
|
+
function createImageError(imageErrors: string | Partial<Record<string, string>>): ImagesError {
|
255
|
+
const error = new ImagesError('Expected image to match');
|
256
|
+
error.images = imageErrors;
|
257
|
+
return error;
|
258
|
+
}
|
259
|
+
|
260
|
+
export function getOdiffMatchers(ctx: ImageContext, config: Config) {
|
261
|
+
const diffOptions = {
|
262
|
+
...config.odiffOptions,
|
263
|
+
noFailOnFsErrors: true,
|
264
|
+
};
|
265
|
+
|
266
|
+
async function assertImage(image: Buffer, imageName?: string): Promise<string | undefined> {
|
267
|
+
const { actual, expect, diff } = await getOdiffExpected(
|
268
|
+
ctx,
|
269
|
+
image,
|
270
|
+
await getImagePaths(config, ctx.testFullPath, imageName),
|
271
|
+
);
|
272
|
+
const result = await compare(actual, expect, diff, diffOptions);
|
273
|
+
if (!result.match) {
|
274
|
+
if (result.reason == 'file-not-exists') {
|
275
|
+
return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
|
276
|
+
}
|
277
|
+
return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
|
278
|
+
}
|
279
|
+
}
|
280
|
+
|
281
|
+
return {
|
282
|
+
matchImage: async (image: Buffer, imageName?: string) => {
|
283
|
+
const errorMessage = await assertImage(image, imageName);
|
284
|
+
if (errorMessage) {
|
285
|
+
throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
|
286
|
+
}
|
287
|
+
},
|
288
|
+
matchImages: async (images: Record<string, Buffer>) => {
|
289
|
+
const errors: Record<string, string> = {};
|
290
|
+
await Promise.all(
|
291
|
+
Object.entries(images).map(async ([imageName, image]) => {
|
292
|
+
const errorMessage = await assertImage(image, imageName);
|
293
|
+
if (errorMessage) {
|
294
|
+
errors[imageName] = errorMessage;
|
295
|
+
}
|
296
|
+
}),
|
297
|
+
);
|
298
|
+
if (Object.keys(errors).length > 0) {
|
299
|
+
throw createImageError(errors);
|
300
|
+
}
|
301
|
+
},
|
302
|
+
};
|
303
|
+
}
|
@@ -0,0 +1,303 @@
|
|
1
|
+
import chai from 'chai';
|
2
|
+
import chalk from 'chalk';
|
3
|
+
import Logger from 'loglevel';
|
4
|
+
import EventEmitter from 'events';
|
5
|
+
import { Key, until, WebDriver } from 'selenium-webdriver';
|
6
|
+
import {
|
7
|
+
BaseCreeveyTestContext,
|
8
|
+
Config,
|
9
|
+
CreeveyWebdriver,
|
10
|
+
FakeSuite,
|
11
|
+
FakeTest,
|
12
|
+
Images,
|
13
|
+
Options,
|
14
|
+
ServerTest,
|
15
|
+
TEST_EVENTS,
|
16
|
+
TestMessage,
|
17
|
+
isDefined,
|
18
|
+
isImageError,
|
19
|
+
} from '../../types.js';
|
20
|
+
import { subscribeOn, emitTestMessage, emitWorkerMessage } from '../messages.js';
|
21
|
+
import chaiImage from './chai-image.js';
|
22
|
+
import { SeleniumWebdriver } from '../selenium/webdriver.js';
|
23
|
+
import { getMatchers, getOdiffMatchers, ImageContext } from './match-image.js';
|
24
|
+
import { loadTestsFromStories } from '../stories.js';
|
25
|
+
import { logger } from '../logger.js';
|
26
|
+
import { getTestPath } from '../utils.js';
|
27
|
+
|
28
|
+
async function getTestsFromStories(
|
29
|
+
config: Config,
|
30
|
+
browserName: string,
|
31
|
+
webdriver: CreeveyWebdriver,
|
32
|
+
): Promise<Map<string, ServerTest>> {
|
33
|
+
const testsById = new Map<string, ServerTest>();
|
34
|
+
const tests = await loadTestsFromStories(
|
35
|
+
[browserName],
|
36
|
+
(listener) => config.storiesProvider(config, listener, webdriver),
|
37
|
+
(testsDiff) => {
|
38
|
+
Object.entries(testsDiff).forEach(([id, newTest]) => {
|
39
|
+
if (newTest) testsById.set(id, newTest);
|
40
|
+
else testsById.delete(id);
|
41
|
+
});
|
42
|
+
},
|
43
|
+
);
|
44
|
+
|
45
|
+
Object.values(tests)
|
46
|
+
.filter(isDefined)
|
47
|
+
.forEach((test) => testsById.set(test.id, test));
|
48
|
+
|
49
|
+
return testsById;
|
50
|
+
}
|
51
|
+
|
52
|
+
async function outputTraceLogs(browser: WebDriver, test: ServerTest, logger: Logger.Logger): Promise<void> {
|
53
|
+
const output: string[] = [];
|
54
|
+
const types = await browser.manage().logs().getAvailableLogTypes();
|
55
|
+
for (const type of types) {
|
56
|
+
const logs = await browser.manage().logs().get(type);
|
57
|
+
output.push(logs.map((log) => JSON.stringify(log.toJSON(), null, 2)).join('\n'));
|
58
|
+
}
|
59
|
+
logger.debug(
|
60
|
+
'----------',
|
61
|
+
getTestPath(test).join('/'),
|
62
|
+
'----------\n',
|
63
|
+
output.join('\n'),
|
64
|
+
'\n----------------------------------------------------------------------------------------------------',
|
65
|
+
);
|
66
|
+
}
|
67
|
+
|
68
|
+
function runHandler(browserName: string, images: Partial<Record<string, Images>>, error?: unknown): void {
|
69
|
+
// TODO How handle browser corruption?
|
70
|
+
if (isImageError(error)) {
|
71
|
+
if (typeof error.images == 'string') {
|
72
|
+
const image = images[browserName];
|
73
|
+
if (image) image.error = error.images;
|
74
|
+
} else {
|
75
|
+
const imageErrors = error.images ?? {};
|
76
|
+
Object.keys(imageErrors).forEach((imageName) => {
|
77
|
+
const image = images[imageName];
|
78
|
+
if (image) image.error = imageErrors[imageName];
|
79
|
+
});
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
if (error || Object.values(images).some((image) => image?.error != null)) {
|
84
|
+
const errorMessage = serializeError(error);
|
85
|
+
|
86
|
+
const isUnexpectedError =
|
87
|
+
hasTimeout(errorMessage) ||
|
88
|
+
hasDisconnected(errorMessage) ||
|
89
|
+
Object.values(images).some((image) => hasTimeout(image?.error));
|
90
|
+
if (isUnexpectedError) emitWorkerMessage({ type: 'error', payload: { subtype: 'unknown', error: errorMessage } });
|
91
|
+
else
|
92
|
+
emitTestMessage({
|
93
|
+
type: 'end',
|
94
|
+
payload: {
|
95
|
+
status: 'failed',
|
96
|
+
images,
|
97
|
+
error: errorMessage,
|
98
|
+
},
|
99
|
+
});
|
100
|
+
} else {
|
101
|
+
emitTestMessage({ type: 'end', payload: { status: 'success', images } });
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
async function setupWebdriver(webdriver: CreeveyWebdriver): Promise<[string, CreeveyWebdriver] | undefined> {
|
106
|
+
if ((await webdriver.openBrowser(true)) == null) {
|
107
|
+
logger.error('Failed to start browser');
|
108
|
+
emitWorkerMessage({
|
109
|
+
type: 'error',
|
110
|
+
payload: { subtype: 'browser', error: 'Failed to start browser' },
|
111
|
+
});
|
112
|
+
return;
|
113
|
+
}
|
114
|
+
|
115
|
+
const sessionId = await webdriver.getSessionId();
|
116
|
+
|
117
|
+
return [sessionId, webdriver];
|
118
|
+
}
|
119
|
+
|
120
|
+
function serializeError(error: unknown): string {
|
121
|
+
if (!error) return 'Unknown error';
|
122
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
123
|
+
return typeof error === 'object' ? JSON.stringify(error) : (error as string);
|
124
|
+
}
|
125
|
+
|
126
|
+
function hasDisconnected(str: string | null | undefined): boolean {
|
127
|
+
return str?.toLowerCase().includes('disconnected') ?? false;
|
128
|
+
}
|
129
|
+
|
130
|
+
function hasTimeout(str: string | null | undefined): boolean {
|
131
|
+
return str?.toLowerCase().includes('timeout') ?? false;
|
132
|
+
}
|
133
|
+
|
134
|
+
export async function start(browser: string, gridUrl: string, config: Config, options: Options): Promise<void> {
|
135
|
+
let retries = 0;
|
136
|
+
const imagesContext: ImageContext = {
|
137
|
+
attachments: [],
|
138
|
+
testFullPath: [],
|
139
|
+
images: {},
|
140
|
+
};
|
141
|
+
const Webdriver = config.webdriver;
|
142
|
+
const [sessionId, webdriver] = (await setupWebdriver(new Webdriver(browser, gridUrl, config, options))) ?? [];
|
143
|
+
|
144
|
+
if (!webdriver || !sessionId) return;
|
145
|
+
|
146
|
+
const workerLogger = Logger.getLogger(`${browser}:${chalk.gray(sessionId)}`);
|
147
|
+
|
148
|
+
const reporterOptions = {
|
149
|
+
...config.reporterOptions,
|
150
|
+
creevey: {
|
151
|
+
sessionId,
|
152
|
+
reportDir: config.reportDir,
|
153
|
+
browserName: browser,
|
154
|
+
get willRetry() {
|
155
|
+
return retries < config.maxRetries;
|
156
|
+
},
|
157
|
+
get images() {
|
158
|
+
return imagesContext.images;
|
159
|
+
},
|
160
|
+
},
|
161
|
+
};
|
162
|
+
|
163
|
+
class FakeRunner extends EventEmitter {}
|
164
|
+
const runner = new FakeRunner();
|
165
|
+
const Reporter = config.reporter;
|
166
|
+
new Reporter(runner, { reporterOptions });
|
167
|
+
|
168
|
+
const { matchImage, matchImages } = options.odiff
|
169
|
+
? getOdiffMatchers(imagesContext, config)
|
170
|
+
: await getMatchers(imagesContext, config);
|
171
|
+
chai.use(chaiImage(matchImage, matchImages, workerLogger));
|
172
|
+
|
173
|
+
const tests = await (async () => {
|
174
|
+
try {
|
175
|
+
return await getTestsFromStories(config, browser, webdriver);
|
176
|
+
} catch (error) {
|
177
|
+
workerLogger.error('Failed to get tests from stories:', error);
|
178
|
+
emitWorkerMessage({
|
179
|
+
type: 'error',
|
180
|
+
payload: { subtype: 'browser', error: serializeError(error) },
|
181
|
+
});
|
182
|
+
return null;
|
183
|
+
}
|
184
|
+
})();
|
185
|
+
|
186
|
+
if (!tests) return;
|
187
|
+
|
188
|
+
subscribeOn('test', (message: TestMessage) => {
|
189
|
+
if (message.type != 'start') return;
|
190
|
+
|
191
|
+
const test = tests.get(message.payload.id);
|
192
|
+
|
193
|
+
if (!test) {
|
194
|
+
const error = `Test with id ${message.payload.id} not found`;
|
195
|
+
workerLogger.error(error);
|
196
|
+
emitWorkerMessage({
|
197
|
+
type: 'error',
|
198
|
+
payload: { subtype: 'test', error },
|
199
|
+
});
|
200
|
+
return;
|
201
|
+
}
|
202
|
+
|
203
|
+
const baseContext: BaseCreeveyTestContext = {
|
204
|
+
browserName: browser,
|
205
|
+
// TODO Pass browser instance to test
|
206
|
+
// browser: browser,
|
207
|
+
screenshots: [],
|
208
|
+
|
209
|
+
matchImage: matchImage,
|
210
|
+
matchImages: matchImages,
|
211
|
+
|
212
|
+
// NOTE: Deprecated
|
213
|
+
expect: chai.expect,
|
214
|
+
//TODO Move things below to the separate module
|
215
|
+
until: until,
|
216
|
+
keys: Key,
|
217
|
+
};
|
218
|
+
|
219
|
+
imagesContext.attachments = [];
|
220
|
+
imagesContext.testFullPath = getTestPath(test);
|
221
|
+
imagesContext.images = {};
|
222
|
+
|
223
|
+
retries = message.payload.retries;
|
224
|
+
let error = undefined;
|
225
|
+
|
226
|
+
const fakeSuite: FakeSuite = {
|
227
|
+
title: test.storyPath.slice(0, -1).join('/'),
|
228
|
+
fullTitle: () => fakeSuite.title,
|
229
|
+
titlePath: () => [fakeSuite.title],
|
230
|
+
tests: [],
|
231
|
+
};
|
232
|
+
|
233
|
+
const fakeTest: FakeTest = {
|
234
|
+
parent: fakeSuite,
|
235
|
+
title: [test.story.name, test.testName, test.browser].filter(isDefined).join('/'),
|
236
|
+
fullTitle: () => getTestPath(test).join('/'),
|
237
|
+
titlePath: () => getTestPath(test),
|
238
|
+
currentRetry: () => retries,
|
239
|
+
retires: () => config.maxRetries,
|
240
|
+
slow: () => 1000,
|
241
|
+
};
|
242
|
+
|
243
|
+
fakeSuite.tests.push(fakeTest);
|
244
|
+
|
245
|
+
void (async () => {
|
246
|
+
runner.emit(TEST_EVENTS.RUN_BEGIN);
|
247
|
+
runner.emit(TEST_EVENTS.TEST_BEGIN, fakeTest);
|
248
|
+
|
249
|
+
const start = Date.now();
|
250
|
+
try {
|
251
|
+
await Promise.race([
|
252
|
+
new Promise((reject) =>
|
253
|
+
setTimeout(() => {
|
254
|
+
reject(`Timeout of ${config.testTimeout}ms exceeded`);
|
255
|
+
}, config.testTimeout),
|
256
|
+
),
|
257
|
+
(async () => {
|
258
|
+
const context = await webdriver.switchStory(test.story, baseContext, workerLogger);
|
259
|
+
await test.fn(context);
|
260
|
+
})(),
|
261
|
+
]);
|
262
|
+
} catch (testError) {
|
263
|
+
error = testError;
|
264
|
+
fakeTest.err = error;
|
265
|
+
}
|
266
|
+
const duration = Date.now() - start;
|
267
|
+
fakeTest.attachments = imagesContext.attachments;
|
268
|
+
fakeTest.state = error ? 'failed' : 'passed';
|
269
|
+
fakeTest.duration = duration;
|
270
|
+
fakeTest.speed = duration > fakeTest.slow() ? 'slow' : duration / 2 > fakeTest.slow() ? 'medium' : 'fast';
|
271
|
+
|
272
|
+
if (error) {
|
273
|
+
runner.emit(TEST_EVENTS.TEST_FAIL, fakeTest, error);
|
274
|
+
} else {
|
275
|
+
runner.emit(TEST_EVENTS.TEST_PASS, fakeTest);
|
276
|
+
}
|
277
|
+
runner.emit(TEST_EVENTS.TEST_END, fakeTest);
|
278
|
+
runner.emit(TEST_EVENTS.RUN_END);
|
279
|
+
|
280
|
+
if (options.trace) {
|
281
|
+
try {
|
282
|
+
if (webdriver instanceof SeleniumWebdriver && webdriver.browser) {
|
283
|
+
await outputTraceLogs(webdriver.browser, test, workerLogger);
|
284
|
+
}
|
285
|
+
} catch (_) {
|
286
|
+
/* noop */
|
287
|
+
}
|
288
|
+
}
|
289
|
+
|
290
|
+
runHandler(baseContext.browserName, imagesContext.images, error);
|
291
|
+
})().catch((error: unknown) => {
|
292
|
+
workerLogger.error('Unexpected error:', error);
|
293
|
+
emitWorkerMessage({
|
294
|
+
type: 'error',
|
295
|
+
payload: { subtype: 'test', error: serializeError(error) },
|
296
|
+
});
|
297
|
+
});
|
298
|
+
});
|
299
|
+
|
300
|
+
workerLogger.info('Browser is ready');
|
301
|
+
|
302
|
+
emitWorkerMessage({ type: 'ready' });
|
303
|
+
}
|