creevey 0.10.0-beta.43 → 0.10.0-beta.45

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 (137) hide show
  1. package/CHANGELOG.md +282 -0
  2. package/dist/client/addon/controller.js +1 -1
  3. package/dist/client/addon/controller.js.map +1 -1
  4. package/dist/client/addon/withCreevey.js +1 -18
  5. package/dist/client/addon/withCreevey.js.map +1 -1
  6. package/dist/client/shared/components/PageHeader/PageHeader.js +13 -4
  7. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  8. package/dist/client/shared/creeveyClientApi.js +10 -0
  9. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  10. package/dist/client/web/CreeveyApp.d.ts +1 -0
  11. package/dist/client/web/CreeveyApp.js +1 -0
  12. package/dist/client/web/CreeveyApp.js.map +1 -1
  13. package/dist/client/web/CreeveyContext.d.ts +1 -0
  14. package/dist/client/web/CreeveyContext.js +1 -0
  15. package/dist/client/web/CreeveyContext.js.map +1 -1
  16. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +9 -8
  17. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  18. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +13 -3
  19. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  20. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +2 -3
  21. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  22. package/dist/client/web/CreeveyView/SideBar/TestLink.js +2 -3
  23. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  24. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +1 -0
  25. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
  26. package/dist/client/web/assets/{index-C47njyZV.js → index-BU4jjKVC.js} +68 -68
  27. package/dist/client/web/index.html +1 -1
  28. package/dist/client/web/index.js +8 -3
  29. package/dist/client/web/index.js.map +1 -1
  30. package/dist/creevey.d.ts +1 -1
  31. package/dist/creevey.js +1 -22
  32. package/dist/creevey.js.map +1 -1
  33. package/dist/playwright-reporter.d.ts +2 -0
  34. package/dist/playwright-reporter.js +5 -0
  35. package/dist/playwright-reporter.js.map +1 -0
  36. package/dist/playwright.d.ts +1 -1
  37. package/dist/server/config.js +8 -1
  38. package/dist/server/config.js.map +1 -1
  39. package/dist/server/index.js +12 -5
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/master/api.d.ts +11 -6
  42. package/dist/server/master/api.js +88 -25
  43. package/dist/server/master/api.js.map +1 -1
  44. package/dist/server/master/handlers/capture-handler.d.ts +5 -0
  45. package/dist/server/master/handlers/capture-handler.js +25 -0
  46. package/dist/server/master/handlers/capture-handler.js.map +1 -0
  47. package/dist/server/master/handlers/index.d.ts +4 -0
  48. package/dist/server/master/handlers/index.js +21 -0
  49. package/dist/server/master/handlers/index.js.map +1 -0
  50. package/dist/server/master/handlers/ping-handler.d.ts +2 -0
  51. package/dist/server/master/handlers/ping-handler.js +8 -0
  52. package/dist/server/master/handlers/ping-handler.js.map +1 -0
  53. package/dist/server/master/handlers/static-handler.d.ts +1 -0
  54. package/dist/server/master/handlers/static-handler.js +22 -0
  55. package/dist/server/master/handlers/static-handler.js.map +1 -0
  56. package/dist/server/master/handlers/stories-handler.d.ts +4 -0
  57. package/dist/server/master/handlers/stories-handler.js +24 -0
  58. package/dist/server/master/handlers/stories-handler.js.map +1 -0
  59. package/dist/server/master/master.js +7 -24
  60. package/dist/server/master/master.js.map +1 -1
  61. package/dist/server/master/runner.d.ts +4 -6
  62. package/dist/server/master/runner.js +30 -127
  63. package/dist/server/master/runner.js.map +1 -1
  64. package/dist/server/master/server.js +191 -89
  65. package/dist/server/master/server.js.map +1 -1
  66. package/dist/server/master/start.d.ts +1 -2
  67. package/dist/server/master/start.js +11 -29
  68. package/dist/server/master/start.js.map +1 -1
  69. package/dist/server/master/testsManager.d.ts +81 -0
  70. package/dist/server/master/testsManager.js +281 -0
  71. package/dist/server/master/testsManager.js.map +1 -0
  72. package/dist/server/playwright/docker-file.js +2 -2
  73. package/dist/server/playwright/docker-file.js.map +1 -1
  74. package/dist/server/playwright/reporter.d.ts +87 -0
  75. package/dist/server/playwright/reporter.js +351 -0
  76. package/dist/server/playwright/reporter.js.map +1 -0
  77. package/dist/server/selenium/internal.js +20 -2
  78. package/dist/server/selenium/internal.js.map +1 -1
  79. package/dist/server/selenium/selenoid.js +4 -0
  80. package/dist/server/selenium/selenoid.js.map +1 -1
  81. package/dist/server/shutdown.d.ts +1 -0
  82. package/dist/server/shutdown.js +23 -0
  83. package/dist/server/shutdown.js.map +1 -0
  84. package/dist/server/stories.d.ts +0 -1
  85. package/dist/server/stories.js +0 -12
  86. package/dist/server/stories.js.map +1 -1
  87. package/dist/server/ui-update.d.ts +10 -0
  88. package/dist/server/ui-update.js +39 -0
  89. package/dist/server/ui-update.js.map +1 -0
  90. package/dist/server/utils.d.ts +6 -0
  91. package/dist/server/utils.js +39 -8
  92. package/dist/server/utils.js.map +1 -1
  93. package/dist/server/worker/start.js +1 -1
  94. package/dist/server/worker/start.js.map +1 -1
  95. package/dist/types.d.ts +14 -8
  96. package/dist/types.js.map +1 -1
  97. package/docs/examples/playwright-reporter-example.ts +202 -0
  98. package/docs/migration-0.9-to-0.10.md +144 -0
  99. package/docs/playwright-reporter.md +357 -0
  100. package/package.json +10 -14
  101. package/src/client/addon/controller.ts +1 -1
  102. package/src/client/addon/withCreevey.ts +2 -16
  103. package/src/client/shared/components/PageHeader/PageHeader.tsx +18 -4
  104. package/src/client/shared/creeveyClientApi.ts +10 -0
  105. package/src/client/web/CreeveyApp.tsx +2 -0
  106. package/src/client/web/CreeveyContext.tsx +2 -0
  107. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +19 -17
  108. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +18 -3
  109. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +9 -7
  110. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
  111. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +1 -0
  112. package/src/client/web/index.tsx +8 -3
  113. package/src/creevey.ts +1 -24
  114. package/src/playwright-reporter.ts +3 -0
  115. package/src/server/config.ts +9 -1
  116. package/src/server/index.ts +13 -6
  117. package/src/server/master/api.ts +94 -28
  118. package/src/server/master/handlers/capture-handler.ts +20 -0
  119. package/src/server/master/handlers/index.ts +4 -0
  120. package/src/server/master/handlers/ping-handler.ts +6 -0
  121. package/src/server/master/handlers/static-handler.ts +18 -0
  122. package/src/server/master/handlers/stories-handler.ts +20 -0
  123. package/src/server/master/master.ts +10 -27
  124. package/src/server/master/runner.ts +38 -132
  125. package/src/server/master/server.ts +210 -98
  126. package/src/server/master/start.ts +17 -41
  127. package/src/server/master/testsManager.ts +315 -0
  128. package/src/server/playwright/docker-file.ts +2 -2
  129. package/src/server/playwright/reporter.ts +386 -0
  130. package/src/server/selenium/internal.ts +23 -3
  131. package/src/server/selenium/selenoid.ts +5 -0
  132. package/src/server/shutdown.ts +19 -0
  133. package/src/server/stories.ts +1 -12
  134. package/src/server/ui-update.ts +46 -0
  135. package/src/server/utils.ts +40 -9
  136. package/src/server/worker/start.ts +1 -1
  137. package/src/types.ts +14 -8
