creevey 0.10.0-beta.4 → 0.10.0-beta.40
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/README.md +19 -41
- package/dist/client/addon/components/Addon.js +17 -7
- package/dist/client/addon/components/Addon.js.map +1 -1
- package/dist/client/addon/components/Panel.js +2 -2
- package/dist/client/addon/components/Panel.js.map +1 -1
- package/dist/client/addon/components/Tools.js +17 -7
- package/dist/client/addon/components/Tools.js.map +1 -1
- package/dist/client/addon/withCreevey.d.ts +2 -1
- package/dist/client/addon/withCreevey.js +11 -1
- 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 +17 -7
- package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
- package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
- 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 +17 -7
- 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 +29 -7
- package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
- package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
- package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.d.ts +1 -1
- package/dist/client/shared/components/ResultsPage.js +43 -13
- package/dist/client/shared/components/ResultsPage.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.js +8 -1
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/shared/helpers.d.ts +1 -3
- package/dist/client/shared/helpers.js +4 -19
- package/dist/client/shared/helpers.js.map +1 -1
- package/dist/client/web/CreeveyApp.js +42 -14
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +5 -0
- package/dist/client/web/CreeveyContext.js +20 -7
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyLoader.js +2 -2
- package/dist/client/web/CreeveyLoader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
- package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
- package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
- package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
- package/dist/client/web/KeyboardEventsContext.js +79 -64
- package/dist/client/web/KeyboardEventsContext.js.map +1 -1
- package/dist/client/web/assets/index-B0Xv0lOY.js +802 -0
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +17 -7
- package/dist/client/web/index.js.map +1 -1
- package/dist/client/web/themes.d.ts +2 -0
- package/dist/client/web/themes.js +22 -0
- package/dist/client/web/themes.js.map +1 -0
- package/dist/creevey.js +16 -9
- package/dist/creevey.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/server/config.d.ts +1 -1
- package/dist/server/config.js +27 -5
- package/dist/server/config.js.map +1 -1
- package/dist/server/connection.d.ts +3 -0
- package/dist/server/connection.js +28 -0
- package/dist/server/connection.js.map +1 -0
- package/dist/server/docker.d.ts +1 -1
- package/dist/server/docker.js +56 -32
- package/dist/server/docker.js.map +1 -1
- package/dist/server/index.js +64 -11
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +2 -1
- package/dist/server/logger.js +7 -3
- package/dist/server/logger.js.map +1 -1
- package/dist/server/master/api.js +1 -1
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/pool.d.ts +4 -3
- package/dist/server/master/pool.js +13 -66
- package/dist/server/master/pool.js.map +1 -1
- package/dist/server/master/queue.d.ts +13 -0
- package/dist/server/master/queue.js +71 -0
- package/dist/server/master/queue.js.map +1 -0
- package/dist/server/master/runner.d.ts +3 -0
- package/dist/server/master/runner.js +76 -10
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +1 -1
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.js +13 -11
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/playwright/docker-file.d.ts +1 -1
- package/dist/server/playwright/docker-file.js +15 -6
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/docker.d.ts +2 -1
- package/dist/server/playwright/docker.js +10 -2
- package/dist/server/playwright/docker.js.map +1 -1
- package/dist/server/playwright/index-source.mjs +16 -0
- package/dist/server/playwright/internal.d.ts +6 -6
- package/dist/server/playwright/internal.js +143 -91
- package/dist/server/playwright/internal.js.map +1 -1
- package/dist/server/playwright/webdriver.d.ts +1 -1
- package/dist/server/playwright/webdriver.js +5 -8
- package/dist/server/playwright/webdriver.js.map +1 -1
- package/dist/server/providers/browser.js +6 -4
- package/dist/server/providers/browser.js.map +1 -1
- package/dist/server/providers/hybrid.js +1 -1
- package/dist/server/providers/hybrid.js.map +1 -1
- package/dist/server/reporter.d.ts +4 -19
- package/dist/server/reporter.js +30 -21
- package/dist/server/reporter.js.map +1 -1
- package/dist/server/selenium/internal.d.ts +3 -4
- package/dist/server/selenium/internal.js +127 -108
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +8 -6
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/selenium/webdriver.d.ts +1 -1
- package/dist/server/selenium/webdriver.js +5 -9
- package/dist/server/selenium/webdriver.js.map +1 -1
- package/dist/server/telemetry.js +2 -2
- package/dist/server/testsFiles/parser.js +45 -5
- package/dist/server/testsFiles/parser.js.map +1 -1
- package/dist/server/utils.d.ts +19 -1
- package/dist/server/utils.js +87 -8
- package/dist/server/utils.js.map +1 -1
- package/dist/server/webdriver.d.ts +5 -4
- package/dist/server/webdriver.js +23 -10
- package/dist/server/webdriver.js.map +1 -1
- package/dist/server/worker/chai-image.d.ts +1 -2
- package/dist/server/worker/chai-image.js +4 -3
- package/dist/server/worker/chai-image.js.map +1 -1
- package/dist/server/worker/context.d.ts +3 -0
- package/dist/server/worker/context.js +15 -0
- package/dist/server/worker/context.js.map +1 -0
- package/dist/server/worker/match-image.d.ts +4 -4
- package/dist/server/worker/match-image.js +7 -4
- package/dist/server/worker/match-image.js.map +1 -1
- package/dist/server/worker/start.js +45 -73
- package/dist/server/worker/start.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/types.d.ts +40 -8
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/docs/cli.md +12 -0
- package/docs/config.md +179 -165
- package/docs/storybook.md +60 -0
- package/docs/tests.md +50 -45
- package/package.json +64 -63
- package/src/client/addon/components/Panel.tsx +2 -2
- package/src/client/addon/withCreevey.ts +10 -2
- package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
- package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
- package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
- package/src/client/shared/components/ResultsPage.tsx +31 -8
- package/src/client/shared/creeveyClientApi.ts +9 -1
- package/src/client/shared/helpers.ts +4 -24
- package/src/client/web/CreeveyApp.tsx +27 -8
- package/src/client/web/CreeveyContext.tsx +9 -0
- package/src/client/web/CreeveyLoader.tsx +1 -1
- package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
- package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
- package/src/client/web/KeyboardEventsContext.tsx +61 -73
- package/src/client/web/themes.ts +24 -0
- package/src/creevey.ts +16 -10
- package/src/server/config.ts +28 -6
- package/src/server/connection.ts +26 -0
- package/src/server/docker.ts +63 -34
- package/src/server/index.ts +72 -14
- package/src/server/logger.ts +6 -2
- package/src/server/master/api.ts +1 -1
- package/src/server/master/pool.ts +23 -59
- package/src/server/master/queue.ts +77 -0
- package/src/server/master/runner.ts +94 -10
- package/src/server/master/server.ts +1 -1
- package/src/server/master/start.ts +16 -11
- package/src/server/playwright/docker-file.ts +18 -6
- package/src/server/playwright/docker.ts +16 -3
- package/src/server/playwright/index-source.mjs +16 -0
- package/src/server/playwright/internal.ts +182 -111
- package/src/server/playwright/webdriver.ts +6 -9
- package/src/server/providers/browser.ts +6 -4
- package/src/server/providers/hybrid.ts +1 -1
- package/src/server/reporter.ts +37 -34
- package/src/server/selenium/internal.ts +131 -116
- package/src/server/selenium/selenoid.ts +8 -6
- package/src/server/selenium/webdriver.ts +6 -10
- package/src/server/telemetry.ts +2 -2
- package/src/server/testsFiles/parser.ts +52 -4
- package/src/server/utils.ts +97 -9
- package/src/server/webdriver.ts +24 -16
- package/src/server/worker/chai-image.ts +4 -4
- package/src/server/worker/context.ts +14 -0
- package/src/server/worker/match-image.ts +12 -8
- package/src/server/worker/start.ts +49 -86
- package/src/shared/index.ts +1 -1
- package/src/types.ts +44 -8
- package/types/global.d.ts +1 -0
- package/.yarnrc.yml +0 -1
- package/chromatic.config.json +0 -5
- package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
@@ -2,9 +2,9 @@ import path from 'path';
|
|
2
2
|
import assert from 'assert';
|
3
3
|
import { lstatSync, existsSync } from 'fs';
|
4
4
|
import { mkdir, writeFile, copyFile } from 'fs/promises';
|
5
|
-
import
|
5
|
+
import { exec, chmod } from 'shelljs';
|
6
6
|
import { Config, BrowserConfigObject } from '../../types.js';
|
7
|
-
import { downloadBinary, getCreeveyCache } from '../utils.js';
|
7
|
+
import { downloadBinary, getCreeveyCache, killTree } from '../utils.js';
|
8
8
|
import { pullImages, runImage } from '../docker.js';
|
9
9
|
import { subscribeOn } from '../messages.js';
|
10
10
|
|
@@ -34,7 +34,7 @@ async function createSelenoidConfig(
|
|
34
34
|
dockerImage = `selenoid/${browserName}:${browserVersion}`,
|
35
35
|
webdriverCommand = [],
|
36
36
|
}) => {
|
37
|
-
|
37
|
+
selenoidConfig[browserName] ??= { default: browserVersion, versions: {} };
|
38
38
|
if (!useDocker && webdriverCommand.length == 0)
|
39
39
|
throw new Error('Please specify "webdriverCommand" browser option with path to browser webdriver');
|
40
40
|
selenoidConfig[browserName].versions[browserVersion] = {
|
@@ -91,12 +91,12 @@ export async function startSelenoidStandalone(config: Config, debug: boolean): P
|
|
91
91
|
|
92
92
|
// TODO Download browser webdrivers
|
93
93
|
try {
|
94
|
-
if (process.platform != 'win32')
|
94
|
+
if (process.platform != 'win32') chmod('+x', binaryPath);
|
95
95
|
} catch {
|
96
96
|
/* noop */
|
97
97
|
}
|
98
98
|
|
99
|
-
const selenoidProcess =
|
99
|
+
const selenoidProcess = exec(`${binaryPath} -conf ./browsers.json -disable-docker`, {
|
100
100
|
async: true,
|
101
101
|
cwd: selenoidConfigDir,
|
102
102
|
});
|
@@ -106,7 +106,9 @@ export async function startSelenoidStandalone(config: Config, debug: boolean): P
|
|
106
106
|
selenoidProcess.stderr?.pipe(process.stderr);
|
107
107
|
}
|
108
108
|
|
109
|
-
subscribeOn('shutdown', () =>
|
109
|
+
subscribeOn('shutdown', () => {
|
110
|
+
if (selenoidProcess.pid) void killTree(selenoidProcess.pid);
|
111
|
+
});
|
110
112
|
}
|
111
113
|
|
112
114
|
export async function startSelenoidContainer(config: Config, debug: boolean): Promise<string> {
|
@@ -1,10 +1,11 @@
|
|
1
1
|
/// <reference types="../../../types/selenium-context" />
|
2
|
-
import { Args } from '@storybook/
|
2
|
+
import type { Args } from '@storybook/types';
|
3
3
|
import { Config, StorybookGlobals, StoryInput, StoriesRaw, Options, ServerTest } from '../../types.js';
|
4
4
|
import { subscribeOn } from '../messages.js';
|
5
5
|
import { CreeveyWebdriverBase } from '../webdriver.js';
|
6
6
|
import type { InternalBrowser } from './internal.js';
|
7
7
|
import { logger } from '../logger.js';
|
8
|
+
import { removeWorkerContainer } from '../worker/context.js';
|
8
9
|
|
9
10
|
declare global {
|
10
11
|
interface Window {
|
@@ -31,7 +32,9 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
31
32
|
this.#options = options;
|
32
33
|
|
33
34
|
subscribeOn('shutdown', () => {
|
34
|
-
void this.#browser?.closeBrowser().finally(() =>
|
35
|
+
void this.#browser?.closeBrowser().finally(() => {
|
36
|
+
void removeWorkerContainer().finally(() => process.exit());
|
37
|
+
});
|
35
38
|
this.#browser = null;
|
36
39
|
});
|
37
40
|
}
|
@@ -42,7 +45,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
42
45
|
|
43
46
|
getSessionId(): Promise<string> {
|
44
47
|
if (!this.#browser) {
|
45
|
-
// TODO Describe the error
|
46
48
|
throw new Error('Browser is not initialized');
|
47
49
|
}
|
48
50
|
|
@@ -63,7 +65,7 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
63
65
|
try {
|
64
66
|
return await import('./internal.js');
|
65
67
|
} catch (error) {
|
66
|
-
logger.error(error);
|
68
|
+
logger().error(error);
|
67
69
|
return null;
|
68
70
|
}
|
69
71
|
})();
|
@@ -89,7 +91,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
89
91
|
|
90
92
|
async loadStoriesFromBrowser(): Promise<StoriesRaw> {
|
91
93
|
if (!this.#browser) {
|
92
|
-
// TODO Describe the error
|
93
94
|
throw new Error('Browser is not initialized');
|
94
95
|
}
|
95
96
|
|
@@ -98,7 +99,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
98
99
|
|
99
100
|
afterTest(test: ServerTest): Promise<void> {
|
100
101
|
if (!this.#browser) {
|
101
|
-
// TODO Describe the error
|
102
102
|
throw new Error('Browser is not initialized');
|
103
103
|
}
|
104
104
|
|
@@ -110,7 +110,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
110
110
|
ignoreElements?: string | string[] | null,
|
111
111
|
): Promise<Buffer> {
|
112
112
|
if (!this.#browser) {
|
113
|
-
// TODO Describe the error
|
114
113
|
throw new Error('Browser is not initialized');
|
115
114
|
}
|
116
115
|
|
@@ -119,7 +118,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
119
118
|
|
120
119
|
protected waitForComplete(callback: (isCompleted: boolean) => void): void {
|
121
120
|
if (!this.#browser) {
|
122
|
-
// TODO Describe the error
|
123
121
|
throw new Error('Browser is not initialized');
|
124
122
|
}
|
125
123
|
|
@@ -128,7 +126,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
128
126
|
|
129
127
|
protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
|
130
128
|
if (!this.#browser) {
|
131
|
-
// TODO Describe the error
|
132
129
|
throw new Error('Browser is not initialized');
|
133
130
|
}
|
134
131
|
|
@@ -137,7 +134,6 @@ export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
|
137
134
|
|
138
135
|
protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
|
139
136
|
if (!this.#browser) {
|
140
|
-
// TODO Describe the error
|
141
137
|
throw new Error('Browser is not initialized');
|
142
138
|
}
|
143
139
|
|
package/src/server/telemetry.ts
CHANGED
@@ -181,8 +181,8 @@ export async function sendScreenshotsCount(
|
|
181
181
|
const testsMeta = { runId: uuid, tests };
|
182
182
|
|
183
183
|
const fullPathname = buildPathname('tests', testsMeta);
|
184
|
-
// NOTE: Keep request path shorter than
|
185
|
-
const chunksCount = Math.ceil(fullPathname.length /
|
184
|
+
// NOTE: Keep request path shorter than 24k symbols
|
185
|
+
const chunksCount = Math.ceil(fullPathname.length / 24_000);
|
186
186
|
let chunks: string[] = [];
|
187
187
|
if (chunksCount > 1) {
|
188
188
|
const testsString = JSON.stringify(tests);
|
@@ -1,8 +1,58 @@
|
|
1
1
|
import { pathToFileURL } from 'url';
|
2
|
-
import { toId, storyNameFromExport } from '@storybook/csf';
|
3
2
|
import { CreeveyStoryParams, CreeveyTestFunction } from '../../types.js';
|
4
3
|
import { loadThroughTSX } from '../utils.js';
|
5
4
|
|
5
|
+
// NOTE: Copy-pasted from @storybook/csf
|
6
|
+
function toStartCaseStr(str: string) {
|
7
|
+
return str
|
8
|
+
.replace(/_/g, ' ')
|
9
|
+
.replace(/-/g, ' ')
|
10
|
+
.replace(/\./g, ' ')
|
11
|
+
.replace(/([^\n])([A-Z])([a-z])/g, (_, $1, $2, $3) => `${$1} ${$2}${$3}`)
|
12
|
+
.replace(/([a-z])([A-Z])/g, (_, $1, $2) => `${$1} ${$2}`)
|
13
|
+
.replace(/([a-z])([0-9])/gi, (_, $1, $2) => `${$1} ${$2}`)
|
14
|
+
.replace(/([0-9])([a-z])/gi, (_, $1, $2) => `${$1} ${$2}`)
|
15
|
+
.replace(/(\s|^)(\w)/g, (_, $1, $2: string) => `${$1}${$2.toUpperCase()}`)
|
16
|
+
.replace(/ +/g, ' ')
|
17
|
+
.trim();
|
18
|
+
}
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Remove punctuation and illegal characters from a story ID.
|
22
|
+
*
|
23
|
+
* See https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a
|
24
|
+
*/
|
25
|
+
const sanitize = (string: string) => {
|
26
|
+
return (
|
27
|
+
string
|
28
|
+
.toLowerCase()
|
29
|
+
// eslint-disable-next-line no-useless-escape
|
30
|
+
.replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '-')
|
31
|
+
.replace(/-+/g, '-')
|
32
|
+
.replace(/^-+/, '')
|
33
|
+
.replace(/-+$/, '')
|
34
|
+
);
|
35
|
+
};
|
36
|
+
|
37
|
+
const sanitizeSafe = (string: string, part: string) => {
|
38
|
+
const sanitized = sanitize(string);
|
39
|
+
if (sanitized === '') {
|
40
|
+
throw new Error(`Invalid ${part} '${string}', must include alphanumeric characters`);
|
41
|
+
}
|
42
|
+
return sanitized;
|
43
|
+
};
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Generate a storybook ID from a component/kind and story name.
|
47
|
+
*/
|
48
|
+
const toId = (kind: string, name?: string) =>
|
49
|
+
`${sanitizeSafe(kind, 'kind')}${name ? `--${sanitizeSafe(name, 'name')}` : ''}`;
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Transform a CSF named export into a readable story name
|
53
|
+
*/
|
54
|
+
const storyNameFromExport = (key: string) => toStartCaseStr(key);
|
55
|
+
|
6
56
|
export type CreeveyParamsByStoryId = Record<string, CreeveyStoryParams>;
|
7
57
|
|
8
58
|
export default async function parse(files: string[]): Promise<CreeveyParamsByStoryId> {
|
@@ -55,8 +105,6 @@ export const story = (
|
|
55
105
|
|
56
106
|
export const test = (title: string, testFn: CreeveyTestFunction): void => {
|
57
107
|
const storyId = getStoryId(kindTitle, storyTitle);
|
58
|
-
|
59
|
-
result[storyId] = {};
|
60
|
-
}
|
108
|
+
result[storyId] ??= {};
|
61
109
|
result[storyId].tests = Object.assign({}, result[storyId].tests, { [title]: testFn });
|
62
110
|
};
|
package/src/server/utils.ts
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
import fs from 'fs';
|
2
|
-
import
|
2
|
+
import https from 'https';
|
3
|
+
import http from 'http';
|
3
4
|
import cluster from 'cluster';
|
4
5
|
import { dirname } from 'path';
|
5
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
6
|
-
import { createRequire } from 'module';
|
7
7
|
import { register as esmRegister } from 'tsx/esm/api';
|
8
8
|
import { register as cjsRegister } from 'tsx/cjs/api';
|
9
|
-
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
|
9
|
+
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest, Worker } from '../types.js';
|
10
10
|
import { emitShutdownMessage, sendShutdownMessage } from './messages.js';
|
11
|
+
import { LOCALHOST_REGEXP } from './webdriver.js';
|
12
|
+
import assert from 'assert';
|
13
|
+
import pidtree from 'pidtree';
|
11
14
|
|
12
15
|
const importMetaUrl = pathToFileURL(__filename).href;
|
13
16
|
|
@@ -15,6 +18,19 @@ export const isShuttingDown = { current: false };
|
|
15
18
|
|
16
19
|
export const configExt = ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts'];
|
17
20
|
|
21
|
+
const browserTypes = {
|
22
|
+
chromium: 'chromium',
|
23
|
+
'chromium-headless-shell': 'chromium',
|
24
|
+
chrome: 'chromium',
|
25
|
+
'chrome-beta': 'chromium',
|
26
|
+
msedge: 'chromium',
|
27
|
+
'msedge-beta': 'chromium',
|
28
|
+
'msedge-dev': 'chromium',
|
29
|
+
'bidi-chromium': 'chromium',
|
30
|
+
firefox: 'firefox',
|
31
|
+
webkit: 'webkit',
|
32
|
+
} as const;
|
33
|
+
|
18
34
|
export const skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
|
19
35
|
|
20
36
|
function matchBy(pattern: string | string[] | RegExp | undefined, value: string): boolean {
|
@@ -83,19 +99,56 @@ export async function shutdownWorkers(): Promise<void> {
|
|
83
99
|
(worker) =>
|
84
100
|
new Promise<void>((resolve) => {
|
85
101
|
const timeout = setTimeout(() => {
|
86
|
-
worker.
|
87
|
-
},
|
102
|
+
if (worker.process.pid) void killTree(worker.process.pid);
|
103
|
+
}, 10_000);
|
88
104
|
worker.on('exit', () => {
|
89
105
|
clearTimeout(timeout);
|
90
106
|
resolve();
|
91
107
|
});
|
92
108
|
sendShutdownMessage(worker);
|
109
|
+
worker.disconnect();
|
93
110
|
}),
|
94
111
|
),
|
95
112
|
);
|
96
113
|
emitShutdownMessage();
|
97
114
|
}
|
98
115
|
|
116
|
+
export function gracefullyKill(worker: Worker): void {
|
117
|
+
worker.isShuttingDown = true;
|
118
|
+
const timeout = setTimeout(() => {
|
119
|
+
if (worker.process.pid) void killTree(worker.process.pid);
|
120
|
+
}, 10000);
|
121
|
+
worker.on('exit', () => {
|
122
|
+
clearTimeout(timeout);
|
123
|
+
});
|
124
|
+
sendShutdownMessage(worker);
|
125
|
+
}
|
126
|
+
|
127
|
+
export async function killTree(rootPid: number): Promise<void> {
|
128
|
+
const pids = await pidtree(rootPid, { root: true });
|
129
|
+
|
130
|
+
pids.forEach((pid) => {
|
131
|
+
try {
|
132
|
+
process.kill(pid, 'SIGKILL');
|
133
|
+
} catch {
|
134
|
+
/* noop */
|
135
|
+
}
|
136
|
+
});
|
137
|
+
}
|
138
|
+
|
139
|
+
export function shutdownWithError(): void {
|
140
|
+
process.exit(1);
|
141
|
+
}
|
142
|
+
|
143
|
+
export function resolvePlaywrightBrowserType(browserName: string): (typeof browserTypes)[keyof typeof browserTypes] {
|
144
|
+
assert(
|
145
|
+
browserName in browserTypes,
|
146
|
+
new Error(`Failed to match browser name "${browserName}" to playwright browserType`),
|
147
|
+
);
|
148
|
+
|
149
|
+
return browserTypes[browserName as keyof typeof browserTypes];
|
150
|
+
}
|
151
|
+
|
99
152
|
export async function getCreeveyCache(): Promise<string | undefined> {
|
100
153
|
const { default: findCacheDir } = await import('find-cache-dir');
|
101
154
|
return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
|
@@ -131,11 +184,12 @@ export function testsToImages(tests: (TestData | undefined)[]): Set<string> {
|
|
131
184
|
|
132
185
|
// https://tuhrig.de/how-to-know-you-are-inside-a-docker-container/
|
133
186
|
export const isInsideDocker =
|
134
|
-
fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker')
|
187
|
+
(fs.existsSync('/proc/1/cgroup') && fs.readFileSync('/proc/1/cgroup', 'utf-8').includes('docker')) ||
|
188
|
+
process.env.DOCKER === 'true';
|
135
189
|
|
136
190
|
export const downloadBinary = (downloadUrl: string, destination: string): Promise<void> =>
|
137
191
|
new Promise((resolve, reject) =>
|
138
|
-
get(downloadUrl, (response) => {
|
192
|
+
https.get(downloadUrl, (response) => {
|
139
193
|
if (response.statusCode == 302) {
|
140
194
|
const { location } = response.headers;
|
141
195
|
if (!location) {
|
@@ -175,10 +229,10 @@ export function readDirRecursive(dirPath: string): string[] {
|
|
175
229
|
);
|
176
230
|
}
|
177
231
|
|
178
|
-
const _require = createRequire(importMetaUrl);
|
179
232
|
export function tryToLoadTestsData(filename: string): Partial<Record<string, ServerTest>> | undefined {
|
180
233
|
try {
|
181
|
-
|
234
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
235
|
+
return require(filename) as Partial<Record<string, ServerTest>>;
|
182
236
|
} catch {
|
183
237
|
/* noop */
|
184
238
|
}
|
@@ -204,3 +258,37 @@ export async function loadThroughTSX<T>(
|
|
204
258
|
|
205
259
|
return result;
|
206
260
|
}
|
261
|
+
|
262
|
+
export function waitOnUrl(waitUrl: string, timeout: number, delay: number) {
|
263
|
+
const urls = [waitUrl];
|
264
|
+
if (!LOCALHOST_REGEXP.test(waitUrl)) {
|
265
|
+
const parsedUrl = new URL(waitUrl);
|
266
|
+
parsedUrl.host = 'localhost';
|
267
|
+
urls.push(parsedUrl.toString());
|
268
|
+
}
|
269
|
+
const startTime = Date.now();
|
270
|
+
return Promise.race(
|
271
|
+
urls.map(
|
272
|
+
(url) =>
|
273
|
+
new Promise<void>((resolve, reject) => {
|
274
|
+
const interval = setInterval(() => {
|
275
|
+
http
|
276
|
+
.get(url, (response) => {
|
277
|
+
if (response.statusCode === 200) {
|
278
|
+
clearInterval(interval);
|
279
|
+
resolve();
|
280
|
+
}
|
281
|
+
})
|
282
|
+
.on('error', () => {
|
283
|
+
// Ignore HTTP errors
|
284
|
+
});
|
285
|
+
|
286
|
+
if (Date.now() - startTime > timeout) {
|
287
|
+
clearInterval(interval);
|
288
|
+
reject(new Error(`${url} didn't respond within ${timeout / 1000} seconds`));
|
289
|
+
}
|
290
|
+
}, delay);
|
291
|
+
}),
|
292
|
+
),
|
293
|
+
);
|
294
|
+
}
|
package/src/server/webdriver.ts
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
import Logger from 'loglevel';
|
2
1
|
import chalk from 'chalk';
|
3
2
|
import { networkInterfaces } from 'os';
|
4
|
-
import { logger
|
5
|
-
import { Args } from '@storybook/
|
3
|
+
import { logger } from './logger.js';
|
4
|
+
import type { Args } from '@storybook/types';
|
6
5
|
import {
|
7
6
|
isDefined,
|
8
7
|
StoryInput,
|
@@ -19,18 +18,31 @@ export const storybookRootID = 'storybook-root';
|
|
19
18
|
export const LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
|
20
19
|
const DOCKER_INTERNAL = 'host.docker.internal';
|
21
20
|
|
21
|
+
let browserClosePromise: Promise<void> | null = null;
|
22
|
+
|
23
|
+
export const openBrowser = () => {
|
24
|
+
let resolve: () => void;
|
25
|
+
browserClosePromise = new Promise((r) => (resolve = r));
|
26
|
+
return () => {
|
27
|
+
resolve();
|
28
|
+
browserClosePromise = null;
|
29
|
+
};
|
30
|
+
};
|
31
|
+
|
32
|
+
export const waitForBrowserClose = () => browserClosePromise;
|
33
|
+
|
22
34
|
export async function resolveStorybookUrl(
|
23
35
|
storybookUrl: string,
|
24
36
|
checkUrl: (url: string) => Promise<boolean>,
|
25
|
-
logger: Logger.Logger = defaultLogger,
|
26
37
|
): Promise<string> {
|
27
|
-
logger.debug('Resolving storybook url');
|
38
|
+
logger().debug('Resolving storybook url');
|
28
39
|
const addresses = getAddresses();
|
40
|
+
// TODO Use Promise.race?
|
29
41
|
for (const ip of addresses) {
|
30
42
|
const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
|
31
|
-
logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
43
|
+
logger().debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
32
44
|
if (await checkUrl(resolvedUrl)) {
|
33
|
-
logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
45
|
+
logger().debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
34
46
|
return resolvedUrl;
|
35
47
|
}
|
36
48
|
}
|
@@ -74,11 +86,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
74
86
|
|
75
87
|
abstract afterTest(test: ServerTest): Promise<void>;
|
76
88
|
|
77
|
-
async switchStory(
|
78
|
-
story: StoryInput,
|
79
|
-
context: BaseCreeveyTestContext,
|
80
|
-
logger: Logger.Logger,
|
81
|
-
): Promise<CreeveyTestContext> {
|
89
|
+
async switchStory(story: StoryInput, context: BaseCreeveyTestContext): Promise<CreeveyTestContext> {
|
82
90
|
const { id, title, name, parameters } = story;
|
83
91
|
const {
|
84
92
|
captureElement = `#${storybookRootID}`,
|
@@ -86,7 +94,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
86
94
|
ignoreElements,
|
87
95
|
} = (parameters.creevey ?? {}) as CreeveyStoryParams;
|
88
96
|
|
89
|
-
logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
97
|
+
logger().debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
90
98
|
|
91
99
|
let storyPlayResolver: (isCompleted: boolean) => void;
|
92
100
|
let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
@@ -107,7 +115,7 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
107
115
|
const isCaptureCalled = await this.selectStory(id, waitForReady);
|
108
116
|
|
109
117
|
if (isCaptureCalled) {
|
110
|
-
logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
118
|
+
logger().debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
111
119
|
while (!(await waitForComplete)) {
|
112
120
|
waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
113
121
|
}
|
@@ -115,8 +123,8 @@ export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
|
115
123
|
|
116
124
|
unsubscribe();
|
117
125
|
|
118
|
-
if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
|
119
|
-
else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
126
|
+
if (isCaptureCalled) logger().debug(`Story ${chalk.magenta(id)} completed capturing`);
|
127
|
+
else logger().debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
120
128
|
|
121
129
|
return Object.assign(
|
122
130
|
{
|
@@ -1,8 +1,8 @@
|
|
1
|
-
import
|
1
|
+
import { logger } from '../logger';
|
2
|
+
|
2
3
|
export default function (
|
3
4
|
matchImage: (image: Buffer, imageName?: string) => Promise<void>,
|
4
5
|
matchImages: (images: Record<string, Buffer>) => Promise<void>,
|
5
|
-
logger: Logger.Logger,
|
6
6
|
) {
|
7
7
|
let isWarningShown = false;
|
8
8
|
return function chaiImage({ Assertion }: Chai.ChaiStatic, utils: Chai.ChaiUtils): void {
|
@@ -11,7 +11,7 @@ export default function (
|
|
11
11
|
'matchImage',
|
12
12
|
async function (this: Record<string, unknown>, imageName?: string) {
|
13
13
|
if (!isWarningShown) {
|
14
|
-
logger.warn(
|
14
|
+
logger().warn(
|
15
15
|
'`expect(...).to.matchImage()` is deprecated and will be removed in the next major release. Please use `context.matchImage()` instead.',
|
16
16
|
);
|
17
17
|
isWarningShown = true;
|
@@ -23,7 +23,7 @@ export default function (
|
|
23
23
|
|
24
24
|
utils.addMethod(Assertion.prototype, 'matchImages', async function (this: Record<string, unknown>) {
|
25
25
|
if (!isWarningShown) {
|
26
|
-
logger.warn(
|
26
|
+
logger().warn(
|
27
27
|
'`expect(...).to.matchImages()` is deprecated and will be removed in the next major release. Please use `context.matchImages()` instead.',
|
28
28
|
);
|
29
29
|
isWarningShown = true;
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import type { Container } from 'dockerode';
|
2
|
+
|
3
|
+
let workerContainer: Container | null = null;
|
4
|
+
|
5
|
+
export function setWorkerContainer(container: Container): void {
|
6
|
+
workerContainer = container;
|
7
|
+
}
|
8
|
+
|
9
|
+
export async function removeWorkerContainer(): Promise<void> {
|
10
|
+
if (workerContainer) {
|
11
|
+
await workerContainer.remove({ force: true });
|
12
|
+
workerContainer = null;
|
13
|
+
}
|
14
|
+
}
|
@@ -21,6 +21,10 @@ interface ImagePaths {
|
|
21
21
|
reportImageDir: string;
|
22
22
|
}
|
23
23
|
|
24
|
+
function toBuffer(bufferOrBase64: Buffer | string) {
|
25
|
+
return typeof bufferOrBase64 === 'string' ? Buffer.from(bufferOrBase64, 'base64') : bufferOrBase64;
|
26
|
+
}
|
27
|
+
|
24
28
|
async function getStat(filePath: string): Promise<Stats | null> {
|
25
29
|
try {
|
26
30
|
return await stat(filePath);
|
@@ -228,17 +232,17 @@ export async function getMatchers(ctx: ImageContext, config: Config) {
|
|
228
232
|
}
|
229
233
|
|
230
234
|
return {
|
231
|
-
matchImage: async (image: Buffer, imageName?: string) => {
|
232
|
-
const errorMessage = await assertImage(image, imageName);
|
235
|
+
matchImage: async (image: Buffer | string, imageName?: string) => {
|
236
|
+
const errorMessage = await assertImage(toBuffer(image), imageName);
|
233
237
|
if (errorMessage) {
|
234
238
|
throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
|
235
239
|
}
|
236
240
|
},
|
237
|
-
matchImages: async (images: Record<string, Buffer>) => {
|
241
|
+
matchImages: async (images: Record<string, Buffer | string>) => {
|
238
242
|
const errors: Record<string, string> = {};
|
239
243
|
await Promise.all(
|
240
244
|
Object.entries(images).map(async ([imageName, image]) => {
|
241
|
-
const errorMessage = await assertImage(image, imageName);
|
245
|
+
const errorMessage = await assertImage(toBuffer(image), imageName);
|
242
246
|
if (errorMessage) {
|
243
247
|
errors[imageName] = errorMessage;
|
244
248
|
}
|
@@ -279,17 +283,17 @@ export function getOdiffMatchers(ctx: ImageContext, config: Config) {
|
|
279
283
|
}
|
280
284
|
|
281
285
|
return {
|
282
|
-
matchImage: async (image: Buffer, imageName?: string) => {
|
283
|
-
const errorMessage = await assertImage(image, imageName);
|
286
|
+
matchImage: async (image: Buffer | string, imageName?: string) => {
|
287
|
+
const errorMessage = await assertImage(toBuffer(image), imageName);
|
284
288
|
if (errorMessage) {
|
285
289
|
throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
|
286
290
|
}
|
287
291
|
},
|
288
|
-
matchImages: async (images: Record<string, Buffer>) => {
|
292
|
+
matchImages: async (images: Record<string, Buffer | string>) => {
|
289
293
|
const errors: Record<string, string> = {};
|
290
294
|
await Promise.all(
|
291
295
|
Object.entries(images).map(async ([imageName, image]) => {
|
292
|
-
const errorMessage = await assertImage(image, imageName);
|
296
|
+
const errorMessage = await assertImage(toBuffer(image), imageName);
|
293
297
|
if (errorMessage) {
|
294
298
|
errors[imageName] = errorMessage;
|
295
299
|
}
|