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
@@ -1,16 +1,17 @@
|
|
1
1
|
import path from 'path';
|
2
2
|
import assert from 'assert';
|
3
|
-
import cluster from 'cluster';
|
4
3
|
import { lstatSync, existsSync } from 'fs';
|
5
4
|
import { mkdir, writeFile, copyFile } from 'fs/promises';
|
6
5
|
import sh from 'shelljs';
|
7
|
-
import {
|
8
|
-
import { Config, BrowserConfig } from '../../types.js';
|
6
|
+
import { Config, BrowserConfigObject } from '../../types.js';
|
9
7
|
import { downloadBinary, getCreeveyCache } from '../utils.js';
|
10
8
|
import { pullImages, runImage } from '../docker.js';
|
11
9
|
import { subscribeOn } from '../messages.js';
|
12
10
|
|
13
|
-
async function createSelenoidConfig(
|
11
|
+
async function createSelenoidConfig(
|
12
|
+
browsers: BrowserConfigObject[],
|
13
|
+
{ useDocker }: { useDocker: boolean },
|
14
|
+
): Promise<string> {
|
14
15
|
const selenoidConfig: Partial<
|
15
16
|
Record<
|
16
17
|
string,
|
@@ -20,7 +21,7 @@ async function createSelenoidConfig(browsers: BrowserConfig[], { useDocker }: {
|
|
20
21
|
}
|
21
22
|
>
|
22
23
|
> = {};
|
23
|
-
const cacheDir = getCreeveyCache();
|
24
|
+
const cacheDir = await getCreeveyCache();
|
24
25
|
|
25
26
|
assert(cacheDir, "Couldn't get cache directory");
|
26
27
|
|
@@ -29,9 +30,7 @@ async function createSelenoidConfig(browsers: BrowserConfig[], { useDocker }: {
|
|
29
30
|
browsers.forEach(
|
30
31
|
({
|
31
32
|
browserName,
|
32
|
-
|
33
|
-
version = 'latest',
|
34
|
-
browserVersion = version,
|
33
|
+
browserVersion = 'latest',
|
35
34
|
dockerImage = `selenoid/${browserName}:${browserVersion}`,
|
36
35
|
webdriverCommand = [],
|
37
36
|
}) => {
|
@@ -58,6 +57,8 @@ async function downloadSelenoidBinary(destination: string): Promise<void> {
|
|
58
57
|
linux: 'selenoid_linux_amd64',
|
59
58
|
win32: 'selenoid_windows_amd64.exe',
|
60
59
|
};
|
60
|
+
// TODO Replace with `import from`
|
61
|
+
const { Octokit } = await import('@octokit/core');
|
61
62
|
const octokit = new Octokit();
|
62
63
|
const response = await octokit.request('GET /repos/{owner}/{repo}/releases/latest', {
|
63
64
|
owner: 'aerokube',
|
@@ -79,11 +80,7 @@ async function downloadSelenoidBinary(destination: string): Promise<void> {
|
|
79
80
|
}
|
80
81
|
|
81
82
|
export async function startSelenoidStandalone(config: Config, debug: boolean): Promise<void> {
|
82
|
-
config.
|
83
|
-
|
84
|
-
if (cluster.isWorker) return;
|
85
|
-
|
86
|
-
const browsers = (Object.values(config.browsers) as BrowserConfig[]).filter((browser) => !browser.gridUrl);
|
83
|
+
const browsers = (Object.values(config.browsers) as BrowserConfigObject[]).filter((browser) => !browser.gridUrl);
|
87
84
|
const selenoidConfigDir = await createSelenoidConfig(browsers, { useDocker: false });
|
88
85
|
const binaryPath = path.join(selenoidConfigDir, process.platform == 'win32' ? 'selenoid.exe' : 'selenoid');
|
89
86
|
if (config.selenoidPath) {
|
@@ -113,16 +110,14 @@ export async function startSelenoidStandalone(config: Config, debug: boolean): P
|
|
113
110
|
}
|
114
111
|
|
115
112
|
export async function startSelenoidContainer(config: Config, debug: boolean): Promise<string> {
|
116
|
-
const browsers = (Object.values(config.browsers) as
|
113
|
+
const browsers = (Object.values(config.browsers) as BrowserConfigObject[]).filter((browser) => !browser.gridUrl);
|
117
114
|
const images: string[] = [];
|
118
115
|
let limit = 0;
|
119
116
|
|
120
117
|
browsers.forEach(
|
121
118
|
({
|
122
119
|
browserName,
|
123
|
-
|
124
|
-
version = 'latest',
|
125
|
-
browserVersion = version,
|
120
|
+
browserVersion = 'latest',
|
126
121
|
limit: browserLimit = 1,
|
127
122
|
dockerImage = `selenoid/${browserName}:${browserVersion}`,
|
128
123
|
}) => {
|
@@ -0,0 +1,136 @@
|
|
1
|
+
import { Args } from '@storybook/csf';
|
2
|
+
import { Config, StorybookGlobals, StoryInput, StoriesRaw, Options } from '../../types.js';
|
3
|
+
import { subscribeOn } from '../messages.js';
|
4
|
+
import { CreeveyWebdriverBase } from '../webdriver.js';
|
5
|
+
import type { InternalBrowser } from './internal.js';
|
6
|
+
import { logger } from '../logger.js';
|
7
|
+
|
8
|
+
declare global {
|
9
|
+
interface Window {
|
10
|
+
__CREEVEY_RESTORE_SCROLL__?: () => void;
|
11
|
+
__CREEVEY_UPDATE_GLOBALS__: (globals: StorybookGlobals) => void;
|
12
|
+
__CREEVEY_INSERT_IGNORE_STYLES__: (ignoreElements: string[]) => HTMLStyleElement;
|
13
|
+
__CREEVEY_REMOVE_IGNORE_STYLES__: (ignoreStyles: HTMLStyleElement) => void;
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
// TODO Update context interface through references
|
18
|
+
export class SeleniumWebdriver extends CreeveyWebdriverBase {
|
19
|
+
#browser: InternalBrowser | null = null;
|
20
|
+
#browserName: string;
|
21
|
+
#gridUrl: string;
|
22
|
+
#config: Config;
|
23
|
+
#options: Options;
|
24
|
+
constructor(browser: string, gridUrl: string, config: Config, options: Options) {
|
25
|
+
super();
|
26
|
+
|
27
|
+
this.#browserName = browser;
|
28
|
+
this.#gridUrl = gridUrl;
|
29
|
+
this.#config = config;
|
30
|
+
this.#options = options;
|
31
|
+
|
32
|
+
subscribeOn('shutdown', () => {
|
33
|
+
void this.#browser?.closeBrowser().finally(() => process.exit());
|
34
|
+
this.#browser = null;
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
38
|
+
get browser() {
|
39
|
+
return this.#browser?.browser;
|
40
|
+
}
|
41
|
+
|
42
|
+
getSessionId(): Promise<string> {
|
43
|
+
if (!this.#browser) {
|
44
|
+
// TODO Describe the error
|
45
|
+
throw new Error('Browser is not initialized');
|
46
|
+
}
|
47
|
+
|
48
|
+
return this.#browser.browser.getSession().then((session) => session.getId());
|
49
|
+
}
|
50
|
+
|
51
|
+
async openBrowser(fresh = false): Promise<SeleniumWebdriver | null> {
|
52
|
+
if (this.#browser) {
|
53
|
+
if (fresh) {
|
54
|
+
await this.#browser.closeBrowser();
|
55
|
+
this.#browser = null;
|
56
|
+
} else {
|
57
|
+
return this;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
const internalModule = await (async () => {
|
62
|
+
try {
|
63
|
+
return await import('./internal.js');
|
64
|
+
} catch (error) {
|
65
|
+
logger.error(error);
|
66
|
+
return null;
|
67
|
+
}
|
68
|
+
})();
|
69
|
+
|
70
|
+
if (!internalModule) return null;
|
71
|
+
|
72
|
+
const { InternalBrowser } = internalModule;
|
73
|
+
const browser = await InternalBrowser.getBrowser(this.#browserName, this.#gridUrl, this.#config, this.#options);
|
74
|
+
|
75
|
+
if (!browser) return null;
|
76
|
+
|
77
|
+
this.#browser = browser;
|
78
|
+
|
79
|
+
return this;
|
80
|
+
}
|
81
|
+
|
82
|
+
async closeBrowser(): Promise<void> {
|
83
|
+
if (this.#browser) {
|
84
|
+
await this.#browser.closeBrowser();
|
85
|
+
this.#browser = null;
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
async loadStoriesFromBrowser(): Promise<StoriesRaw> {
|
90
|
+
if (!this.#browser) {
|
91
|
+
// TODO Describe the error
|
92
|
+
throw new Error('Browser is not initialized');
|
93
|
+
}
|
94
|
+
|
95
|
+
return this.#browser.loadStoriesFromBrowser();
|
96
|
+
}
|
97
|
+
|
98
|
+
protected async takeScreenshot(
|
99
|
+
captureElement: string | null,
|
100
|
+
ignoreElements?: string | string[] | null,
|
101
|
+
): Promise<Buffer> {
|
102
|
+
if (!this.#browser) {
|
103
|
+
// TODO Describe the error
|
104
|
+
throw new Error('Browser is not initialized');
|
105
|
+
}
|
106
|
+
|
107
|
+
return this.#browser.takeScreenshot(captureElement, ignoreElements);
|
108
|
+
}
|
109
|
+
|
110
|
+
protected waitForComplete(callback: (isCompleted: boolean) => void): void {
|
111
|
+
if (!this.#browser) {
|
112
|
+
// TODO Describe the error
|
113
|
+
throw new Error('Browser is not initialized');
|
114
|
+
}
|
115
|
+
|
116
|
+
this.#browser.waitForComplete(callback);
|
117
|
+
}
|
118
|
+
|
119
|
+
protected async selectStory(id: string, waitForReady?: boolean): Promise<boolean> {
|
120
|
+
if (!this.#browser) {
|
121
|
+
// TODO Describe the error
|
122
|
+
throw new Error('Browser is not initialized');
|
123
|
+
}
|
124
|
+
|
125
|
+
return this.#browser.selectStory(id, waitForReady);
|
126
|
+
}
|
127
|
+
|
128
|
+
protected async updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void> {
|
129
|
+
if (!this.#browser) {
|
130
|
+
// TODO Describe the error
|
131
|
+
throw new Error('Browser is not initialized');
|
132
|
+
}
|
133
|
+
|
134
|
+
return this.#browser.updateStoryArgs(story, updatedArgs);
|
135
|
+
}
|
136
|
+
}
|
package/src/server/stories.ts
CHANGED
@@ -2,7 +2,6 @@ import path from 'path';
|
|
2
2
|
import { mkdirSync, writeFileSync } from 'fs';
|
3
3
|
import { createHash } from 'crypto';
|
4
4
|
import _ from 'lodash';
|
5
|
-
import type { Context } from 'mocha';
|
6
5
|
import type {
|
7
6
|
TestData,
|
8
7
|
CreeveyStoryParams,
|
@@ -11,26 +10,27 @@ import type {
|
|
11
10
|
ServerTest,
|
12
11
|
StoryInput,
|
13
12
|
CreeveyTestFunction,
|
13
|
+
CreeveyTestContext,
|
14
14
|
} from '../types.js';
|
15
15
|
import { isDefined, isFunction } from '../types.js';
|
16
16
|
import { shouldSkip } from './utils.js';
|
17
17
|
|
18
18
|
function storyTestFabric(delay?: number, testFn?: CreeveyTestFunction) {
|
19
|
-
return async function storyTest(
|
19
|
+
return async function storyTest(context: CreeveyTestContext) {
|
20
20
|
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
21
21
|
await (testFn
|
22
|
-
? testFn
|
23
|
-
:
|
24
|
-
?
|
25
|
-
|
22
|
+
? testFn(context)
|
23
|
+
: context.screenshots.length > 0
|
24
|
+
? context.matchImages(
|
25
|
+
context.screenshots.reduce(
|
26
26
|
(screenshots, { imageName, screenshot }, index) => ({
|
27
27
|
...screenshots,
|
28
28
|
[imageName ?? `screenshot_${index}`]: screenshot,
|
29
29
|
}),
|
30
30
|
{},
|
31
31
|
),
|
32
|
-
)
|
33
|
-
:
|
32
|
+
)
|
33
|
+
: context.matchImage(await context.takeScreenshot()));
|
34
34
|
};
|
35
35
|
}
|
36
36
|
|
@@ -41,10 +41,17 @@ function createCreeveyTest(
|
|
41
41
|
testName?: string,
|
42
42
|
): TestData {
|
43
43
|
const { title, name, id: storyId } = storyMeta;
|
44
|
-
const
|
44
|
+
const testPath = [title, name, testName, browser].filter(isDefined);
|
45
45
|
const skip = skipOptions ? shouldSkip(browser, { title, name }, skipOptions, testName) : false;
|
46
|
-
const id = createHash('sha1').update(
|
47
|
-
return {
|
46
|
+
const id = createHash('sha1').update(testPath.join('/')).digest('hex');
|
47
|
+
return {
|
48
|
+
id,
|
49
|
+
skip,
|
50
|
+
browser,
|
51
|
+
testName,
|
52
|
+
storyPath: [...title.split('/').map((x) => x.trim()), name],
|
53
|
+
storyId,
|
54
|
+
};
|
48
55
|
}
|
49
56
|
|
50
57
|
function convertStories(browserName: string, stories: StoriesRaw | StoryInput[]): Partial<Record<string, ServerTest>> {
|
package/src/server/telemetry.ts
CHANGED
@@ -6,7 +6,7 @@ import { set } from 'lodash';
|
|
6
6
|
import { v4 } from 'uuid';
|
7
7
|
import { pathToFileURL } from 'url';
|
8
8
|
import { createRequire } from 'module';
|
9
|
-
import { Config, CreeveyStatus, isDefined, Options } from '../types';
|
9
|
+
import { Config, CreeveyStatus, isDefined, Options } from '../types.js';
|
10
10
|
|
11
11
|
const konturGitHost = 'git.skbkontur.ru';
|
12
12
|
|
@@ -151,7 +151,7 @@ export async function sendScreenshotsCount(
|
|
151
151
|
name,
|
152
152
|
typeof browser === 'object'
|
153
153
|
? {
|
154
|
-
name:
|
154
|
+
name: name,
|
155
155
|
gridUrl: browser.gridUrl ? sanitizeGridUrl(browser.gridUrl) : undefined,
|
156
156
|
browserName: browser.browserName,
|
157
157
|
browserVersion: browser.browserVersion,
|
package/src/server/utils.ts
CHANGED
@@ -4,7 +4,6 @@ import cluster from 'cluster';
|
|
4
4
|
import { dirname } from 'path';
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'url';
|
6
6
|
import { createRequire } from 'module';
|
7
|
-
import findCacheDir from 'find-cache-dir';
|
8
7
|
import { register as esmRegister } from 'tsx/esm/api';
|
9
8
|
import { register as cjsRegister } from 'tsx/cjs/api';
|
10
9
|
import { SkipOptions, SkipOption, isDefined, TestData, noop, ServerTest } from '../types.js';
|
@@ -14,8 +13,6 @@ const importMetaUrl = pathToFileURL(__filename).href;
|
|
14
13
|
|
15
14
|
export const isShuttingDown = { current: false };
|
16
15
|
|
17
|
-
export const LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
|
18
|
-
|
19
16
|
export const configExt = ['.js', '.mjs', '.ts', '.cjs', '.mts', '.cts'];
|
20
17
|
|
21
18
|
export const skipOptionKeys = ['in', 'kinds', 'stories', 'tests', 'reason'];
|
@@ -99,18 +96,20 @@ export async function shutdownWorkers(): Promise<void> {
|
|
99
96
|
emitShutdownMessage();
|
100
97
|
}
|
101
98
|
|
102
|
-
export function
|
103
|
-
|
104
|
-
}
|
105
|
-
|
106
|
-
export function getCreeveyCache(): string | undefined {
|
99
|
+
export async function getCreeveyCache(): Promise<string | undefined> {
|
100
|
+
const { default: findCacheDir } = await import('find-cache-dir');
|
107
101
|
return findCacheDir({ name: 'creevey', cwd: dirname(fileURLToPath(importMetaUrl)) });
|
108
102
|
}
|
109
103
|
|
110
|
-
export async function runSequence(seq: (() => unknown)[], predicate: () => boolean): Promise<
|
104
|
+
export async function runSequence(seq: (() => unknown)[], predicate: () => boolean): Promise<boolean> {
|
111
105
|
for (const fn of seq) {
|
112
106
|
if (predicate()) await fn();
|
113
107
|
}
|
108
|
+
return predicate();
|
109
|
+
}
|
110
|
+
|
111
|
+
export function getTestPath(test: ServerTest): string[] {
|
112
|
+
return [...test.storyPath, test.testName, test.browser].filter(isDefined);
|
114
113
|
}
|
115
114
|
|
116
115
|
export function testsToImages(tests: (TestData | undefined)[]): Set<string> {
|
@@ -189,6 +188,7 @@ const [nodeVersion] = process.versions.node.split('.').map(Number);
|
|
189
188
|
export async function loadThroughTSX<T>(
|
190
189
|
callback: (load: (modulePath: string) => Promise<T>) => Promise<T>,
|
191
190
|
): Promise<T> {
|
191
|
+
// TODO Check if it work in node18 and type: 'module'
|
192
192
|
const unregister = nodeVersion > 18 ? esmRegister() : cjsRegister();
|
193
193
|
|
194
194
|
const result = await callback((modulePath) =>
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import Logger from 'loglevel';
|
2
|
+
import chalk from 'chalk';
|
3
|
+
import { networkInterfaces } from 'os';
|
4
|
+
import { logger as defaultLogger } from './logger.js';
|
5
|
+
import { Args } from '@storybook/csf';
|
6
|
+
import {
|
7
|
+
isDefined,
|
8
|
+
StoryInput,
|
9
|
+
BaseCreeveyTestContext,
|
10
|
+
CreeveyTestContext,
|
11
|
+
CreeveyStoryParams,
|
12
|
+
StoriesRaw,
|
13
|
+
CreeveyWebdriver,
|
14
|
+
} from '../types.js';
|
15
|
+
import { emitStoriesMessage, subscribeOn } from './messages.js';
|
16
|
+
|
17
|
+
export const storybookRootID = 'storybook-root';
|
18
|
+
export const LOCALHOST_REGEXP = /(localhost|127\.0\.0\.1)/i;
|
19
|
+
const DOCKER_INTERNAL = 'host.docker.internal';
|
20
|
+
|
21
|
+
export async function resolveStorybookUrl(
|
22
|
+
storybookUrl: string,
|
23
|
+
checkUrl: (url: string) => Promise<boolean>,
|
24
|
+
logger: Logger.Logger = defaultLogger,
|
25
|
+
): Promise<string> {
|
26
|
+
logger.debug('Resolving storybook url');
|
27
|
+
const addresses = getAddresses();
|
28
|
+
for (const ip of addresses) {
|
29
|
+
const resolvedUrl = storybookUrl.replace(LOCALHOST_REGEXP, ip);
|
30
|
+
logger.debug(`Checking storybook availability on ${chalk.magenta(resolvedUrl)}`);
|
31
|
+
if (await checkUrl(resolvedUrl)) {
|
32
|
+
logger.debug(`Resolved storybook url ${chalk.magenta(resolvedUrl)}`);
|
33
|
+
return resolvedUrl;
|
34
|
+
}
|
35
|
+
}
|
36
|
+
const error = new Error('Please specify `storybookUrl` with IP address that accessible from remote browser');
|
37
|
+
error.name = 'ResolveUrlError';
|
38
|
+
throw error;
|
39
|
+
}
|
40
|
+
|
41
|
+
export function appendIframePath(url: string): string {
|
42
|
+
return `${url.replace(/\/$/, '')}/iframe.html`;
|
43
|
+
}
|
44
|
+
|
45
|
+
export function getAddresses(): string[] {
|
46
|
+
// TODO Check if docker is used
|
47
|
+
return [DOCKER_INTERNAL].concat(
|
48
|
+
...Object.values(networkInterfaces())
|
49
|
+
.filter(isDefined)
|
50
|
+
.map((network) => network.filter((info) => info.family == 'IPv4').map((info) => info.address)),
|
51
|
+
);
|
52
|
+
}
|
53
|
+
|
54
|
+
export abstract class CreeveyWebdriverBase implements CreeveyWebdriver {
|
55
|
+
protected abstract takeScreenshot(
|
56
|
+
captureElement: string | null,
|
57
|
+
ignoreElements?: string | string[] | null,
|
58
|
+
): Promise<Buffer>;
|
59
|
+
|
60
|
+
protected abstract waitForComplete(callback: (isCompleted: boolean) => void): void;
|
61
|
+
|
62
|
+
protected abstract selectStory(id: string, waitForReady?: boolean): Promise<boolean>;
|
63
|
+
|
64
|
+
protected abstract updateStoryArgs(story: StoryInput, updatedArgs: Args): Promise<void>;
|
65
|
+
|
66
|
+
abstract getSessionId(): Promise<string>;
|
67
|
+
|
68
|
+
abstract openBrowser(fresh?: boolean): Promise<CreeveyWebdriver | null>;
|
69
|
+
|
70
|
+
abstract closeBrowser(): Promise<void>;
|
71
|
+
|
72
|
+
abstract loadStoriesFromBrowser(): Promise<StoriesRaw>;
|
73
|
+
|
74
|
+
async switchStory(
|
75
|
+
story: StoryInput,
|
76
|
+
context: BaseCreeveyTestContext,
|
77
|
+
logger: Logger.Logger,
|
78
|
+
): Promise<CreeveyTestContext> {
|
79
|
+
const { id, title, name, parameters } = story;
|
80
|
+
const {
|
81
|
+
captureElement = `#${storybookRootID}`,
|
82
|
+
waitForReady,
|
83
|
+
ignoreElements,
|
84
|
+
} = (parameters.creevey ?? {}) as CreeveyStoryParams;
|
85
|
+
|
86
|
+
logger.debug(`Switching to story ${chalk.cyan(title)}/${chalk.cyan(name)} by id ${chalk.magenta(id)}`);
|
87
|
+
|
88
|
+
let storyPlayResolver: (isCompleted: boolean) => void;
|
89
|
+
let waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
90
|
+
const unsubscribe = subscribeOn('stories', (message) => {
|
91
|
+
if (message.type != 'capture') return;
|
92
|
+
const { payload = {}, payload: { imageName } = {} } = message;
|
93
|
+
void this.takeScreenshot(payload.captureElement ?? captureElement, payload.ignoreElements ?? ignoreElements).then(
|
94
|
+
(screenshot) => {
|
95
|
+
context.screenshots.push({ imageName, screenshot });
|
96
|
+
|
97
|
+
this.waitForComplete(storyPlayResolver);
|
98
|
+
|
99
|
+
emitStoriesMessage({ type: 'capture' });
|
100
|
+
},
|
101
|
+
);
|
102
|
+
});
|
103
|
+
|
104
|
+
const isCaptureCalled = await this.selectStory(id, waitForReady);
|
105
|
+
|
106
|
+
if (isCaptureCalled) {
|
107
|
+
logger.debug(`Capturing screenshots from ${chalk.magenta(id)} story's \`play()\` function`);
|
108
|
+
while (!(await waitForComplete)) {
|
109
|
+
waitForComplete = new Promise<boolean>((resolve) => (storyPlayResolver = resolve));
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
unsubscribe();
|
114
|
+
|
115
|
+
if (isCaptureCalled) logger.debug(`Story ${chalk.magenta(id)} completed capturing`);
|
116
|
+
else logger.debug(`Story ${chalk.magenta(id)} ready for capturing`);
|
117
|
+
|
118
|
+
return Object.assign(
|
119
|
+
{
|
120
|
+
takeScreenshot: () => this.takeScreenshot(captureElement, ignoreElements),
|
121
|
+
updateStoryArgs: (updatedArgs: Args) => this.updateStoryArgs(story, updatedArgs),
|
122
|
+
captureElement,
|
123
|
+
},
|
124
|
+
context,
|
125
|
+
);
|
126
|
+
}
|
127
|
+
}
|
@@ -1,147 +1,35 @@
|
|
1
|
-
import
|
2
|
-
import pixelmatch from 'pixelmatch';
|
3
|
-
|
4
|
-
import { DiffOptions, ImagesError } from '../../types.js';
|
5
|
-
|
6
|
-
function normalizeImageSize(image: PNG, width: number, height: number): Buffer {
|
7
|
-
const normalizedImage = Buffer.alloc(4 * width * height);
|
8
|
-
|
9
|
-
for (let y = 0; y < height; y++) {
|
10
|
-
for (let x = 0; x < width; x++) {
|
11
|
-
const i = (y * width + x) * 4;
|
12
|
-
if (x < image.width && y < image.height) {
|
13
|
-
const j = (y * image.width + x) * 4;
|
14
|
-
normalizedImage[i + 0] = image.data[j + 0];
|
15
|
-
normalizedImage[i + 1] = image.data[j + 1];
|
16
|
-
normalizedImage[i + 2] = image.data[j + 2];
|
17
|
-
normalizedImage[i + 3] = image.data[j + 3];
|
18
|
-
} else {
|
19
|
-
normalizedImage[i + 0] = 0;
|
20
|
-
normalizedImage[i + 1] = 0;
|
21
|
-
normalizedImage[i + 2] = 0;
|
22
|
-
normalizedImage[i + 3] = 0;
|
23
|
-
}
|
24
|
-
}
|
25
|
-
}
|
26
|
-
return normalizedImage;
|
27
|
-
}
|
28
|
-
|
29
|
-
function hasDiffPixels(diff: Buffer): boolean {
|
30
|
-
for (let i = 0; i < diff.length; i += 4) {
|
31
|
-
if (diff[i + 0] == 255 && diff[i + 1] == 0 && diff[i + 2] == 0 && diff[i + 3] == 255) return true;
|
32
|
-
}
|
33
|
-
return false;
|
34
|
-
}
|
35
|
-
|
36
|
-
function compareImages(expect: Buffer, actual: Buffer, diffOptions: DiffOptions): { isEqual: boolean; diff: Buffer } {
|
37
|
-
const expectImage = PNG.sync.read(expect);
|
38
|
-
const actualImage = PNG.sync.read(actual);
|
39
|
-
|
40
|
-
const width = Math.max(actualImage.width, expectImage.width);
|
41
|
-
const height = Math.max(actualImage.height, expectImage.height);
|
42
|
-
|
43
|
-
const diffImage = new PNG({ width, height });
|
44
|
-
|
45
|
-
let actualImageData = actualImage.data;
|
46
|
-
if (actualImage.width < width || actualImage.height < height) {
|
47
|
-
actualImageData = normalizeImageSize(actualImage, width, height);
|
48
|
-
}
|
49
|
-
|
50
|
-
let expectImageData = expectImage.data;
|
51
|
-
if (expectImage.width < width || expectImage.height < height) {
|
52
|
-
expectImageData = normalizeImageSize(expectImage, width, height);
|
53
|
-
}
|
54
|
-
|
55
|
-
pixelmatch(expectImageData, actualImageData, diffImage.data, width, height, diffOptions);
|
56
|
-
|
57
|
-
return {
|
58
|
-
isEqual: !hasDiffPixels(diffImage.data),
|
59
|
-
diff: PNG.sync.write(diffImage),
|
60
|
-
};
|
61
|
-
}
|
62
|
-
|
1
|
+
import Logger from 'loglevel';
|
63
2
|
export default function (
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
| { expected: Buffer | null; onCompare: (actual: Buffer, expect?: Buffer, diff?: Buffer) => Promise<void> }
|
68
|
-
| Buffer
|
69
|
-
| null
|
70
|
-
>,
|
71
|
-
diffOptions: DiffOptions,
|
3
|
+
matchImage: (image: Buffer, imageName?: string) => Promise<void>,
|
4
|
+
matchImages: (images: Record<string, Buffer>) => Promise<void>,
|
5
|
+
logger: Logger.Logger,
|
72
6
|
) {
|
7
|
+
let isWarningShown = false;
|
73
8
|
return function chaiImage({ Assertion }: Chai.ChaiStatic, utils: Chai.ChaiUtils): void {
|
74
|
-
async function assertImage(actual: Buffer, imageName?: string): Promise<string | undefined> {
|
75
|
-
let onCompare: (actual: Buffer, expect?: Buffer, diff?: Buffer) => Promise<void> = () => Promise.resolve();
|
76
|
-
let expected = await getExpected(imageName);
|
77
|
-
if (!(expected instanceof Buffer) && expected != null) ({ expected, onCompare } = expected);
|
78
|
-
|
79
|
-
if (expected == null) {
|
80
|
-
await onCompare(actual);
|
81
|
-
return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
|
82
|
-
}
|
83
|
-
|
84
|
-
if (actual.equals(expected)) {
|
85
|
-
await onCompare(actual);
|
86
|
-
return;
|
87
|
-
}
|
88
|
-
|
89
|
-
const { isEqual, diff } = compareImages(expected, actual, diffOptions);
|
90
|
-
|
91
|
-
if (isEqual) {
|
92
|
-
await onCompare(actual);
|
93
|
-
return;
|
94
|
-
}
|
95
|
-
|
96
|
-
await onCompare(actual, expected, diff);
|
97
|
-
|
98
|
-
return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
|
99
|
-
}
|
100
|
-
|
101
9
|
utils.addMethod(
|
102
10
|
Assertion.prototype,
|
103
11
|
'matchImage',
|
104
|
-
async function
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
if (errorMessage) {
|
111
|
-
throw createImageError(imageName ? { [imageName]: errorMessage } : errorMessage);
|
12
|
+
async function (this: Record<string, unknown>, imageName?: string) {
|
13
|
+
if (!isWarningShown) {
|
14
|
+
logger.warn(
|
15
|
+
'`expect(...).to.matchImage()` is deprecated and will be removed in the next major release. Please use `context.matchImage()` instead.',
|
16
|
+
);
|
17
|
+
isWarningShown = true;
|
112
18
|
}
|
19
|
+
const image = utils.flag(this, 'object') as Buffer;
|
20
|
+
await matchImage(image, imageName);
|
113
21
|
},
|
114
22
|
);
|
115
23
|
|
116
|
-
utils.addMethod(Assertion.prototype, 'matchImages', async function
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
try {
|
123
|
-
errorMessage = await assertImage(
|
124
|
-
typeof imageOrBase64 == 'string' ? Buffer.from(imageOrBase64, 'base64') : imageOrBase64,
|
125
|
-
imageName,
|
126
|
-
);
|
127
|
-
} catch (error) {
|
128
|
-
errorMessage = (error as Error).stack;
|
129
|
-
}
|
130
|
-
if (errorMessage) {
|
131
|
-
errors[imageName] = errorMessage;
|
132
|
-
}
|
133
|
-
},
|
134
|
-
),
|
135
|
-
);
|
136
|
-
if (Object.keys(errors).length > 0) {
|
137
|
-
throw createImageError(errors);
|
24
|
+
utils.addMethod(Assertion.prototype, 'matchImages', async function (this: Record<string, unknown>) {
|
25
|
+
if (!isWarningShown) {
|
26
|
+
logger.warn(
|
27
|
+
'`expect(...).to.matchImages()` is deprecated and will be removed in the next major release. Please use `context.matchImages()` instead.',
|
28
|
+
);
|
29
|
+
isWarningShown = true;
|
138
30
|
}
|
31
|
+
const images = utils.flag(this, 'object') as Record<string, Buffer>;
|
32
|
+
await matchImages(images);
|
139
33
|
});
|
140
34
|
};
|
141
35
|
}
|
142
|
-
|
143
|
-
function createImageError(imageErrors: string | Partial<Record<string, string>>): ImagesError {
|
144
|
-
const error = new Error('Expected image to match') as ImagesError;
|
145
|
-
error.images = imageErrors;
|
146
|
-
return error;
|
147
|
-
}
|