@@ -16,6 +16,7 @@ export async function initCreeveyClientApi(): Promise<CreeveyClientApi> {
16
16
  const updateListeners = new Set<(update: CreeveyUpdate) => void>();
17
17
  let statusRequest: Promise<CreeveyStatus> | null = null;
18
18
  let statusResolver: (status: CreeveyStatus) => void = noop;
19
+ let isUpdateMode = false;
19
20
 
20
21
  const ws = new WebSocket(`ws://${getConnectionUrl()}`);
21
22
 
@@ -30,9 +31,17 @@ export async function initCreeveyClientApi(): Promise<CreeveyClientApi> {
30
31
  ws.addEventListener('open', () => {
31
32
  clientApiResolver({
32
33
  start(ids: string[]) {
34
+ if (isUpdateMode) {
35
+ console.warn('Tests cannot be started in Update Mode. This mode is for approving screenshots only.');
36
+ return;
37
+ }
33
38
  send({ type: 'start', payload: ids });
34
39
  },
35
40
  stop() {
41
+ if (isUpdateMode) {
42
+ console.warn('Tests cannot be stopped in Update Mode. This mode is for approving screenshots only.');
43
+ return;
44
+ }
36
45
  send({ type: 'stop' });
37
46
  },
38
47
  approve(id: string, retry: number, image: string) {
@@ -62,6 +71,7 @@ export async function initCreeveyClientApi(): Promise<CreeveyClientApi> {
62
71
  fn(data.payload);
63
72
  });
64
73
  if (data.type == 'status') {
74
+ isUpdateMode = data.payload.isUpdateMode;
65
75
  statusResolver(data.payload);
66
76
  statusResolver = noop;
67
77
  statusRequest = null;
@@ -29,6 +29,7 @@ export interface CreeveyAppProps {
29
29
  tests: CreeveySuite;
30
30
  isRunning: boolean;
31
31
  isReport: boolean;
32
+ isUpdateMode: boolean;
32
33
  };
33
34
  }
34
35
 
@@ -214,6 +215,7 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
214
215
  onSuiteToggle: handleSuiteToggle,
215
216
  sidebarFocusedItem,
216
217
  setSidebarFocusedItem,
218
+ isUpdateMode: initialState.isUpdateMode,
217
219
  }}
218
220
  >
219
221
  <ThemeProvider theme={ensure(themes[theme])}>
@@ -7,6 +7,7 @@ export type FocusableItem = null | SuitePath;
7
7
  export interface CreeveyContextType {
8
8
  isReport: boolean;
9
9
  isRunning: boolean;
10
+ isUpdateMode: boolean;
10
11
  onStop: () => void;
11
12
  onImageNext?: () => void;
12
13
  onImageApprove?: () => void;
@@ -21,6 +22,7 @@ export interface CreeveyContextType {
21
22
  export const CreeveyContext = React.createContext<CreeveyContextType>({
22
23
  isReport: true,
23
24
  isRunning: false,
25
+ isUpdateMode: false,
24
26
  onImageNext: noop,
25
27
  onImageApprove: noop,
26
28
  onApproveAll: noop,
@@ -48,23 +48,25 @@ export function SideBarFooter(): JSX.Element {
48
48
  }, [handleKeyDown, handleKeyUp]);
49
49
 
50
50
  return (
51
- <Sticky>
52
- <Container>
53
- {isAlt ? (
54
- <Button variant="outline" size="medium" onClick={onImageNext} disabled={!onImageApprove}>
55
- Next
56
- <ChevronRightIcon />
51
+ <>
52
+ <Sticky>
53
+ <Container>
54
+ {isAlt ? (
55
+ <Button variant="outline" size="medium" onClick={onImageNext} disabled={!onImageApprove}>
56
+ Next
57
+ <ChevronRightIcon />
58
+ </Button>
59
+ ) : (
60
+ <Button variant="solid" size="medium" onClick={onImageApprove} disabled={!onImageApprove}>
61
+ Approve
62
+ <ChevronRightIcon />
63
+ </Button>
64
+ )}
65
+ <Button variant="outline" size="medium" onClick={onApproveAll}>
66
+ Approve all
57
67
  </Button>
58
- ) : (
59
- <Button variant="solid" size="medium" onClick={onImageApprove} disabled={!onImageApprove}>
60
- Approve
61
- <ChevronRightIcon />
62
- </Button>
63
- )}
64
- <Button variant="outline" size="medium" onClick={onApproveAll}>
65
- Approve all
66
- </Button>
67
- </Container>
68
- </Sticky>
68
+ </Container>
69
+ </Sticky>
70
+ </>
69
71
  );
70
72
  }
@@ -36,6 +36,7 @@ const Container = styled.div({
36
36
  const Header = styled.h2({
37
37
  fontWeight: 'normal',
38
38
  margin: 0,
39
+ padding: '2px 6px',
39
40
  });
40
41
 
41
42
  const Button = withTheme(
@@ -58,12 +59,23 @@ const Button = withTheme(
58
59
  })),
59
60
  );
60
61
 
62
+ const UpdateModeDescription = withTheme(
63
+ styled.div(({ theme }) => ({
64
+ fontSize: '0.8em',
65
+ marginTop: '4px',
66
+ padding: '2px 6px',
67
+ color: theme.color.positive,
68
+ backgroundColor: `${theme.color.positive}20`,
69
+ })),
70
+ );
71
+
61
72
  const MarginContainer = styled.div<{ left?: string; right?: string; top?: string; bottom?: string }>(
62
73
  ({ left, right, top, bottom }) => ({
63
74
  marginLeft: left ?? 0,
64
75
  marginRight: right ?? 0,
65
76
  marginTop: top ?? 0,
66
77
  marginBottom: bottom ?? 0,
78
+ padding: '2px 6px',
67
79
  }),
68
80
  );
69
81
 
@@ -92,7 +104,7 @@ export function SideBarHeader({
92
104
  onFilterChange,
93
105
  canStart,
94
106
  }: SideBarHeaderProps): JSX.Element {
95
- const { isReport, isRunning } = useContext(CreeveyContext);
107
+ const { isReport, isRunning, isUpdateMode } = useContext(CreeveyContext);
96
108
  const [filterInput, setFilterInput] = useState('');
97
109
 
98
110
  const handleClickByStatus = (status: TestStatus): void => {
@@ -115,9 +127,12 @@ export function SideBarHeader({
115
127
  <Container>
116
128
  <div>
117
129
  <Header>colin.creevey</Header>
130
+ {isUpdateMode && (
131
+ <UpdateModeDescription>Review and approve screenshots from previous test runs</UpdateModeDescription>
132
+ )}
118
133
  <TestsStatus {...testsStatus} onClickByStatus={handleClickByStatus} />
119
134
  </div>
120
- {isReport ? null : (
135
+ {isReport || isUpdateMode ? null : (
121
136
  <MarginContainer top="10px">
122
137
  {isRunning ? (
123
138
  <Button variant="outline" onClick={onStop}>
@@ -131,7 +146,7 @@ export function SideBarHeader({
131
146
  </MarginContainer>
132
147
  )}
133
148
  </Container>
134
- <MarginContainer top="24px" bottom="24px">
149
+ <MarginContainer top="12px" bottom="12px">
135
150
  <Search onChange={handleInputFilterChange} value={filterInput} />
136
151
  </MarginContainer>
137
152
  </Sticky>
@@ -78,7 +78,7 @@ export const SuiteTitle = styled.span({
78
78
  });
79
79
 
80
80
  export function SuiteLink({ title, suite, 'data-testid': dataTid }: SuiteLinkProps): JSX.Element {
81
- const { onSuiteOpen, onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem } = useCreeveyContext();
81
+ const { onSuiteOpen, onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem, isUpdateMode } = useCreeveyContext();
82
82
  const checkboxRef = useRef<Checkbox>(null);
83
83
  const buttonRef = useRef<HTMLButtonElement | null>(null);
84
84
 
@@ -117,12 +117,14 @@ export function SuiteLink({ title, suite, 'data-testid': dataTid }: SuiteLinkPro
117
117
  return (
118
118
  <Container focused={isSuiteFocused}>
119
119
  <CheckboxContainer>
120
- <Checkbox
121
- ref={checkboxRef}
122
- checked={suite.skip ? false : suite.checked}
123
- disabled={Boolean(suite.skip)}
124
- onValueChange={handleCheck}
125
- />
120
+ {!isUpdateMode && (
121
+ <Checkbox
122
+ ref={checkboxRef}
123
+ checked={suite.skip ? false : suite.checked}
124
+ disabled={Boolean(suite.skip)}
125
+ onValueChange={handleCheck}
126
+ />
127
+ )}
126
128
  </CheckboxContainer>
127
129
  <Button onClick={handleOpen} onFocus={handleFocus} data-testid={dataTid} ref={buttonRef}>
128
130
  <SuiteContainer padding={(suite.path.length - 1) * 8}>
@@ -19,7 +19,7 @@ const TestContainer = styled(SuiteContainer)({
19
19
  });
20
20
 
21
21
  export function TestLink({ title, opened, test }: TestLinkProps): JSX.Element {
22
- const { onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem } = useCreeveyContext();
22
+ const { onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem, isUpdateMode } = useCreeveyContext();
23
23
  const { onOpenTest } = useContext(SideBarContext);
24
24
  const buttonRef = useRef<HTMLButtonElement | null>(null);
25
25
 
@@ -55,11 +55,13 @@ export function TestLink({ title, opened, test }: TestLinkProps): JSX.Element {
55
55
  {/* NOTE Little hack to allow click on checkbox and don't trigger Button click */}
56
56
  {/* We can use other approach, but checkbox has vertical-align: top */}
57
57
  <CheckboxContainer>
58
- <Checkbox
59
- checked={test.skip ? false : test.checked}
60
- disabled={Boolean(test.skip)}
61
- onValueChange={handleCheck}
62
- />
58
+ {!isUpdateMode && (
59
+ <Checkbox
60
+ checked={test.skip ? false : test.checked}
61
+ disabled={Boolean(test.skip)}
62
+ onValueChange={handleCheck}
63
+ />
64
+ )}
63
65
  </CheckboxContainer>
64
66
  <Button onClick={handleOpen} disabled={emptyResults} ref={buttonRef}>
65
67
  <TestContainer padding={(testPath.length + 1) * 8}>
@@ -15,6 +15,7 @@ const Container = styled.div({
15
15
  alignItems: 'center',
16
16
  fontSize: '14px',
17
17
  lineHeight: '22px',
18
+ padding: '2px 6px',
18
19
  });
19
20
 
20
21
  const IconContainer = styled.div<{ color?: string }>(({ color }) => ({
@@ -36,11 +36,11 @@ const CreeveyAppAsync = React.lazy(async () => {
36
36
  // NOTE: Failed to get status from API
37
37
  // NOTE: It might happen on circle ci from artifact
38
38
  isReport = true;
39
- creeveyStatus = { isRunning: false, tests: await loadCreeveyData(), browsers: [] };
39
+ creeveyStatus = { isRunning: false, tests: await loadCreeveyData(), browsers: [], isUpdateMode: false };
40
40
  }
41
41
  } else {
42
42
  isReport = true;
43
- creeveyStatus = { isRunning: false, tests: await loadCreeveyData(), browsers: [] };
43
+ creeveyStatus = { isRunning: false, tests: await loadCreeveyData(), browsers: [], isUpdateMode: false };
44
44
  }
45
45
 
46
46
  return {
@@ -48,7 +48,12 @@ const CreeveyAppAsync = React.lazy(async () => {
48
48
  return (
49
49
  <CreeveyApp
50
50
  api={creeveyApi}
51
- initialState={{ isReport, isRunning: creeveyStatus.isRunning, tests: treeifyTests(creeveyStatus.tests) }}
51
+ initialState={{
52
+ isReport,
53
+ isRunning: creeveyStatus.isRunning,
54
+ tests: treeifyTests(creeveyStatus.tests),
55
+ isUpdateMode: creeveyStatus.isUpdateMode,
56
+ }}
52
57
  />
53
58
  );
54
59
  },
package/src/creevey.ts CHANGED
@@ -2,32 +2,9 @@ import cluster from 'cluster';
2
2
  import minimist from 'minimist';
3
3
  import creevey from './server/index.js';
4
4
  import { Options } from './types.js';
5
- import { emitWorkerMessage } from './server/messages.js';
6
- import { isShuttingDown, shutdownWorkers } from './server/utils.js';
7
5
  import Logger from 'loglevel';
8
6
  import { logger, setRootName } from './server/logger.js';
9
-
10
- function shutdownOnException(reason: unknown): void {
11
- if (isShuttingDown.current) return;
12
-
13
- const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
14
-
15
- logger().error(error);
16
-
17
- process.exitCode = -1;
18
- if (cluster.isWorker) emitWorkerMessage({ type: 'error', payload: { subtype: 'unknown', error } });
19
- if (cluster.isPrimary) void shutdownWorkers();
20
- }
21
-
22
- process.on('uncaughtException', shutdownOnException);
23
- process.on('unhandledRejection', shutdownOnException);
24
- // TODO SIGINT Stuck with selenium
25
- process.on('SIGINT', () => {
26
- if (isShuttingDown.current) {
27
- process.exit(-1);
28
- }
29
- isShuttingDown.current = true;
30
- });
7
+ import './server/shutdown.js';
31
8
 
32
9
  const argv = minimist<Options>(process.argv.slice(2), {
33
10
  string: ['browser', 'config', 'reporter', 'reportDir', 'screenDir', 'gridUrl', 'storybookUrl', 'storybookPort'],
@@ -0,0 +1,3 @@
1
+ import { CreeveyPlaywrightReporter } from './server/playwright/reporter.js';
2
+
3
+ export default CreeveyPlaywrightReporter;
@@ -15,7 +15,7 @@ export const defaultConfig: Omit<Config, 'gridUrl' | 'tsConfig' | 'webdriver'> =
15
15
  disableTelemetry: false,
16
16
  useWorkerQueue: false,
17
17
  useDocker: true,
18
- dockerImage: 'aerokube/selenoid:latest-release', // TODO What about playwright?
18
+ dockerImage: 'aerokube/selenoid:latest', // TODO What about playwright?
19
19
  dockerImagePlatform: '',
20
20
  pullImages: true,
21
21
  failFast: false,
@@ -83,6 +83,14 @@ export async function readConfig(options: Options): Promise<Config> {
83
83
  configData.webdriver = SeleniumWebdriver;
84
84
  }
85
85
 
86
+ for (const key in configData) {
87
+ const configKey = key as keyof typeof configData;
88
+ if (configData[configKey] === undefined) {
89
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
90
+ delete configData[configKey];
91
+ }
92
+ }
93
+
86
94
  Object.assign(userConfig, configData);
87
95
  }
88
96
 
@@ -1,6 +1,6 @@
1
1
  import cluster from 'cluster';
2
2
  import path from 'path';
3
- import { exec } from 'shelljs';
3
+ import sh from 'shelljs';
4
4
  import { getUserAgent } from 'package-manager-detector/detect';
5
5
  import { resolveCommand } from 'package-manager-detector/commands';
6
6
  import { readConfig, defaultBrowser } from './config.js';
@@ -87,6 +87,7 @@ async function startWebdriverServer(browser: string, config: Config, options: Op
87
87
  }
88
88
  }
89
89
 
90
+ // TODO Why docker containers are not deleting after stop?
90
91
  export default async function (options: Options): Promise<void> {
91
92
  const config = await readConfig(options);
92
93
  const { browser = defaultBrowser, update, ui, port } = options;
@@ -104,7 +105,7 @@ export default async function (options: Options): Promise<void> {
104
105
  gridUrl = await startWebdriverServer(browser, config, options);
105
106
  }
106
107
 
107
- if (cluster.isPrimary) {
108
+ if (cluster.isPrimary && !update) {
108
109
  const [localUrl, remoteUrl] = getStorybookUrl(config, options);
109
110
 
110
111
  if (options.storybookStart) {
@@ -120,7 +121,7 @@ export default async function (options: Options): Promise<void> {
120
121
  if (remoteUrl && localUrl != remoteUrl) logger().info(`On your network - ${remoteUrl}`);
121
122
  logger().info('Waiting Storybook...');
122
123
 
123
- const storybook = exec(storybookCommand, { async: true });
124
+ const storybook = sh.exec(storybookCommand, { async: true });
124
125
  subscribeOn('shutdown', () => {
125
126
  if (storybook.pid) void killTree(storybook.pid);
126
127
  });
@@ -145,7 +146,15 @@ export default async function (options: Options): Promise<void> {
145
146
  }
146
147
  }
147
148
 
149
+ await import('./shutdown.js');
150
+
148
151
  switch (true) {
152
+ case Boolean(update) && Boolean(ui): {
153
+ // New UI Update mode
154
+ const { uiUpdate } = await import('./ui-update.js');
155
+ await uiUpdate(config, port);
156
+ return;
157
+ }
149
158
  case Boolean(update): {
150
159
  (await import('./update.js')).update(config, typeof update == 'string' ? update : undefined);
151
160
  return;
@@ -168,9 +177,7 @@ export default async function (options: Options): Promise<void> {
168
177
  }
169
178
  logger().info('Starting Master Process');
170
179
 
171
- const resolveApi = (await import('./master/server.js')).start(config.reportDir, port, ui);
172
-
173
- return (await import('./master/start.js')).start(gridUrl, config, options, resolveApi);
180
+ return (await import('./master/start.js')).start(gridUrl, config, options);
174
181
  }
175
182
  default: {
176
183
  logger().info(`Starting Worker for ${browser}`);
@@ -1,14 +1,10 @@
1
- import WebSocket from 'ws';
2
- import Runner from './runner.js';
3
- import { Request, Response, CreeveyUpdate } from '../../types.js';
1
+ import { Data, WebSocket, WebSocketServer } from 'ws';
2
+ import type { Request, Response, CreeveyUpdate } from '../../types.js';
3
+ import type { TestsManager } from './testsManager.js';
4
+ import type Runner from './runner.js';
4
5
  import { logger } from '../logger.js';
5
6
 
6
- export interface CreeveyApi {
7
- subscribe: (wss: WebSocket.Server) => void;
8
- handleMessage: (ws: WebSocket, message: WebSocket.Data) => void;
9
- }
10
-
11
- function broadcast(wss: WebSocket.Server, message: Response): void {
7
+ function broadcast(wss: WebSocketServer, message: Response): void {
12
8
  wss.clients.forEach((ws) => {
13
9
  if (ws.readyState === WebSocket.OPEN) {
14
10
  ws.send(JSON.stringify(message));
@@ -16,44 +12,114 @@ function broadcast(wss: WebSocket.Server, message: Response): void {
16
12
  });
17
13
  }
18
14
 
19
- export default function creeveyApi(runner: Runner): CreeveyApi {
20
- return {
21
- subscribe(wss: WebSocket.Server) {
22
- runner.on('update', (payload: CreeveyUpdate) => {
23
- broadcast(wss, { type: 'update', payload });
15
+ function send(ws: WebSocket, message: Response): void {
16
+ if (ws.readyState === WebSocket.OPEN) {
17
+ ws.send(JSON.stringify(message));
18
+ }
19
+ }
20
+
21
+ // The class-based implementation of CreeveyApi for native WebSockets
22
+ export class CreeveyApi {
23
+ private runner: Runner | null = null;
24
+ private testsManager: TestsManager;
25
+ private wss: WebSocketServer | null = null;
26
+
27
+ constructor(testsManager: TestsManager, runner?: Runner) {
28
+ this.testsManager = testsManager;
29
+
30
+ // Use the provided runner in normal mode, or keep as null in update mode
31
+ if (runner) {
32
+ this.runner = runner;
33
+ }
34
+ }
35
+
36
+ subscribe(wss: WebSocketServer): void {
37
+ this.wss = wss;
38
+
39
+ // If we have a runner, subscribe to its updates
40
+ if (this.runner) {
41
+ this.runner.on('update', (payload: CreeveyUpdate) => {
42
+ this.broadcastUpdate(payload);
43
+ });
44
+ } else {
45
+ // Subscribe to TestsManager updates
46
+ this.testsManager.on('update', (update: CreeveyUpdate) => {
47
+ this.broadcastUpdate(update);
24
48
  });
25
- },
49
+ }
50
+ }
26
51
 
27
- handleMessage(ws: WebSocket, message: WebSocket.Data) {
28
- if (typeof message != 'string') {
29
- logger().info('unhandled message', message);
30
- return;
31
- }
52
+ handleMessage(ws: WebSocket, message: Data): void {
53
+ if (typeof message != 'string') {
54
+ logger().info('unhandled message', message);
55
+ return;
56
+ }
32
57
 
33
- const command = JSON.parse(message) as Request;
58
+ const command = JSON.parse(message) as Request;
34
59
 
60
+ if (this.runner) {
61
+ // Normal mode handling with runner
35
62
  switch (command.type) {
36
63
  case 'status': {
37
- ws.send(JSON.stringify({ type: 'status', payload: runner.status }));
64
+ const status = this.runner.status;
65
+ send(ws, { type: 'status', payload: status });
38
66
  return;
39
67
  }
40
68
  case 'start': {
41
- runner.start(command.payload);
69
+ this.runner.start(command.payload);
42
70
  return;
43
71
  }
44
72
  case 'stop': {
45
- runner.stop();
73
+ this.runner.stop();
74
+ return;
75
+ }
76
+ case 'approve': {
77
+ void this.runner.approve(command.payload);
78
+ return;
79
+ }
80
+ case 'approveAll': {
81
+ void this.runner.approveAll();
46
82
  return;
47
83
  }
84
+ }
85
+ } else {
86
+ // In update mode, only approve and approveAll commands are allowed
87
+ switch (command.type) {
48
88
  case 'approve': {
49
- void runner.approve(command.payload);
89
+ void this.testsManager.approve(command.payload);
50
90
  return;
51
91
  }
52
92
  case 'approveAll': {
53
- void runner.approveAll();
93
+ void this.testsManager.approveAll();
94
+ return;
95
+ }
96
+ case 'status': {
97
+ // In update mode, respond with static status including tests data
98
+ send(ws, {
99
+ type: 'status',
100
+ payload: {
101
+ isRunning: false,
102
+ tests: this.testsManager.getTestsData(),
103
+ browsers: [],
104
+ isUpdateMode: true,
105
+ },
106
+ });
107
+ return;
108
+ }
109
+ default: {
110
+ // Ignore other commands in update mode
111
+ logger().debug(`Command ${command.type} is not available in update mode`);
54
112
  return;
55
113
  }
56
114
  }
57
- },
58
- };
115
+ }
116
+ }
117
+
118
+ private broadcastUpdate(payload: CreeveyUpdate): void {
119
+ if (!this.wss) return;
120
+
121
+ const message: Response = { type: 'update', payload };
122
+
123
+ broadcast(this.wss, message);
124
+ }
59
125
  }
@@ -0,0 +1,20 @@
1
+ import cluster from 'cluster';
2
+ import { subscribeOnWorker, sendStoriesMessage } from '../../messages.js';
3
+ import { CaptureOptions, isDefined } from '../../../types.js';
4
+
5
+ export function captureHandler({ workerId, options }: { workerId: number; options?: CaptureOptions }): void {
6
+ const worker = Object.values(cluster.workers ?? {})
7
+ .filter(isDefined)
8
+ .find((worker) => worker.process.pid == workerId);
9
+
10
+ // NOTE: Hypothetical case when someone send to us capture req and we don't have a worker with browser session for it
11
+ if (!worker) {
12
+ return;
13
+ }
14
+
15
+ const unsubscribe = subscribeOnWorker(worker, 'stories', (message) => {
16
+ if (message.type != 'capture') return;
17
+ unsubscribe();
18
+ });
19
+ sendStoriesMessage(worker, { type: 'capture', payload: options });
20
+ }
@@ -0,0 +1,4 @@
1
+ export * from './ping-handler.js';
2
+ export * from './stories-handler.js';
3
+ export * from './capture-handler.js';
4
+ export * from './static-handler.js';
@@ -0,0 +1,6 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+
3
+ export function pingHandler(_request: IncomingMessage, response: ServerResponse): void {
4
+ response.setHeader('Content-Type', 'text/plain');
5
+ response.end('pong');
6
+ }
@@ -0,0 +1,18 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+
4
+ export function staticHandler(baseDir: string, pathPrefix?: string) {
5
+ return (requestedPath: string): string | undefined => {
6
+ const relativePath = pathPrefix ? requestedPath.replace(pathPrefix, '') : requestedPath;
7
+ let filePath = path.join(baseDir, relativePath || 'index.html');
8
+
9
+ // If the path points to a directory, append index.html
10
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
11
+ filePath = path.join(filePath, 'index.html');
12
+ }
13
+
14
+ if (!fs.existsSync(filePath)) {
15
+ return undefined;
16
+ }
17
+ };
18
+ }
@@ -0,0 +1,20 @@
1
+ import cluster from 'cluster';
2
+ import { emitStoriesMessage, sendStoriesMessage } from '../../messages.js';
3
+ import { isDefined, StoryInput } from '../../../types.js';
4
+ import { deserializeStory } from '../../../shared/index.js';
5
+
6
+ export function storiesHandler({ stories }: { stories: [string, StoryInput[]][] }): void {
7
+ const deserializedStories = stories.map<[string, StoryInput[]]>(([file, stories]) => [
8
+ file,
9
+ stories.map(deserializeStory),
10
+ ]);
11
+
12
+ emitStoriesMessage({ type: 'update', payload: deserializedStories });
13
+
14
+ Object.values(cluster.workers ?? {})
15
+ .filter(isDefined)
16
+ .filter((worker) => worker.isConnected())
17
+ .forEach((worker) => {
18
+ sendStoriesMessage(worker, { type: 'update', payload: deserializedStories });
19
+ });
20
+ }