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
@@ -11,17 +11,17 @@ import {
|
|
11
11
|
getTestByPath,
|
12
12
|
removeTests,
|
13
13
|
getTestPath,
|
14
|
-
useTheme,
|
15
14
|
setSearchParams,
|
16
15
|
getTestPathFromSearch,
|
17
16
|
CreeveyViewFilter,
|
18
17
|
getFailedTests,
|
19
18
|
} from '../shared/helpers.js';
|
20
|
-
import { CreeveyContext } from './CreeveyContext.js';
|
19
|
+
import { CreeveyContext, FocusableItem } from './CreeveyContext.js';
|
21
20
|
import { KeyboardEvents } from './KeyboardEventsContext.js';
|
22
21
|
import { SideBar } from './CreeveyView/SideBar/index.js';
|
23
22
|
import { ResultsPage } from '../shared/components/ResultsPage.js';
|
24
23
|
import { Toggle } from './CreeveyView/SideBar/Toggle.js';
|
24
|
+
import { useTheme } from './themes.js';
|
25
25
|
|
26
26
|
export interface CreeveyAppProps {
|
27
27
|
api?: CreeveyClientApi;
|
@@ -59,8 +59,9 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
59
59
|
const failedTests = useMemo(() => getFailedTests(tests), [tests]);
|
60
60
|
|
61
61
|
const [retry, setRetry] = useState(openedTest?.results?.length ?? 0);
|
62
|
-
const result = openedTest?.results?.[retry - 1]
|
63
|
-
const [imageName, setImageName] = useState(Object.keys(result
|
62
|
+
const result = useMemo(() => openedTest?.results?.[retry - 1], [openedTest, retry]);
|
63
|
+
const [imageName, setImageName] = useState(Object.keys(result?.images ?? {})[0] ?? '');
|
64
|
+
const [sidebarFocusedItem, setSidebarFocusedItem] = useState<FocusableItem>([]);
|
64
65
|
const canApprove = useMemo(
|
65
66
|
() =>
|
66
67
|
Boolean(
|
@@ -94,6 +95,7 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
94
95
|
(test: CreeveyTest): void => {
|
95
96
|
const testPath = getTestPath(test);
|
96
97
|
setSearchParams(testPath);
|
98
|
+
setSidebarFocusedItem(testPath);
|
97
99
|
updateTests((draft) => {
|
98
100
|
openSuite(draft, testPath, true);
|
99
101
|
openTest(testPath);
|
@@ -103,11 +105,24 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
103
105
|
);
|
104
106
|
|
105
107
|
const handleGoToNextFailedTest = useCallback(() => {
|
106
|
-
if (failedTests.length
|
108
|
+
if (failedTests.length == 0) return;
|
107
109
|
const currentTest = failedTests.findIndex((t) => t.id === openedTest?.id);
|
108
|
-
const
|
109
|
-
|
110
|
-
|
110
|
+
const failedImages = Object.entries(result?.images ?? {})
|
111
|
+
.filter(([name, image]) =>
|
112
|
+
// TODO Move to helpers, it duplicates in a few places
|
113
|
+
Boolean(image?.error != null && openedTest?.approved?.[name] != retry - 1 && result?.status != 'success'),
|
114
|
+
)
|
115
|
+
.map(([name]) => name);
|
116
|
+
if (
|
117
|
+
failedImages.length > 1 &&
|
118
|
+
(failedTests.length == 1 || failedImages.indexOf(imageName) < failedImages.length - 1)
|
119
|
+
) {
|
120
|
+
setImageName((name) => failedImages[failedImages.indexOf(name) + 1] ?? failedImages[0]);
|
121
|
+
} else {
|
122
|
+
const nextFailedTest = failedTests[currentTest + 1] ?? failedTests[0];
|
123
|
+
handleOpenTest(nextFailedTest);
|
124
|
+
}
|
125
|
+
}, [failedTests, handleOpenTest, openedTest, retry, result, imageName]);
|
111
126
|
|
112
127
|
const handleImageApproveNew = useCallback((): void => {
|
113
128
|
const id = openedTest?.id;
|
@@ -122,6 +137,7 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
122
137
|
}, [handleImageApproveNew, handleGoToNextFailedTest]);
|
123
138
|
|
124
139
|
const handleApproveAll = useCallback(() => {
|
140
|
+
// TODO Update handled incorrectly
|
125
141
|
api?.approveAll();
|
126
142
|
}, [api]);
|
127
143
|
|
@@ -189,12 +205,15 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
189
205
|
value={{
|
190
206
|
isReport: initialState.isReport,
|
191
207
|
isRunning,
|
208
|
+
onImageNext: canApprove ? handleGoToNextFailedTest : undefined,
|
192
209
|
onImageApprove: canApprove ? handleImageApproveAndGoNext : undefined,
|
193
210
|
onApproveAll: handleApproveAll,
|
194
211
|
onStart: handleStart,
|
195
212
|
onStop: handleStop,
|
196
213
|
onSuiteOpen: handleSuiteOpen,
|
197
214
|
onSuiteToggle: handleSuiteToggle,
|
215
|
+
sidebarFocusedItem,
|
216
|
+
setSidebarFocusedItem,
|
198
217
|
}}
|
199
218
|
>
|
200
219
|
<ThemeProvider theme={ensure(themes[theme])}>
|
@@ -1,26 +1,35 @@
|
|
1
1
|
import React, { useContext } from 'react';
|
2
2
|
import { CreeveySuite, noop } from '../../types.js';
|
3
3
|
|
4
|
+
export type SuitePath = string[];
|
5
|
+
export type FocusableItem = null | SuitePath;
|
6
|
+
|
4
7
|
export interface CreeveyContextType {
|
5
8
|
isReport: boolean;
|
6
9
|
isRunning: boolean;
|
7
10
|
onStop: () => void;
|
11
|
+
onImageNext?: () => void;
|
8
12
|
onImageApprove?: () => void;
|
9
13
|
onApproveAll: () => void;
|
10
14
|
onStart: (rootSuite: CreeveySuite) => void;
|
11
15
|
onSuiteOpen: (path: string[], opened: boolean) => void;
|
12
16
|
onSuiteToggle: (path: string[], checked: boolean) => void;
|
17
|
+
sidebarFocusedItem: FocusableItem;
|
18
|
+
setSidebarFocusedItem: (item: FocusableItem) => void;
|
13
19
|
}
|
14
20
|
|
15
21
|
export const CreeveyContext = React.createContext<CreeveyContextType>({
|
16
22
|
isReport: true,
|
17
23
|
isRunning: false,
|
24
|
+
onImageNext: noop,
|
18
25
|
onImageApprove: noop,
|
19
26
|
onApproveAll: noop,
|
20
27
|
onStop: noop,
|
21
28
|
onStart: noop,
|
22
29
|
onSuiteOpen: noop,
|
23
30
|
onSuiteToggle: noop,
|
31
|
+
sidebarFocusedItem: [],
|
32
|
+
setSidebarFocusedItem: noop,
|
24
33
|
});
|
25
34
|
|
26
35
|
export const useCreeveyContext = () => useContext(CreeveyContext);
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import React, { JSX } from 'react';
|
2
2
|
import { styled, withTheme, Theme, keyframes, ensure, ThemeProvider, themes, Keyframes } from '@storybook/theming';
|
3
|
-
import { useTheme } from '
|
3
|
+
import { useTheme } from './themes.js';
|
4
4
|
|
5
5
|
const Container = withTheme(
|
6
6
|
styled.div(({ theme }) => ({
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import React, { JSX, ChangeEvent,
|
1
|
+
import React, { JSX, ChangeEvent, useRef, useState } from 'react';
|
2
2
|
import { SearchIcon, CloseAltIcon } from '@storybook/icons';
|
3
3
|
import { styled, Theme, withTheme } from '@storybook/theming';
|
4
|
-
import {
|
4
|
+
import { useCreeveyContext } from '../../CreeveyContext.js';
|
5
5
|
|
6
6
|
interface SearchProps {
|
7
7
|
onChange: (arg: string) => void;
|
@@ -100,7 +100,7 @@ const FilterForm = withTheme(
|
|
100
100
|
);
|
101
101
|
|
102
102
|
export const Search = ({ onChange, value }: SearchProps): JSX.Element => {
|
103
|
-
const { setSidebarFocusedItem } =
|
103
|
+
const { setSidebarFocusedItem } = useCreeveyContext();
|
104
104
|
const [focussed, onSetFocussed] = useState(false);
|
105
105
|
const searchRef = useRef<HTMLInputElement>(null);
|
106
106
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { JSX } from 'react';
|
1
|
+
import React, { JSX, useCallback, useEffect, useState } from 'react';
|
2
2
|
import { styled, withTheme } from '@storybook/theming';
|
3
3
|
import { Button } from '@storybook/components';
|
4
4
|
import { ChevronRightIcon } from '@storybook/icons';
|
@@ -21,15 +21,46 @@ const Container = styled.div({
|
|
21
21
|
});
|
22
22
|
|
23
23
|
export function SideBarFooter(): JSX.Element {
|
24
|
-
const { onApproveAll, onImageApprove } = useCreeveyContext();
|
24
|
+
const { onApproveAll, onImageApprove, onImageNext } = useCreeveyContext();
|
25
|
+
const [isAlt, setIsAlt] = useState(false);
|
26
|
+
|
27
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
28
|
+
if (e.code === 'AltLeft') {
|
29
|
+
e.preventDefault();
|
30
|
+
setIsAlt(true);
|
31
|
+
}
|
32
|
+
}, []);
|
33
|
+
const handleKeyUp = useCallback((e: KeyboardEvent) => {
|
34
|
+
if (e.code === 'AltLeft') {
|
35
|
+
e.preventDefault();
|
36
|
+
setIsAlt(false);
|
37
|
+
}
|
38
|
+
}, []);
|
39
|
+
|
40
|
+
useEffect(() => {
|
41
|
+
document.addEventListener('keydown', handleKeyDown, false);
|
42
|
+
document.addEventListener('keyup', handleKeyUp, false);
|
43
|
+
|
44
|
+
return () => {
|
45
|
+
document.removeEventListener('keydown', handleKeyDown, false);
|
46
|
+
document.removeEventListener('keyup', handleKeyUp, false);
|
47
|
+
};
|
48
|
+
}, [handleKeyDown, handleKeyUp]);
|
25
49
|
|
26
50
|
return (
|
27
51
|
<Sticky>
|
28
52
|
<Container>
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
53
|
+
{isAlt ? (
|
54
|
+
<Button variant="outline" size="medium" onClick={onImageNext} disabled={!onImageApprove}>
|
55
|
+
Next
|
56
|
+
<ChevronRightIcon />
|
57
|
+
</Button>
|
58
|
+
) : (
|
59
|
+
<Button variant="solid" size="medium" onClick={onImageApprove} disabled={!onImageApprove}>
|
60
|
+
Approve
|
61
|
+
<ChevronRightIcon />
|
62
|
+
</Button>
|
63
|
+
)}
|
33
64
|
<Button variant="outline" size="medium" onClick={onApproveAll}>
|
34
65
|
Approve all
|
35
66
|
</Button>
|
@@ -1,11 +1,10 @@
|
|
1
|
-
import React, { JSX, useRef,
|
1
|
+
import React, { JSX, useRef, useEffect, useMemo } from 'react';
|
2
2
|
import { ChevronDownIcon, ChevronRightIcon } from '@storybook/icons';
|
3
3
|
import { styled, withTheme, Theme } from '@storybook/theming';
|
4
4
|
import { Checkbox, CheckboxContainer } from './Checkbox.js';
|
5
5
|
import { TestStatusIcon } from './TestStatusIcon.js';
|
6
6
|
import { CreeveySuite, isTest } from '../../../../types.js';
|
7
|
-
import {
|
8
|
-
import { KeyboardEventsContext } from '../../KeyboardEventsContext.js';
|
7
|
+
import { useCreeveyContext } from '../../CreeveyContext.js';
|
9
8
|
|
10
9
|
export interface SuiteLinkProps {
|
11
10
|
title: string;
|
@@ -79,8 +78,7 @@ export const SuiteTitle = styled.span({
|
|
79
78
|
});
|
80
79
|
|
81
80
|
export function SuiteLink({ title, suite, 'data-testid': dataTid }: SuiteLinkProps): JSX.Element {
|
82
|
-
const { onSuiteOpen, onSuiteToggle } =
|
83
|
-
const { sidebarFocusedItem, setSidebarFocusedItem } = useContext(KeyboardEventsContext);
|
81
|
+
const { onSuiteOpen, onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem } = useCreeveyContext();
|
84
82
|
const checkboxRef = useRef<Checkbox>(null);
|
85
83
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
86
84
|
|
@@ -1,9 +1,8 @@
|
|
1
1
|
import React, { JSX, useEffect, useCallback, useContext, useMemo, useRef } from 'react';
|
2
2
|
import { CreeveyTest } from '../../../../types.js';
|
3
3
|
import { TestStatusIcon } from './TestStatusIcon.js';
|
4
|
-
import {
|
4
|
+
import { useCreeveyContext } from '../../CreeveyContext.js';
|
5
5
|
import { SideBarContext } from './SideBar.js';
|
6
|
-
import { KeyboardEventsContext } from '../../KeyboardEventsContext.js';
|
7
6
|
import { Button, Container, SuiteContainer, SuiteTitle } from './SuiteLink.js';
|
8
7
|
import { Checkbox, CheckboxContainer } from './Checkbox.js';
|
9
8
|
import { getTestPath } from '../../../shared/helpers.js';
|
@@ -20,9 +19,8 @@ const TestContainer = styled(SuiteContainer)({
|
|
20
19
|
});
|
21
20
|
|
22
21
|
export function TestLink({ title, opened, test }: TestLinkProps): JSX.Element {
|
23
|
-
const { onSuiteToggle } =
|
22
|
+
const { onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem } = useCreeveyContext();
|
24
23
|
const { onOpenTest } = useContext(SideBarContext);
|
25
|
-
const { sidebarFocusedItem, setSidebarFocusedItem } = useContext(KeyboardEventsContext);
|
26
24
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
27
25
|
|
28
26
|
const emptyResults = (test.results?.length ?? 0) == 0;
|
@@ -1,20 +1,7 @@
|
|
1
|
-
import React, { JSX,
|
2
|
-
import { CreeveySuite, isTest
|
1
|
+
import React, { JSX, useEffect, useCallback, PropsWithChildren } from 'react';
|
2
|
+
import { CreeveySuite, isTest } from '../../types.js';
|
3
3
|
import { CreeveyViewFilter, filterTests, flattenSuite, getSuiteByPath, getTestPath } from '../shared/helpers.js';
|
4
|
-
import {
|
5
|
-
|
6
|
-
export type SuitePath = string[];
|
7
|
-
export type FocusableItem = null | SuitePath;
|
8
|
-
|
9
|
-
export interface KeyboardEventsContextType {
|
10
|
-
sidebarFocusedItem: FocusableItem;
|
11
|
-
setSidebarFocusedItem: (item: FocusableItem) => void;
|
12
|
-
}
|
13
|
-
|
14
|
-
export const KeyboardEventsContext = React.createContext<KeyboardEventsContextType>({
|
15
|
-
sidebarFocusedItem: [],
|
16
|
-
setSidebarFocusedItem: noop,
|
17
|
-
});
|
4
|
+
import { useCreeveyContext } from './CreeveyContext.js';
|
18
5
|
|
19
6
|
export const KeyboardEvents = ({
|
20
7
|
children,
|
@@ -24,9 +11,7 @@ export const KeyboardEvents = ({
|
|
24
11
|
rootSuite: CreeveySuite;
|
25
12
|
filter: CreeveyViewFilter;
|
26
13
|
}>): JSX.Element => {
|
27
|
-
const
|
28
|
-
|
29
|
-
const { onSuiteOpen, onSuiteToggle } = useContext(CreeveyContext);
|
14
|
+
const { onSuiteOpen, onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem } = useCreeveyContext();
|
30
15
|
|
31
16
|
const suiteList = flattenSuite(filterTests(rootSuite, filter));
|
32
17
|
|
@@ -45,63 +30,70 @@ export const KeyboardEvents = ({
|
|
45
30
|
(e: KeyboardEvent) => {
|
46
31
|
if (sidebarFocusedItem === null) return;
|
47
32
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
33
|
+
switch (e.code) {
|
34
|
+
case 'Enter': {
|
35
|
+
if (sidebarFocusedItem.length === 0) return;
|
36
|
+
|
37
|
+
const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
|
38
|
+
if (!focusedSuite) return;
|
39
|
+
if (!isTest(focusedSuite)) {
|
40
|
+
e.preventDefault();
|
41
|
+
onSuiteOpen(focusedSuite.path, !focusedSuite.opened);
|
42
|
+
}
|
43
|
+
if (isTest(focusedSuite) && focusedSuite.results?.length == 0) {
|
44
|
+
e.preventDefault();
|
45
|
+
}
|
46
|
+
return;
|
56
47
|
}
|
57
|
-
|
48
|
+
case 'Space': {
|
58
49
|
e.preventDefault();
|
50
|
+
// TODO handle keys in one place
|
51
|
+
if (e.altKey) return;
|
52
|
+
const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
|
53
|
+
if (!focusedSuite) return;
|
54
|
+
const path = isTest(focusedSuite) ? getTestPath(focusedSuite) : focusedSuite.path;
|
55
|
+
onSuiteToggle(path, !focusedSuite.checked);
|
56
|
+
return;
|
59
57
|
}
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
const nextPath = isTest(nextSuite.suite) ? getTestPath(nextSuite.suite) : nextSuite.suite.path;
|
74
|
-
setSidebarFocusedItem(nextPath);
|
75
|
-
}
|
76
|
-
if (e.code === 'ArrowUp') {
|
77
|
-
const currentIndex = sidebarFocusedItem.length === 0 ? 0 : getFocusedItemIndex(sidebarFocusedItem);
|
78
|
-
const nextSuite = currentIndex > 0 ? suiteList[currentIndex - 1].suite : rootSuite;
|
79
|
-
const nextPath = isTest(nextSuite) ? getTestPath(nextSuite) : nextSuite.path;
|
80
|
-
setSidebarFocusedItem(nextPath);
|
81
|
-
}
|
82
|
-
|
83
|
-
if (e.code === 'ArrowRight') {
|
84
|
-
if (sidebarFocusedItem.length === 0) return;
|
85
|
-
const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
|
86
|
-
|
87
|
-
if (!focusedSuite || isTest(focusedSuite)) return;
|
88
|
-
onSuiteOpen(focusedSuite.path, true);
|
89
|
-
}
|
90
|
-
|
91
|
-
if (e.code === 'ArrowLeft') {
|
92
|
-
if (sidebarFocusedItem.length === 0) return;
|
93
|
-
const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
|
94
|
-
if (!focusedSuite) return;
|
95
|
-
if (!isTest(focusedSuite) && focusedSuite.opened) {
|
96
|
-
onSuiteOpen(focusedSuite.path, false);
|
58
|
+
case 'ArrowDown': {
|
59
|
+
const currentIndex = sidebarFocusedItem.length === 0 ? -1 : getFocusedItemIndex(sidebarFocusedItem);
|
60
|
+
if (currentIndex === suiteList.length - 1) return;
|
61
|
+
const nextSuite = suiteList[currentIndex + 1];
|
62
|
+
const nextPath = isTest(nextSuite.suite) ? getTestPath(nextSuite.suite) : nextSuite.suite.path;
|
63
|
+
setSidebarFocusedItem(nextPath);
|
64
|
+
return;
|
65
|
+
}
|
66
|
+
case 'ArrowUp': {
|
67
|
+
const currentIndex = sidebarFocusedItem.length === 0 ? 0 : getFocusedItemIndex(sidebarFocusedItem);
|
68
|
+
const nextSuite = currentIndex > 0 ? suiteList[currentIndex - 1].suite : rootSuite;
|
69
|
+
const nextPath = isTest(nextSuite) ? getTestPath(nextSuite) : nextSuite.path;
|
70
|
+
setSidebarFocusedItem(nextPath);
|
97
71
|
return;
|
98
72
|
}
|
73
|
+
case 'ArrowRight': {
|
74
|
+
if (sidebarFocusedItem.length === 0) return;
|
75
|
+
const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
|
99
76
|
|
100
|
-
|
101
|
-
|
77
|
+
if (!focusedSuite || isTest(focusedSuite)) return;
|
78
|
+
onSuiteOpen(focusedSuite.path, true);
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
case 'ArrowLeft': {
|
82
|
+
if (sidebarFocusedItem.length === 0) return;
|
83
|
+
const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
|
84
|
+
if (!focusedSuite) return;
|
85
|
+
if (!isTest(focusedSuite) && focusedSuite.opened) {
|
86
|
+
onSuiteOpen(focusedSuite.path, false);
|
87
|
+
return;
|
88
|
+
}
|
89
|
+
|
90
|
+
const path = isTest(focusedSuite) ? getTestPath(focusedSuite) : focusedSuite.path;
|
91
|
+
setSidebarFocusedItem(path.slice(0, -1));
|
92
|
+
return;
|
93
|
+
}
|
102
94
|
}
|
103
95
|
},
|
104
|
-
[onSuiteOpen, onSuiteToggle, rootSuite, suiteList, getFocusedItemIndex, sidebarFocusedItem],
|
96
|
+
[onSuiteOpen, onSuiteToggle, rootSuite, suiteList, getFocusedItemIndex, sidebarFocusedItem, setSidebarFocusedItem],
|
105
97
|
);
|
106
98
|
|
107
99
|
useEffect(() => {
|
@@ -112,9 +104,5 @@ export const KeyboardEvents = ({
|
|
112
104
|
};
|
113
105
|
}, [handleKeyDown]);
|
114
106
|
|
115
|
-
return
|
116
|
-
<KeyboardEventsContext.Provider value={{ sidebarFocusedItem, setSidebarFocusedItem }}>
|
117
|
-
{children}
|
118
|
-
</KeyboardEventsContext.Provider>
|
119
|
-
);
|
107
|
+
return <>{children}</>;
|
120
108
|
};
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { useEffect, useState } from 'react';
|
2
|
+
import { themes, ThemeVars } from '@storybook/theming';
|
3
|
+
import { isDefined } from '../../types.js';
|
4
|
+
|
5
|
+
const CREEVEY_THEME = 'Creevey_theme';
|
6
|
+
|
7
|
+
function isTheme(theme?: string | null): theme is ThemeVars['base'] {
|
8
|
+
return isDefined(theme) && Object.prototype.hasOwnProperty.call(themes, theme);
|
9
|
+
}
|
10
|
+
|
11
|
+
function initialTheme(): ThemeVars['base'] {
|
12
|
+
const theme = localStorage.getItem(CREEVEY_THEME);
|
13
|
+
return isTheme(theme) ? theme : 'light';
|
14
|
+
}
|
15
|
+
|
16
|
+
export function useTheme(): [ThemeVars['base'], (theme: ThemeVars['base']) => void] {
|
17
|
+
const [theme, setTheme] = useState<ThemeVars['base']>(initialTheme());
|
18
|
+
|
19
|
+
useEffect(() => {
|
20
|
+
localStorage.setItem(CREEVEY_THEME, theme);
|
21
|
+
}, [theme]);
|
22
|
+
|
23
|
+
return [theme, setTheme];
|
24
|
+
}
|
package/src/creevey.ts
CHANGED
@@ -5,14 +5,14 @@ import { Options } from './types.js';
|
|
5
5
|
import { emitWorkerMessage } from './server/messages.js';
|
6
6
|
import { isShuttingDown, shutdownWorkers } from './server/utils.js';
|
7
7
|
import Logger from 'loglevel';
|
8
|
-
import { logger } from './server/logger.js';
|
8
|
+
import { logger, setRootName } from './server/logger.js';
|
9
9
|
|
10
10
|
function shutdownOnException(reason: unknown): void {
|
11
11
|
if (isShuttingDown.current) return;
|
12
12
|
|
13
13
|
const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
|
14
14
|
|
15
|
-
logger.error(error);
|
15
|
+
logger().error(error);
|
16
16
|
|
17
17
|
process.exitCode = -1;
|
18
18
|
if (cluster.isWorker) emitWorkerMessage({ type: 'error', payload: { subtype: 'unknown', error } });
|
@@ -23,19 +23,25 @@ process.on('uncaughtException', shutdownOnException);
|
|
23
23
|
process.on('unhandledRejection', shutdownOnException);
|
24
24
|
// TODO SIGINT Stuck with selenium
|
25
25
|
process.on('SIGINT', () => {
|
26
|
+
if (isShuttingDown.current) {
|
27
|
+
process.exit(-1);
|
28
|
+
}
|
26
29
|
isShuttingDown.current = true;
|
27
30
|
});
|
28
31
|
|
29
32
|
const argv = minimist<Options>(process.argv.slice(2), {
|
30
|
-
string: ['browser', 'config', 'reporter', 'reportDir', 'screenDir', 'gridUrl', 'storybookUrl'],
|
31
|
-
boolean: ['debug', 'trace', 'ui', 'odiff'],
|
32
|
-
default: { port: 3000 },
|
33
|
-
alias: { port: 'p', config: 'c', debug: 'd', update: 'u' },
|
33
|
+
string: ['browser', 'config', 'reporter', 'reportDir', 'screenDir', 'gridUrl', 'storybookUrl', 'storybookPort'],
|
34
|
+
boolean: ['debug', 'trace', 'ui', 'odiff', 'noDocker'],
|
35
|
+
default: { port: '3000' },
|
36
|
+
alias: { port: 'p', config: 'c', debug: 'd', update: 'u', storybookStart: 's' },
|
34
37
|
});
|
35
38
|
|
39
|
+
if ('port' in argv && !isNaN(argv.port)) argv.port = Number(argv.port);
|
40
|
+
if ('browser' in argv && argv.browser) setRootName(argv.browser);
|
41
|
+
|
36
42
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
37
43
|
if (cluster.isPrimary && argv.reporter) {
|
38
|
-
logger.warn(`--reporter option has been removed please describe reporter in config file:
|
44
|
+
logger().warn(`--reporter option has been removed please describe reporter in config file:
|
39
45
|
import { reporters } from 'mocha';
|
40
46
|
|
41
47
|
const config = {
|
@@ -52,13 +58,13 @@ if (cluster.isPrimary && argv.reporter) {
|
|
52
58
|
// @ts-expect-error: define log level for storybook
|
53
59
|
global.LOGLEVEL = argv.trace ? 'trace' : argv.debug ? 'debug' : 'warn';
|
54
60
|
if (argv.trace) {
|
55
|
-
logger.setDefaultLevel(Logger.levels.TRACE);
|
61
|
+
logger().setDefaultLevel(Logger.levels.TRACE);
|
56
62
|
Logger.setDefaultLevel(Logger.levels.TRACE);
|
57
63
|
} else if (argv.debug) {
|
58
|
-
logger.setDefaultLevel(Logger.levels.DEBUG);
|
64
|
+
logger().setDefaultLevel(Logger.levels.DEBUG);
|
59
65
|
Logger.setDefaultLevel(Logger.levels.DEBUG);
|
60
66
|
} else {
|
61
|
-
logger.setDefaultLevel(Logger.levels.INFO);
|
67
|
+
logger().setDefaultLevel(Logger.levels.INFO);
|
62
68
|
Logger.setDefaultLevel(Logger.levels.INFO);
|
63
69
|
}
|
64
70
|
|
package/src/server/config.ts
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
import fs from 'fs';
|
2
2
|
import path from 'path';
|
3
|
+
import cluster from 'cluster';
|
3
4
|
import { pathToFileURL } from 'url';
|
4
|
-
import { loadStories as
|
5
|
+
import { loadStories as hybridStoriesProvider } from './providers/hybrid.js';
|
5
6
|
import { Config, BrowserConfig, BrowserConfigObject, Options, isDefined } from '../types.js';
|
6
7
|
import { configExt, loadThroughTSX } from './utils.js';
|
7
8
|
import { CreeveyReporter, TeamcityReporter } from './reporter.js';
|
@@ -9,25 +10,27 @@ import { logger } from './logger.js';
|
|
9
10
|
|
10
11
|
export const defaultBrowser = 'chrome';
|
11
12
|
|
12
|
-
export const defaultConfig: Omit<Config, 'gridUrl' | '
|
13
|
+
export const defaultConfig: Omit<Config, 'gridUrl' | 'tsConfig' | 'webdriver'> = {
|
13
14
|
disableTelemetry: false,
|
15
|
+
useWorkerQueue: false,
|
14
16
|
useDocker: true,
|
15
|
-
dockerImage: 'aerokube/selenoid:latest-release',
|
17
|
+
dockerImage: 'aerokube/selenoid:latest-release', // TODO What about playwright?
|
16
18
|
dockerImagePlatform: '',
|
17
19
|
pullImages: true,
|
18
20
|
failFast: false,
|
19
21
|
storybookUrl: 'http://localhost:6006',
|
20
22
|
screenDir: path.resolve('images'),
|
21
23
|
reportDir: path.resolve('report'),
|
24
|
+
testsDir: path.resolve('src'),
|
22
25
|
reporter: process.env.TEAMCITY_VERSION ? TeamcityReporter : CreeveyReporter,
|
23
|
-
storiesProvider:
|
26
|
+
storiesProvider: hybridStoriesProvider,
|
24
27
|
maxRetries: 0,
|
25
28
|
testTimeout: 30000,
|
26
29
|
diffOptions: { threshold: 0.1, includeAA: false },
|
27
30
|
odiffOptions: { threshold: 0.1, antialiasing: true },
|
28
31
|
browsers: { [defaultBrowser]: true },
|
29
32
|
hooks: {},
|
30
|
-
testsRegex: /\.creevey\.(t|j)s$/,
|
33
|
+
testsRegex: /\.creevey\.(m|c)?(t|j)s$/,
|
31
34
|
};
|
32
35
|
|
33
36
|
function normalizeBrowserConfig(name: string, config: BrowserConfig): BrowserConfigObject {
|
@@ -73,7 +76,7 @@ export async function readConfig(options: Options): Promise<Config> {
|
|
73
76
|
|
74
77
|
if (!configData.webdriver) {
|
75
78
|
const { SeleniumWebdriver } = await import('./selenium/webdriver.js');
|
76
|
-
logger.warn(
|
79
|
+
logger().warn(
|
77
80
|
"Creevey supports `Selenium` and `Playwright` webdrivers. For backward compatibility `Selenium` is used by default, but it might changed in the future. Please explicitly specify one of webdrivers in your Creevey's config",
|
78
81
|
);
|
79
82
|
configData.webdriver = SeleniumWebdriver;
|
@@ -82,10 +85,29 @@ export async function readConfig(options: Options): Promise<Config> {
|
|
82
85
|
Object.assign(userConfig, configData);
|
83
86
|
}
|
84
87
|
|
88
|
+
if (userConfig.resolveStorybookUrl && !options.storybookUrl) {
|
89
|
+
userConfig.storybookUrl = await userConfig.resolveStorybookUrl();
|
90
|
+
}
|
91
|
+
|
92
|
+
if (options.noDocker) userConfig.useDocker = false;
|
85
93
|
if (options.failFast != undefined) userConfig.failFast = Boolean(options.failFast);
|
86
94
|
if (options.reportDir) userConfig.reportDir = path.resolve(options.reportDir);
|
87
95
|
if (options.screenDir) userConfig.screenDir = path.resolve(options.screenDir);
|
88
96
|
if (options.storybookUrl) userConfig.storybookUrl = options.storybookUrl;
|
97
|
+
if (options.storybookPort && cluster.isPrimary) {
|
98
|
+
const url = new URL(userConfig.storybookUrl);
|
99
|
+
url.port = options.storybookPort;
|
100
|
+
userConfig.storybookUrl = url.toString();
|
101
|
+
}
|
102
|
+
if (typeof options.storybookStart === 'string') userConfig.storybookAutorunCmd = options.storybookStart;
|
103
|
+
|
104
|
+
if (options.storybookStart && cluster.isPrimary) {
|
105
|
+
const { default: getPort } = await import('get-port');
|
106
|
+
const url = new URL(userConfig.storybookUrl);
|
107
|
+
const port = await getPort({ port: Number(url.port) });
|
108
|
+
url.port = `${port}`;
|
109
|
+
userConfig.storybookUrl = url.toString();
|
110
|
+
}
|
89
111
|
|
90
112
|
// NOTE: Hack to pass typescript checking
|
91
113
|
const config = userConfig as Config;
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import type { Config, Options } from '../types';
|
2
|
+
import { waitOnUrl } from './utils.js';
|
3
|
+
import { logger } from './logger.js';
|
4
|
+
|
5
|
+
const RESPONSE_CHECK_TIMEOUT_MS = 10000;
|
6
|
+
const RESPONSE_CHECK_INTERVAL_MS = 200;
|
7
|
+
|
8
|
+
export function getStorybookUrl({ storybookUrl }: Config, { storybookStart }: Options): [string, string | undefined] {
|
9
|
+
if (storybookStart) {
|
10
|
+
const url = new URL(storybookUrl);
|
11
|
+
url.hostname = 'localhost';
|
12
|
+
return [url.toString(), storybookUrl];
|
13
|
+
}
|
14
|
+
return [storybookUrl, undefined];
|
15
|
+
}
|
16
|
+
|
17
|
+
export async function checkIsStorybookConnected(url: string) {
|
18
|
+
try {
|
19
|
+
await waitOnUrl(url, RESPONSE_CHECK_TIMEOUT_MS, RESPONSE_CHECK_INTERVAL_MS);
|
20
|
+
return true;
|
21
|
+
} catch (reason: unknown) {
|
22
|
+
const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
|
23
|
+
logger().error(error);
|
24
|
+
return false;
|
25
|
+
}
|
26
|
+
}
|