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.
Files changed (211) hide show
  1. package/README.md +19 -41
  2. package/dist/client/addon/components/Addon.js +17 -7
  3. package/dist/client/addon/components/Addon.js.map +1 -1
  4. package/dist/client/addon/components/Panel.js +2 -2
  5. package/dist/client/addon/components/Panel.js.map +1 -1
  6. package/dist/client/addon/components/Tools.js +17 -7
  7. package/dist/client/addon/components/Tools.js.map +1 -1
  8. package/dist/client/addon/withCreevey.d.ts +2 -1
  9. package/dist/client/addon/withCreevey.js +11 -1
  10. package/dist/client/addon/withCreevey.js.map +1 -1
  11. package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
  12. package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
  13. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  14. package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
  15. package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
  16. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  17. package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
  18. package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
  19. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  20. package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
  21. package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
  22. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  23. package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
  24. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  25. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  26. package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
  27. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  28. package/dist/client/shared/components/ResultsPage.d.ts +1 -1
  29. package/dist/client/shared/components/ResultsPage.js +43 -13
  30. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  31. package/dist/client/shared/creeveyClientApi.js +8 -1
  32. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  33. package/dist/client/shared/helpers.d.ts +1 -3
  34. package/dist/client/shared/helpers.js +4 -19
  35. package/dist/client/shared/helpers.js.map +1 -1
  36. package/dist/client/web/CreeveyApp.js +42 -14
  37. package/dist/client/web/CreeveyApp.js.map +1 -1
  38. package/dist/client/web/CreeveyContext.d.ts +5 -0
  39. package/dist/client/web/CreeveyContext.js +20 -7
  40. package/dist/client/web/CreeveyContext.js.map +1 -1
  41. package/dist/client/web/CreeveyLoader.js +2 -2
  42. package/dist/client/web/CreeveyLoader.js.map +1 -1
  43. package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
  44. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  45. package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
  46. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  47. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
  48. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  49. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
  50. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  51. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
  52. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
  53. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  54. package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
  55. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  56. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
  57. package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
  58. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  59. package/dist/client/web/KeyboardEventsContext.js +79 -64
  60. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  61. package/dist/client/web/assets/index-B0Xv0lOY.js +802 -0
  62. package/dist/client/web/index.html +1 -1
  63. package/dist/client/web/index.js +17 -7
  64. package/dist/client/web/index.js.map +1 -1
  65. package/dist/client/web/themes.d.ts +2 -0
  66. package/dist/client/web/themes.js +22 -0
  67. package/dist/client/web/themes.js.map +1 -0
  68. package/dist/creevey.js +16 -9
  69. package/dist/creevey.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/server/config.d.ts +1 -1
  72. package/dist/server/config.js +27 -5
  73. package/dist/server/config.js.map +1 -1
  74. package/dist/server/connection.d.ts +3 -0
  75. package/dist/server/connection.js +28 -0
  76. package/dist/server/connection.js.map +1 -0
  77. package/dist/server/docker.d.ts +1 -1
  78. package/dist/server/docker.js +56 -32
  79. package/dist/server/docker.js.map +1 -1
  80. package/dist/server/index.js +64 -11
  81. package/dist/server/index.js.map +1 -1
  82. package/dist/server/logger.d.ts +2 -1
  83. package/dist/server/logger.js +7 -3
  84. package/dist/server/logger.js.map +1 -1
  85. package/dist/server/master/api.js +1 -1
  86. package/dist/server/master/api.js.map +1 -1
  87. package/dist/server/master/pool.d.ts +4 -3
  88. package/dist/server/master/pool.js +13 -66
  89. package/dist/server/master/pool.js.map +1 -1
  90. package/dist/server/master/queue.d.ts +13 -0
  91. package/dist/server/master/queue.js +71 -0
  92. package/dist/server/master/queue.js.map +1 -0
  93. package/dist/server/master/runner.d.ts +3 -0
  94. package/dist/server/master/runner.js +76 -10
  95. package/dist/server/master/runner.js.map +1 -1
  96. package/dist/server/master/server.js +1 -1
  97. package/dist/server/master/server.js.map +1 -1
  98. package/dist/server/master/start.js +13 -11
  99. package/dist/server/master/start.js.map +1 -1
  100. package/dist/server/playwright/docker-file.d.ts +1 -1
  101. package/dist/server/playwright/docker-file.js +15 -6
  102. package/dist/server/playwright/docker-file.js.map +1 -1
  103. package/dist/server/playwright/docker.d.ts +2 -1
  104. package/dist/server/playwright/docker.js +10 -2
  105. package/dist/server/playwright/docker.js.map +1 -1
  106. package/dist/server/playwright/index-source.mjs +16 -0
  107. package/dist/server/playwright/internal.d.ts +6 -6
  108. package/dist/server/playwright/internal.js +143 -91
  109. package/dist/server/playwright/internal.js.map +1 -1
  110. package/dist/server/playwright/webdriver.d.ts +1 -1
  111. package/dist/server/playwright/webdriver.js +5 -8
  112. package/dist/server/playwright/webdriver.js.map +1 -1
  113. package/dist/server/providers/browser.js +6 -4
  114. package/dist/server/providers/browser.js.map +1 -1
  115. package/dist/server/providers/hybrid.js +1 -1
  116. package/dist/server/providers/hybrid.js.map +1 -1
  117. package/dist/server/reporter.d.ts +4 -19
  118. package/dist/server/reporter.js +30 -21
  119. package/dist/server/reporter.js.map +1 -1
  120. package/dist/server/selenium/internal.d.ts +3 -4
  121. package/dist/server/selenium/internal.js +127 -108
  122. package/dist/server/selenium/internal.js.map +1 -1
  123. package/dist/server/selenium/selenoid.js +8 -6
  124. package/dist/server/selenium/selenoid.js.map +1 -1
  125. package/dist/server/selenium/webdriver.d.ts +1 -1
  126. package/dist/server/selenium/webdriver.js +5 -9
  127. package/dist/server/selenium/webdriver.js.map +1 -1
  128. package/dist/server/telemetry.js +2 -2
  129. package/dist/server/testsFiles/parser.js +45 -5
  130. package/dist/server/testsFiles/parser.js.map +1 -1
  131. package/dist/server/utils.d.ts +19 -1
  132. package/dist/server/utils.js +87 -8
  133. package/dist/server/utils.js.map +1 -1
  134. package/dist/server/webdriver.d.ts +5 -4
  135. package/dist/server/webdriver.js +23 -10
  136. package/dist/server/webdriver.js.map +1 -1
  137. package/dist/server/worker/chai-image.d.ts +1 -2
  138. package/dist/server/worker/chai-image.js +4 -3
  139. package/dist/server/worker/chai-image.js.map +1 -1
  140. package/dist/server/worker/context.d.ts +3 -0
  141. package/dist/server/worker/context.js +15 -0
  142. package/dist/server/worker/context.js.map +1 -0
  143. package/dist/server/worker/match-image.d.ts +4 -4
  144. package/dist/server/worker/match-image.js +7 -4
  145. package/dist/server/worker/match-image.js.map +1 -1
  146. package/dist/server/worker/start.js +45 -73
  147. package/dist/server/worker/start.js.map +1 -1
  148. package/dist/shared/index.d.ts +1 -1
  149. package/dist/types.d.ts +40 -8
  150. package/dist/types.js +2 -0
  151. package/dist/types.js.map +1 -1
  152. package/docs/cli.md +12 -0
  153. package/docs/config.md +179 -165
  154. package/docs/storybook.md +60 -0
  155. package/docs/tests.md +50 -45
  156. package/package.json +64 -63
  157. package/src/client/addon/components/Panel.tsx +2 -2
  158. package/src/client/addon/withCreevey.ts +10 -2
  159. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  160. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  161. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  162. package/src/client/shared/components/ResultsPage.tsx +31 -8
  163. package/src/client/shared/creeveyClientApi.ts +9 -1
  164. package/src/client/shared/helpers.ts +4 -24
  165. package/src/client/web/CreeveyApp.tsx +27 -8
  166. package/src/client/web/CreeveyContext.tsx +9 -0
  167. package/src/client/web/CreeveyLoader.tsx +1 -1
  168. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  169. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  170. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  171. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  172. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  173. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  174. package/src/client/web/themes.ts +24 -0
  175. package/src/creevey.ts +16 -10
  176. package/src/server/config.ts +28 -6
  177. package/src/server/connection.ts +26 -0
  178. package/src/server/docker.ts +63 -34
  179. package/src/server/index.ts +72 -14
  180. package/src/server/logger.ts +6 -2
  181. package/src/server/master/api.ts +1 -1
  182. package/src/server/master/pool.ts +23 -59
  183. package/src/server/master/queue.ts +77 -0
  184. package/src/server/master/runner.ts +94 -10
  185. package/src/server/master/server.ts +1 -1
  186. package/src/server/master/start.ts +16 -11
  187. package/src/server/playwright/docker-file.ts +18 -6
  188. package/src/server/playwright/docker.ts +16 -3
  189. package/src/server/playwright/index-source.mjs +16 -0
  190. package/src/server/playwright/internal.ts +182 -111
  191. package/src/server/playwright/webdriver.ts +6 -9
  192. package/src/server/providers/browser.ts +6 -4
  193. package/src/server/providers/hybrid.ts +1 -1
  194. package/src/server/reporter.ts +37 -34
  195. package/src/server/selenium/internal.ts +131 -116
  196. package/src/server/selenium/selenoid.ts +8 -6
  197. package/src/server/selenium/webdriver.ts +6 -10
  198. package/src/server/telemetry.ts +2 -2
  199. package/src/server/testsFiles/parser.ts +52 -4
  200. package/src/server/utils.ts +97 -9
  201. package/src/server/webdriver.ts +24 -16
  202. package/src/server/worker/chai-image.ts +4 -4
  203. package/src/server/worker/context.ts +14 -0
  204. package/src/server/worker/match-image.ts +12 -8
  205. package/src/server/worker/start.ts +49 -86
  206. package/src/shared/index.ts +1 -1
  207. package/src/types.ts +44 -8
  208. package/types/global.d.ts +1 -0
  209. package/.yarnrc.yml +0 -1
  210. package/chromatic.config.json +0 -5
  211. 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] ?? { images: {} };
