creevey 0.10.0-beta.4 → 0.10.0-beta.41
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-C47njyZV.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 +30 -7
- 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 +78 -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/reporters/creevey.d.ts +7 -0
- package/dist/server/reporters/creevey.js +63 -0
- package/dist/server/reporters/creevey.js.map +1 -0
- package/dist/server/reporters/index.d.ts +2 -0
- package/dist/server/reporters/index.js +16 -0
- package/dist/server/reporters/index.js.map +1 -0
- package/dist/server/reporters/junit.d.ts +16 -0
- package/dist/server/reporters/junit.js +165 -0
- package/dist/server/reporters/junit.js.map +1 -0
- package/dist/server/reporters/teamcity.d.ts +7 -0
- package/dist/server/reporters/teamcity.js +60 -0
- package/dist/server/reporters/teamcity.js.map +1 -0
- 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 +47 -73
- package/dist/server/worker/start.js.map +1 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/types.d.ts +46 -10
- 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 +30 -7
- 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 +96 -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/reporters/creevey.ts +71 -0
- package/src/server/reporters/index.ts +11 -0
- package/src/server/reporters/junit.ts +205 -0
- package/src/server/reporters/teamcity.ts +74 -0
- 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 +51 -86
- package/src/shared/index.ts +1 -1
- package/src/types.ts +50 -11
- 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
- package/dist/server/reporter.d.ts +0 -26
- package/dist/server/reporter.js +0 -108
- package/dist/server/reporter.js.map +0 -1
- package/src/server/reporter.ts +0 -138
@@ -1,8 +1,12 @@
|
|
1
|
+
import { readFile } from 'fs/promises';
|
2
|
+
import { pathToFileURL } from 'url';
|
1
3
|
import semver from 'semver';
|
2
4
|
import { exec } from 'shelljs';
|
3
5
|
|
6
|
+
const importMetaUrl = pathToFileURL(__filename).href;
|
7
|
+
|
4
8
|
// TODO Support custom docker images
|
5
|
-
export function playwrightDockerFile(browser: string, version: string): string {
|
9
|
+
export async function playwrightDockerFile(browser: string, version: string): Promise<string> {
|
6
10
|
const sv = semver.coerce(version);
|
7
11
|
|
8
12
|
let npmRegistry;
|
@@ -12,23 +16,31 @@ export function playwrightDockerFile(browser: string, version: string): string {
|
|
12
16
|
/* noop */
|
13
17
|
}
|
14
18
|
|
15
|
-
|
16
|
-
|
19
|
+
const indexJs = await readFile(new URL('./index-source.mjs', importMetaUrl), 'utf-8');
|
20
|
+
|
21
|
+
const dockerfile = `
|
22
|
+
FROM node:lts
|
17
23
|
|
18
24
|
WORKDIR /creevey
|
19
25
|
|
20
26
|
RUN echo "{ \\"type\\": \\"module\\" }" > package.json && \\
|
21
|
-
|
22
|
-
|
27
|
+
${indexJs
|
28
|
+
.split('\n')
|
29
|
+
.map((line) => ` echo "${line.replace(/"/g, '\\"')}" >> index.js && \\`)
|
30
|
+
.join('\n')}
|
31
|
+
${
|
23
32
|
npmRegistry
|
24
33
|
? `
|
25
34
|
echo "registry=${npmRegistry}" > .npmrc && \\`
|
26
35
|
: ''
|
27
36
|
}
|
28
|
-
npm i playwright-core${sv ? `@${sv.format()}` : ''}
|
37
|
+
npm i playwright-core${sv ? `@${sv.format()}` : ''} && \\
|
38
|
+
npx -y playwright${sv ? `@${sv.format()}` : ''} install --with-deps ${browser}
|
29
39
|
|
30
40
|
EXPOSE 4444
|
31
41
|
|
32
42
|
ENTRYPOINT [ "node", "./index.js" ]
|
33
43
|
`;
|
44
|
+
|
45
|
+
return dockerfile.replace(/\\\n\s*\\?\n/g, '\\\n');
|
34
46
|
}
|
@@ -1,9 +1,17 @@
|
|
1
|
+
import assert from 'assert';
|
1
2
|
import { runImage } from '../docker';
|
2
3
|
import { emitWorkerMessage, subscribeOn } from '../messages';
|
3
|
-
import { isInsideDocker } from '../utils';
|
4
|
+
import { getCreeveyCache, isInsideDocker, resolvePlaywrightBrowserType } from '../utils';
|
4
5
|
import { LOCALHOST_REGEXP } from '../webdriver';
|
6
|
+
import type { BrowserConfigObject, Config } from '../../types';
|
5
7
|
|
6
|
-
export async function startPlaywrightContainer(
|
8
|
+
export async function startPlaywrightContainer(
|
9
|
+
imageName: string,
|
10
|
+
browser: string,
|
11
|
+
config: Config,
|
12
|
+
debug: boolean,
|
13
|
+
): Promise<string> {
|
14
|
+
const { browserName, playwrightOptions } = config.browsers[browser] as BrowserConfigObject;
|
7
15
|
const port = await new Promise<number>((resolve) => {
|
8
16
|
subscribeOn('worker', (message) => {
|
9
17
|
if (message.type == 'port') {
|
@@ -13,13 +21,18 @@ export async function startPlaywrightContainer(imageName: string, debug: boolean
|
|
13
21
|
emitWorkerMessage({ type: 'port', payload: { port: -1 } });
|
14
22
|
});
|
15
23
|
|
24
|
+
const cacheDir = await getCreeveyCache();
|
25
|
+
|
26
|
+
assert(cacheDir, "Couldn't get cache directory");
|
27
|
+
|
16
28
|
const host = await runImage(
|
17
29
|
imageName,
|
18
|
-
[],
|
30
|
+
[JSON.stringify({ ...playwrightOptions, browser: resolvePlaywrightBrowserType(browserName) })],
|
19
31
|
{
|
20
32
|
ExposedPorts: { [`${port}/tcp`]: {} },
|
21
33
|
HostConfig: {
|
22
34
|
PortBindings: { ['4444/tcp']: [{ HostPort: `${port}` }] },
|
35
|
+
Binds: [`${cacheDir}/${process.pid}:/creevey/traces`],
|
23
36
|
},
|
24
37
|
},
|
25
38
|
debug,
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { chromium, firefox, webkit } from 'playwright-core';
|
2
|
+
|
3
|
+
/** @type import("playwright-core").LaunchOptions & { browser: 'chromium' | 'firefox' | 'webkit' } */
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
5
|
+
const config = JSON.parse(process.argv.slice(2)[0]);
|
6
|
+
|
7
|
+
const browsers = { chromium, firefox, webkit };
|
8
|
+
|
9
|
+
const ws = await browsers[config.browser].launchServer({
|
10
|
+
...config,
|
11
|
+
port: 4444,
|
12
|
+
wsPath: 'creevey',
|
13
|
+
tracesDir: 'traces',
|
14
|
+
});
|
15
|
+
|
16
|
+
console.log(config.browser, 'browser server launched on:', ws.wsEndpoint());
|
@@ -1,8 +1,19 @@
|
|
1
|
-
import
|
2
|
-
import
|
1
|
+
import path from 'path';
|
2
|
+
import {
|
3
|
+
Browser,
|
4
|
+
BrowserContext,
|
5
|
+
BrowserContextOptions,
|
6
|
+
BrowserType,
|
7
|
+
Page,
|
8
|
+
chromium,
|
9
|
+
firefox,
|
10
|
+
webkit,
|
11
|
+
} from 'playwright-core';
|
3
12
|
import chalk from 'chalk';
|
4
13
|
import { v4 } from 'uuid';
|
14
|
+
import Logger from 'loglevel';
|
5
15
|
import prefix from 'loglevel-plugin-prefix';
|
16
|
+
import type { Args } from '@storybook/types';
|
6
17
|
import {
|
7
18
|
BrowserConfigObject,
|
8
19
|
Config,
|
@@ -11,13 +22,24 @@ import {
|
|
11
22
|
StoryInput,
|
12
23
|
StorybookEvents,
|
13
24
|
StorybookGlobals,
|
14
|
-
noop,
|
15
25
|
} from '../../types';
|
16
|
-
import {
|
17
|
-
|
18
|
-
|
26
|
+
import {
|
27
|
+
appendIframePath,
|
28
|
+
getAddresses,
|
29
|
+
LOCALHOST_REGEXP,
|
30
|
+
openBrowser,
|
31
|
+
resolveStorybookUrl,
|
32
|
+
storybookRootID,
|
33
|
+
} from '../webdriver';
|
34
|
+
import { getCreeveyCache, isShuttingDown, resolvePlaywrightBrowserType, runSequence } from '../utils';
|
19
35
|
import { colors, logger } from '../logger';
|
20
|
-
import
|
36
|
+
import assert from 'assert';
|
37
|
+
|
38
|
+
const browsers = {
|
39
|
+
chromium,
|
40
|
+
firefox,
|
41
|
+
webkit,
|
42
|
+
};
|
21
43
|
|
22
44
|
async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser | null> {
|
23
45
|
let timeout: NodeJS.Timeout | null = null;
|
@@ -28,7 +50,7 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
|
|
28
50
|
(resolve) =>
|
29
51
|
(timeout = setTimeout(() => {
|
30
52
|
isTimeout = true;
|
31
|
-
logger.error(`Can't connect to ${type.name()} playwright browser`, error);
|
53
|
+
logger().error(`Can't connect to ${type.name()} playwright browser`, error);
|
32
54
|
resolve(null);
|
33
55
|
}, 10000)),
|
34
56
|
),
|
@@ -50,25 +72,60 @@ async function tryConnect(type: BrowserType, gridUrl: string): Promise<Browser |
|
|
50
72
|
]);
|
51
73
|
}
|
52
74
|
|
75
|
+
async function tryCreateBrowserContext(
|
76
|
+
browser: Browser,
|
77
|
+
options: BrowserContextOptions,
|
78
|
+
): Promise<{ context: BrowserContext; page: Page }> {
|
79
|
+
try {
|
80
|
+
const context = await browser.newContext(options);
|
81
|
+
const page = await context.newPage();
|
82
|
+
return { context, page };
|
83
|
+
} catch (error) {
|
84
|
+
if (error instanceof Error && error.message.includes('ffmpeg')) {
|
85
|
+
logger().warn('Failed to create browser context with video recording. Video recording will be disabled.');
|
86
|
+
logger().warn(error);
|
87
|
+
const context = await browser.newContext({
|
88
|
+
...options,
|
89
|
+
recordVideo: undefined,
|
90
|
+
});
|
91
|
+
const page = await context.newPage();
|
92
|
+
return { context, page };
|
93
|
+
}
|
94
|
+
throw error;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
53
98
|
export class InternalBrowser {
|
54
99
|
#isShuttingDown = false;
|
55
100
|
#browser: Browser;
|
101
|
+
#context: BrowserContext;
|
56
102
|
#page: Page;
|
103
|
+
#traceDir: string;
|
57
104
|
#sessionId: string = v4();
|
58
105
|
#serverHost: string | null = null;
|
59
106
|
#serverPort: number;
|
60
|
-
#
|
61
|
-
#
|
62
|
-
|
107
|
+
#debug: boolean;
|
108
|
+
#storybookGlobals?: StorybookGlobals;
|
109
|
+
#closeBrowser = openBrowser();
|
110
|
+
constructor(
|
111
|
+
browser: Browser,
|
112
|
+
context: BrowserContext,
|
113
|
+
page: Page,
|
114
|
+
traceDir: string,
|
115
|
+
port: number,
|
116
|
+
debug: boolean,
|
117
|
+
storybookGlobals?: StorybookGlobals,
|
118
|
+
) {
|
63
119
|
this.#browser = browser;
|
120
|
+
this.#context = context;
|
64
121
|
this.#page = page;
|
122
|
+
this.#traceDir = traceDir;
|
65
123
|
this.#serverPort = port;
|
66
|
-
this.#
|
67
|
-
this.#
|
68
|
-
void this.closeBrowser();
|
69
|
-
});
|
124
|
+
this.#debug = debug;
|
125
|
+
this.#storybookGlobals = storybookGlobals;
|
70
126
|
}
|
71
127
|
|
128
|
+
// TODO Expose #browser and #context in tests
|
72
129
|
get browser() {
|
73
130
|
return this.#page;
|
74
131
|
}
|
@@ -81,27 +138,36 @@ export class InternalBrowser {
|
|
81
138
|
if (this.#isShuttingDown) return;
|
82
139
|
|
83
140
|
this.#isShuttingDown = true;
|
84
|
-
this.#unsubscribe();
|
85
141
|
|
86
142
|
try {
|
143
|
+
if (this.#debug) await this.#context.tracing.stop({ path: path.join(this.#traceDir, 'trace.zip') });
|
87
144
|
await this.#page.close();
|
145
|
+
if (this.#debug) await this.#page.video()?.saveAs(path.join(this.#traceDir, 'video.webm'));
|
146
|
+
await this.#context.close();
|
88
147
|
await this.#browser.close();
|
89
|
-
} catch
|
148
|
+
} catch {
|
90
149
|
/* noop */
|
150
|
+
} finally {
|
151
|
+
this.#closeBrowser();
|
91
152
|
}
|
92
153
|
}
|
93
154
|
|
94
155
|
async takeScreenshot(captureElement?: string | null, ignoreElements?: string | string[] | null): Promise<Buffer> {
|
95
|
-
// TODO Implement features from selenium `takeScreenshot`
|
96
|
-
// TODO Do we need scroll bar hack from selenium?
|
97
156
|
const ignore = Array.isArray(ignoreElements) ? ignoreElements : ignoreElements ? [ignoreElements] : [];
|
98
157
|
const mask = ignore.map((el) => this.#page.locator(el));
|
99
158
|
if (captureElement) {
|
100
159
|
const element = await this.#page.$(captureElement);
|
101
160
|
if (!element) throw new Error(`Element with selector ${captureElement} not found`);
|
102
|
-
|
161
|
+
|
162
|
+
logger().debug(`Capturing ${chalk.cyan(captureElement)} element`);
|
163
|
+
return element.screenshot({
|
164
|
+
animations: 'disabled',
|
165
|
+
mask,
|
166
|
+
style: ':root { overflow: hidden !important; }',
|
167
|
+
});
|
103
168
|
}
|
104
|
-
|
169
|
+
logger().debug('Capturing viewport screenshot');
|
170
|
+
return this.#page.screenshot({ animations: 'disabled', mask });
|
105
171
|
}
|
106
172
|
|
107
173
|
waitForComplete(callback: (isCompleted: boolean) => void): void {
|
@@ -110,10 +176,11 @@ export class InternalBrowser {
|
|
110
176
|
|
111
177
|
async selectStory(id: string, waitForReady = false): Promise<boolean> {
|
112
178
|
// NOTE: Global variables might be reset after hot reload. I think it's workaround, maybe we need better solution
|
179
|
+
await this.updateStorybookGlobals();
|
113
180
|
await this.updateBrowserGlobalVariables();
|
114
181
|
await this.resetMousePosition();
|
115
182
|
|
116
|
-
|
183
|
+
logger().debug(`Triggering 'SetCurrentStory' event with storyId ${chalk.magenta(id)}`);
|
117
184
|
|
118
185
|
const result = await this.#page.evaluate<
|
119
186
|
[error?: string | null, isCaptureCalled?: boolean] | null,
|
@@ -152,20 +219,12 @@ export class InternalBrowser {
|
|
152
219
|
);
|
153
220
|
}
|
154
221
|
|
155
|
-
async loadStoriesFromBrowser(
|
156
|
-
|
157
|
-
const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
|
222
|
+
async loadStoriesFromBrowser(): Promise<StoriesRaw> {
|
223
|
+
const stories = await this.#page.evaluate<StoriesRaw | undefined>(() => window.__CREEVEY_GET_STORIES__());
|
158
224
|
|
159
|
-
|
225
|
+
if (!stories) throw new Error("Can't get stories, it seems creevey or storybook API isn't available");
|
160
226
|
|
161
|
-
|
162
|
-
} catch (error) {
|
163
|
-
// TODO Check how other solutions with playwright get stories from storybook
|
164
|
-
if (retry) throw error;
|
165
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
166
|
-
// NOTE: Try one more time because of dynamic nature of vite and storybook
|
167
|
-
return this.loadStoriesFromBrowser(true);
|
168
|
-
}
|
227
|
+
return stories;
|
169
228
|
}
|
170
229
|
|
171
230
|
static async getBrowser(
|
@@ -182,56 +241,77 @@ export class InternalBrowser {
|
|
182
241
|
seleniumCapabilities,
|
183
242
|
playwrightOptions,
|
184
243
|
} = browserConfig;
|
244
|
+
const parsedUrl = new URL(gridUrl);
|
245
|
+
const tracesDir = path.join(
|
246
|
+
playwrightOptions?.tracesDir ?? path.join(config.reportDir, 'traces'),
|
247
|
+
process.pid.toString(),
|
248
|
+
);
|
249
|
+
const cacheDir = await getCreeveyCache();
|
185
250
|
|
186
|
-
|
251
|
+
assert(cacheDir, "Couldn't get cache directory");
|
187
252
|
|
188
|
-
|
189
|
-
switch (browserConfig.browserName) {
|
190
|
-
case 'chromium':
|
191
|
-
browser = await tryConnect(chromium, gridUrl);
|
192
|
-
break;
|
193
|
-
case 'firefox':
|
194
|
-
browser = await tryConnect(firefox, gridUrl);
|
195
|
-
break;
|
196
|
-
case 'webkit':
|
197
|
-
browser = await tryConnect(webkit, gridUrl);
|
198
|
-
break;
|
253
|
+
let browser: Browser | null = null;
|
199
254
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
255
|
+
if (parsedUrl.protocol === 'ws:') {
|
256
|
+
browser = await tryConnect(browsers[resolvePlaywrightBrowserType(browserConfig.browserName)], gridUrl);
|
257
|
+
} else if (parsedUrl.protocol === 'creevey:') {
|
258
|
+
browser = await browsers[resolvePlaywrightBrowserType(browserConfig.browserName)].launch({
|
259
|
+
...playwrightOptions,
|
260
|
+
tracesDir: path.join(cacheDir, `${process.pid}`),
|
261
|
+
});
|
205
262
|
} else {
|
206
|
-
if (browserConfig.browserName
|
207
|
-
logger.error("Playwright's Selenium Grid feature supports only chrome browser");
|
263
|
+
if (browserConfig.browserName !== 'chrome') {
|
264
|
+
logger().error("Playwright's Selenium Grid feature supports only chrome browser");
|
208
265
|
return null;
|
209
266
|
}
|
210
267
|
|
211
268
|
process.env.SELENIUM_REMOTE_URL = gridUrl;
|
212
269
|
process.env.SELENIUM_REMOTE_CAPABILITIES = JSON.stringify(seleniumCapabilities);
|
213
270
|
|
214
|
-
browser = await chromium.launch(playwrightOptions);
|
271
|
+
browser = await chromium.launch({ ...playwrightOptions, tracesDir: path.join(cacheDir, `${process.pid}`) });
|
215
272
|
}
|
216
273
|
|
217
274
|
if (!browser) {
|
218
275
|
return null;
|
219
276
|
}
|
220
277
|
|
221
|
-
const page = await browser
|
278
|
+
const { context, page } = await tryCreateBrowserContext(browser, {
|
279
|
+
recordVideo: options.debug
|
280
|
+
? {
|
281
|
+
dir: path.join(cacheDir, `${process.pid}`),
|
282
|
+
size: viewport,
|
283
|
+
}
|
284
|
+
: undefined,
|
285
|
+
screen: viewport,
|
286
|
+
viewport,
|
287
|
+
});
|
288
|
+
if (options.debug) {
|
289
|
+
await context.tracing.start(
|
290
|
+
Object.assign({ screenshots: true, snapshots: true, sources: true }, playwrightOptions?.trace),
|
291
|
+
);
|
292
|
+
}
|
222
293
|
|
223
|
-
|
294
|
+
if (logger().getLevel() <= Logger.levels.DEBUG) {
|
295
|
+
page.on('console', (msg) => {
|
296
|
+
logger().debug(`Console message: ${msg.text()}`);
|
297
|
+
});
|
298
|
+
}
|
224
299
|
|
225
|
-
const internalBrowser = new InternalBrowser(
|
300
|
+
const internalBrowser = new InternalBrowser(
|
301
|
+
browser,
|
302
|
+
context,
|
303
|
+
page,
|
304
|
+
tracesDir,
|
305
|
+
options.port,
|
306
|
+
options.debug,
|
307
|
+
_storybookGlobals,
|
308
|
+
);
|
226
309
|
|
227
310
|
try {
|
228
311
|
if (isShuttingDown.current) return null;
|
229
312
|
const done = await internalBrowser.init({
|
230
313
|
browserName,
|
231
|
-
viewport,
|
232
314
|
storybookUrl: address,
|
233
|
-
storybookGlobals: _storybookGlobals,
|
234
|
-
resolveStorybookUrl: config.resolveStorybookUrl,
|
235
315
|
});
|
236
316
|
|
237
317
|
return done ? internalBrowser : null;
|
@@ -242,90 +322,70 @@ export class InternalBrowser {
|
|
242
322
|
const error = new Error(`Can't load storybook root page: ${message}`);
|
243
323
|
if (originalError instanceof Error) error.stack = originalError.stack;
|
244
324
|
|
245
|
-
logger.error(error);
|
325
|
+
logger().error(error);
|
246
326
|
|
247
327
|
return null;
|
248
328
|
}
|
249
329
|
}
|
250
330
|
|
251
|
-
private async init({
|
252
|
-
browserName,
|
253
|
-
viewport,
|
254
|
-
storybookUrl,
|
255
|
-
storybookGlobals,
|
256
|
-
resolveStorybookUrl,
|
257
|
-
}: {
|
258
|
-
browserName: string;
|
259
|
-
viewport?: { width: number; height: number };
|
260
|
-
storybookUrl: string;
|
261
|
-
storybookGlobals?: StorybookGlobals;
|
262
|
-
resolveStorybookUrl?: () => Promise<string>;
|
263
|
-
}) {
|
331
|
+
private async init({ browserName, storybookUrl }: { browserName: string; storybookUrl: string }) {
|
264
332
|
const sessionId = this.#sessionId;
|
265
333
|
|
266
|
-
prefix.apply(
|
334
|
+
prefix.apply(logger(), {
|
267
335
|
format(level) {
|
268
336
|
const levelColor = colors[level.toUpperCase() as keyof typeof colors];
|
269
|
-
return `[${browserName}:${chalk.gray(
|
337
|
+
return `[${browserName}:${chalk.gray(process.pid)}] ${levelColor(level)} => ${chalk.gray(sessionId)}`;
|
270
338
|
},
|
271
339
|
});
|
272
340
|
|
273
|
-
this.#page.setDefaultNavigationTimeout(10000);
|
274
341
|
this.#page.setDefaultTimeout(60000);
|
275
342
|
|
276
343
|
return await runSequence(
|
277
344
|
[
|
278
|
-
() => this.openStorybookPage(storybookUrl
|
345
|
+
() => this.openStorybookPage(storybookUrl),
|
279
346
|
() => this.waitForStorybook(),
|
280
|
-
() => this.
|
347
|
+
() => this.triggerViteReload(),
|
348
|
+
() => this.updateStorybookGlobals(),
|
281
349
|
() => this.resolveCreeveyHost(),
|
282
350
|
() => this.updateBrowserGlobalVariables(),
|
283
|
-
() => this.resizeViewport(viewport),
|
284
351
|
],
|
285
352
|
() => !this.#isShuttingDown,
|
286
353
|
);
|
287
354
|
}
|
288
355
|
|
289
|
-
private async openStorybookPage(storybookUrl: string
|
356
|
+
private async openStorybookPage(storybookUrl: string): Promise<void> {
|
290
357
|
if (!LOCALHOST_REGEXP.test(storybookUrl)) {
|
291
358
|
await this.#page.goto(appendIframePath(storybookUrl));
|
292
359
|
return;
|
293
360
|
}
|
294
361
|
|
295
362
|
try {
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
const resolvedUrl = await resolver();
|
300
|
-
|
301
|
-
this.#logger.debug(`Resolver storybook url ${resolvedUrl}`);
|
302
|
-
|
303
|
-
await this.#page.goto(appendIframePath(resolvedUrl));
|
304
|
-
} else {
|
305
|
-
await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url), this.#logger);
|
306
|
-
}
|
363
|
+
const resolvedUrl = await resolveStorybookUrl(appendIframePath(storybookUrl), (url) => this.checkUrl(url));
|
364
|
+
await this.#page.goto(resolvedUrl);
|
307
365
|
} catch (error) {
|
308
|
-
|
366
|
+
logger().error('Failed to resolve storybook URL', error instanceof Error ? error.message : '');
|
309
367
|
throw error;
|
310
368
|
}
|
311
369
|
}
|
312
370
|
|
313
371
|
private async checkUrl(url: string): Promise<boolean> {
|
372
|
+
const page = await this.#browser.newPage();
|
314
373
|
try {
|
315
|
-
|
316
|
-
const response = await
|
374
|
+
logger().debug(`Opening ${chalk.magenta(url)} and checking the page source`);
|
375
|
+
const response = await page.goto(url, { waitUntil: 'commit' });
|
317
376
|
const source = await response?.text();
|
318
377
|
|
319
|
-
|
378
|
+
logger().debug(`Checking ${chalk.cyan(`#${storybookRootID}`)} existence on ${chalk.magenta(url)}`);
|
320
379
|
return source?.includes(`id="${storybookRootID}"`) ?? false;
|
321
380
|
} catch {
|
322
381
|
return false;
|
382
|
+
} finally {
|
383
|
+
await page.close();
|
323
384
|
}
|
324
385
|
}
|
325
386
|
|
326
387
|
private async waitForStorybook(): Promise<void> {
|
327
|
-
|
328
|
-
this.#logger.debug('Waiting for `setStories` event to make sure that storybook is initiated');
|
388
|
+
logger().debug('Waiting for Storybook to initiate');
|
329
389
|
|
330
390
|
const isTimeout = await Promise.race([
|
331
391
|
new Promise<boolean>((resolve) => {
|
@@ -344,24 +404,38 @@ export class InternalBrowser {
|
|
344
404
|
return false;
|
345
405
|
}, StorybookEvents.SET_GLOBALS);
|
346
406
|
} catch (e: unknown) {
|
347
|
-
|
407
|
+
logger().debug('An error has been caught during the script:', e);
|
408
|
+
if (this.#page.isClosed()) throw e;
|
348
409
|
}
|
410
|
+
if (wait) await new Promise((resolve) => setTimeout(resolve, 1000));
|
349
411
|
} while (wait);
|
350
412
|
return false;
|
351
413
|
})(),
|
352
414
|
]);
|
353
415
|
|
354
|
-
|
355
|
-
if (isTimeout) throw new Error('Failed to wait `setStories` event');
|
416
|
+
if (isTimeout) throw new Error('Failed to wait Storybook init');
|
356
417
|
}
|
357
418
|
|
358
|
-
|
359
|
-
|
419
|
+
// TODO Doesn't work for some reason, maybe because of race-condition
|
420
|
+
private async triggerViteReload(): Promise<void> {
|
421
|
+
// NOTE: On the first load, Vite might try to optimize some dependencies and reload the page
|
422
|
+
// We need to trigger reload earlier to avoid unnecessary reloads further
|
423
|
+
try {
|
424
|
+
await this.#page.evaluate(async () => {
|
425
|
+
await window.__STORYBOOK_PREVIEW__.extract();
|
426
|
+
});
|
427
|
+
} catch {
|
428
|
+
await this.waitForStorybook();
|
429
|
+
}
|
430
|
+
}
|
431
|
+
|
432
|
+
private async updateStorybookGlobals(): Promise<void> {
|
433
|
+
if (!this.#storybookGlobals) return;
|
360
434
|
|
361
|
-
|
435
|
+
logger().debug('Applying storybook globals');
|
362
436
|
await this.#page.evaluate((globals: StorybookGlobals) => {
|
363
437
|
window.__CREEVEY_UPDATE_GLOBALS__(globals);
|
364
|
-
},
|
438
|
+
}, this.#storybookGlobals);
|
365
439
|
}
|
366
440
|
|
367
441
|
private async resolveCreeveyHost(): Promise<void> {
|
@@ -390,8 +464,10 @@ export class InternalBrowser {
|
|
390
464
|
}
|
391
465
|
|
392
466
|
private async updateBrowserGlobalVariables() {
|
467
|
+
logger().debug('Updating browser global variables');
|
393
468
|
await this.#page.evaluate(
|
394
469
|
([workerId, creeveyHost, creeveyPort]) => {
|
470
|
+
window.__CREEVEY_ENV__ = true;
|
395
471
|
window.__CREEVEY_WORKER_ID__ = workerId;
|
396
472
|
window.__CREEVEY_SERVER_HOST__ = creeveyHost ?? 'localhost';
|
397
473
|
window.__CREEVEY_SERVER_PORT__ = creeveyPort;
|
@@ -400,13 +476,8 @@ export class InternalBrowser {
|
|
400
476
|
);
|
401
477
|
}
|
402
478
|
|
403
|
-
private async resizeViewport(viewport?: { width: number; height: number }): Promise<void> {
|
404
|
-
if (!viewport) return;
|
405
|
-
|
406
|
-
await this.#page.setViewportSize(viewport);
|
407
|
-
}
|
408
|
-
|
409
479
|
private async resetMousePosition(): Promise<void> {
|
480
|
+
logger().debug('Resetting mouse position to (0, 0)');
|
410
481
|
await this.#page.mouse.move(0, 0);
|
411
482
|
}
|
412
483
|
}
|
@@ -1,10 +1,11 @@
|
|
1
1
|
/// <reference types="../../../types/playwright-context" />
|
2
|
-
import { Args } from '@storybook/
|
2
|
+
import type { Args } from '@storybook/types';
|
3
3
|
import { Config, Options, ServerTest, StoriesRaw, StoryInput } from '../../types';
|
4
4
|
import { logger } from '../logger';
|
5
5
|
import { subscribeOn } from '../messages';
|
6
6
|
import { CreeveyWebdriverBase } from '../webdriver';
|
7
7
|
import type { InternalBrowser } from './internal';
|
8
|
+
import { removeWorkerContainer } from '../worker/context.js'; // Import container context
|
8
9
|
|
9
10
|
export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
10
11
|
#browser: InternalBrowser | null = null;
|
@@ -21,7 +22,9 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
21
22
|
this.#options = options;
|
22
23
|
|
23
24
|
subscribeOn('shutdown', () => {
|
24
|
-
void this.#browser?.closeBrowser().finally(() =>
|
25
|
+
void this.#browser?.closeBrowser().finally(() => {
|
26
|
+
void removeWorkerContainer().finally(() => process.exit());
|
27
|
+
});
|
25
28
|
this.#browser = null;
|
26
29
|
});
|
27
30
|
}
|
@@ -32,7 +35,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
32
35
|
|
33
36
|
getSessionId(): Promise<string> {
|
34
37
|
if (!this.#browser) {
|
35
|
-
// TODO Describe the error
|
36
38
|
throw new Error('Browser is not initialized');
|
37
39
|
}
|
38
40
|
|
@@ -53,7 +55,7 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
53
55
|
try {
|
54
56
|
return await import('./internal.js');
|
55
57
|
} catch (error) {
|
56
|
-
logger.error(error);
|
58
|
+
logger().error(error);
|
57
59
|
return null;
|
58
60
|
}
|
59
61
|
})();
|
@@ -79,7 +81,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
79
81
|
|
80
82
|
async loadStoriesFromBrowser(): Promise<StoriesRaw> {
|
81
83
|
if (!this.#browser) {
|
82
|
-
// TODO Describe the error
|
83
84
|
throw new Error('Browser is not initialized');
|
84
85
|
}
|
85
86
|
|
@@ -95,7 +96,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
95
96
|
ignoreElements?: string | string[] | null,
|
96
97
|
): Promise<Buffer> {
|
97
98
|
if (!this.#browser) {
|
98
|
-
// TODO Describe the error
|
99
99
|
throw new Error('Browser is not initialized');
|
100
100
|
}
|
101
101
|
|
@@ -104,7 +104,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
104
104
|
|
105
105
|
protected waitForComplete(callback: (isCompleted: boolean) => void): void {
|
106
106
|
if (!this.#browser) {
|
107
|
-
// TODO Describe the error
|
108
107
|
throw new Error('Browser is not initialized');
|
109
108
|
}
|
110
109
|
|
@@ -113,7 +112,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
113
112
|
|
114
113
|
protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
|
115
114
|
if (!this.#browser) {
|
116
|
-
// TODO Describe the error
|
117
115
|
throw new Error('Browser is not initialized');
|
118
116
|
}
|
119
117
|
|
@@ -122,7 +120,6 @@ export class PlaywrightWebdriver extends CreeveyWebdriverBase {
|
|
122
120
|
|
123
121
|
protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
|
124
122
|
if (!this.#browser) {
|
125
|
-
// TODO Describe the error
|
126
123
|
throw new Error('Browser is not initialized');
|
127
124
|
}
|
128
125
|
|