63
- const [imageName, setImageName] = useState(Object.keys(result.images ?? {})[0] ?? '');
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 <= 1) return;
108
+ if (failedTests.length == 0) return;
107
109
  const currentTest = failedTests.findIndex((t) => t.id === openedTest?.id);
108
- const nextFailedTest = failedTests[currentTest + 1] || failedTests[0];
109
- handleOpenTest(nextFailedTest);
110
- }, [failedTests, handleOpenTest, openedTest?.id]);
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 '../shared/helpers.js';
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, useContext, useRef, useState } from 'react';
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 { KeyboardEventsContext } from '../../KeyboardEventsContext.js';
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 } = useContext(KeyboardEventsContext);
103
+ const { setSidebarFocusedItem } = useCreeveyContext();
104
104
  const [focussed, onSetFocussed] = useState(false);
105
105
  const searchRef = useRef<HTMLInputElement>(null);
106
106
 
@@ -62,6 +62,7 @@ const SelectAllContainer = styled.div({
62
62
  });
63
63
 
64
64
  const TestsContainer = styled.div({
65
+ marginBottom: '8px',
65
66
  position: 'relative',
66
67
  height: '100%',
67
68
  });
@@ -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
- <Button variant="solid" size="medium" onClick={onImageApprove} disabled={!onImageApprove}>
30
- Approve
31
- <ChevronRightIcon />
32
- </Button>
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, useContext, useEffect, useMemo } from 'react';
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 { CreeveyContext } from '../../CreeveyContext.js';
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 } = useContext(CreeveyContext);
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 { CreeveyContext } from '../../CreeveyContext.js';
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 } = useContext(CreeveyContext);
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, useState, useEffect, useCallback, useContext, PropsWithChildren } from 'react';
2
- import { CreeveySuite, isTest, noop } from '../../types.js';
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 { CreeveyContext } from './CreeveyContext.js';
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 [sidebarFocusedItem, setSidebarFocusedItem] = useState<FocusableItem>([]);
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
- if (e.code === 'Enter') {
49
- if (sidebarFocusedItem.length === 0) return;
50
-
51
- const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
52
- if (!focusedSuite) return;
53
- if (!isTest(focusedSuite)) {
54
- e.preventDefault();
55
- onSuiteOpen(focusedSuite.path, !focusedSuite.opened);
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
- if (isTest(focusedSuite) && focusedSuite.results?.length == 0) {
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
- return;
61
- }
62
- if (e.code === 'Space') {
63
- e.preventDefault();
64
- const focusedSuite = getSuiteByPath(rootSuite, sidebarFocusedItem);
65
- if (!focusedSuite) return;
66
- const path = isTest(focusedSuite) ? getTestPath(focusedSuite) : focusedSuite.path;
67
- onSuiteToggle(path, !focusedSuite.checked);
68
- }
69
- if (e.code === 'ArrowDown') {
70
- const currentIndex = sidebarFocusedItem.length === 0 ? -1 : getFocusedItemIndex(sidebarFocusedItem);
71
- if (currentIndex === suiteList.length - 1) return;
72
- const nextSuite = suiteList[currentIndex + 1];
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
- const path = isTest(focusedSuite) ? getTestPath(focusedSuite) : focusedSuite.path;
101
- setSidebarFocusedItem(path.slice(0, -1));
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
 
@@ -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 browserStoriesProvider } from './providers/browser.js';
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' | 'testsDir' | 'tsConfig' | 'webdriver'> = {
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: browserStoriesProvider,
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
+ }