creevey 0.10.0-beta.42 → 0.10.0-beta.44

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 (141) 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/decorator.d.ts +1 -1
  5. package/dist/client/addon/makeDecorator.d.ts +9 -0
  6. package/dist/client/addon/makeDecorator.js +48 -0
  7. package/dist/client/addon/makeDecorator.js.map +1 -0
  8. package/dist/client/addon/preview.d.ts +1 -1
  9. package/dist/client/addon/withCreevey.d.ts +2 -1
  10. package/dist/client/addon/withCreevey.js +3 -20
  11. package/dist/client/addon/withCreevey.js.map +1 -1
  12. package/dist/client/shared/components/PageHeader/PageHeader.js +13 -4
  13. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  14. package/dist/client/shared/creeveyClientApi.js +10 -0
  15. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  16. package/dist/client/web/CreeveyApp.d.ts +1 -0
  17. package/dist/client/web/CreeveyApp.js +1 -0
  18. package/dist/client/web/CreeveyApp.js.map +1 -1
  19. package/dist/client/web/CreeveyContext.d.ts +1 -0
  20. package/dist/client/web/CreeveyContext.js +1 -0
  21. package/dist/client/web/CreeveyContext.js.map +1 -1
  22. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +9 -8
  23. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  24. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +13 -3
  25. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  26. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +2 -3
  27. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  28. package/dist/client/web/CreeveyView/SideBar/TestLink.js +2 -3
  29. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  30. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +1 -0
  31. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
  32. package/dist/client/web/assets/{index-C47njyZV.js → index-BU4jjKVC.js} +68 -68
  33. package/dist/client/web/index.html +1 -1
  34. package/dist/client/web/index.js +8 -3
  35. package/dist/client/web/index.js.map +1 -1
  36. package/dist/creevey.d.ts +1 -1
  37. package/dist/creevey.js +1 -22
  38. package/dist/creevey.js.map +1 -1
  39. package/dist/playwright-reporter.d.ts +2 -0
  40. package/dist/playwright-reporter.js +5 -0
  41. package/dist/playwright-reporter.js.map +1 -0
  42. package/dist/playwright.d.ts +1 -1
  43. package/dist/server/config.js +8 -1
  44. package/dist/server/config.js.map +1 -1
  45. package/dist/server/index.js +10 -3
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/server/master/api.d.ts +15 -5
  48. package/dist/server/master/api.js +89 -27
  49. package/dist/server/master/api.js.map +1 -1
  50. package/dist/server/master/handlers/capture-handler.d.ts +2 -0
  51. package/dist/server/master/handlers/capture-handler.js +35 -0
  52. package/dist/server/master/handlers/capture-handler.js.map +1 -0
  53. package/dist/server/master/handlers/index.d.ts +4 -0
  54. package/dist/server/master/handlers/index.js +21 -0
  55. package/dist/server/master/handlers/index.js.map +1 -0
  56. package/dist/server/master/handlers/ping-handler.d.ts +2 -0
  57. package/dist/server/master/handlers/ping-handler.js +7 -0
  58. package/dist/server/master/handlers/ping-handler.js.map +1 -0
  59. package/dist/server/master/handlers/static-handler.d.ts +2 -0
  60. package/dist/server/master/handlers/static-handler.js +32 -0
  61. package/dist/server/master/handlers/static-handler.js.map +1 -0
  62. package/dist/server/master/handlers/stories-handler.d.ts +2 -0
  63. package/dist/server/master/handlers/stories-handler.js +38 -0
  64. package/dist/server/master/handlers/stories-handler.js.map +1 -0
  65. package/dist/server/master/master.js +7 -24
  66. package/dist/server/master/master.js.map +1 -1
  67. package/dist/server/master/runner.d.ts +4 -6
  68. package/dist/server/master/runner.js +30 -127
  69. package/dist/server/master/runner.js.map +1 -1
  70. package/dist/server/master/server.js +77 -87
  71. package/dist/server/master/server.js.map +1 -1
  72. package/dist/server/master/start.d.ts +1 -2
  73. package/dist/server/master/start.js +11 -29
  74. package/dist/server/master/start.js.map +1 -1
  75. package/dist/server/master/testsManager.d.ts +81 -0
  76. package/dist/server/master/testsManager.js +281 -0
  77. package/dist/server/master/testsManager.js.map +1 -0
  78. package/dist/server/playwright/reporter.d.ts +87 -0
  79. package/dist/server/playwright/reporter.js +351 -0
  80. package/dist/server/playwright/reporter.js.map +1 -0
  81. package/dist/server/selenium/internal.js +20 -2
  82. package/dist/server/selenium/internal.js.map +1 -1
  83. package/dist/server/selenium/selenoid.js +4 -0
  84. package/dist/server/selenium/selenoid.js.map +1 -1
  85. package/dist/server/shutdown.d.ts +1 -0
  86. package/dist/server/shutdown.js +23 -0
  87. package/dist/server/shutdown.js.map +1 -0
  88. package/dist/server/stories.d.ts +0 -1
  89. package/dist/server/stories.js +0 -12
  90. package/dist/server/stories.js.map +1 -1
  91. package/dist/server/ui-update.d.ts +10 -0
  92. package/dist/server/ui-update.js +39 -0
  93. package/dist/server/ui-update.js.map +1 -0
  94. package/dist/server/utils.d.ts +6 -0
  95. package/dist/server/utils.js +39 -8
  96. package/dist/server/utils.js.map +1 -1
  97. package/dist/server/worker/start.js +1 -1
  98. package/dist/server/worker/start.js.map +1 -1
  99. package/dist/types.d.ts +14 -8
  100. package/dist/types.js.map +1 -1
  101. package/docs/examples/playwright-reporter-example.ts +202 -0
  102. package/docs/migration-0.9-to-0.10.md +144 -0
  103. package/docs/playwright-reporter.md +357 -0
  104. package/package.json +9 -13
  105. package/src/client/addon/controller.ts +1 -1
  106. package/src/client/addon/makeDecorator.ts +69 -0
  107. package/src/client/addon/withCreevey.ts +4 -17
  108. package/src/client/shared/components/PageHeader/PageHeader.tsx +18 -4
  109. package/src/client/shared/creeveyClientApi.ts +10 -0
  110. package/src/client/web/CreeveyApp.tsx +2 -0
  111. package/src/client/web/CreeveyContext.tsx +2 -0
  112. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +19 -17
  113. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +18 -3
  114. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +9 -7
  115. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
  116. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +1 -0
  117. package/src/client/web/index.tsx +8 -3
  118. package/src/creevey.ts +1 -24
  119. package/src/playwright-reporter.ts +3 -0
  120. package/src/server/config.ts +9 -1
  121. package/src/server/index.ts +11 -4
  122. package/src/server/master/api.ts +95 -26
  123. package/src/server/master/handlers/capture-handler.ts +39 -0
  124. package/src/server/master/handlers/index.ts +4 -0
  125. package/src/server/master/handlers/ping-handler.ts +5 -0
  126. package/src/server/master/handlers/static-handler.ts +29 -0
  127. package/src/server/master/handlers/stories-handler.ts +48 -0
  128. package/src/server/master/master.ts +10 -27
  129. package/src/server/master/runner.ts +38 -132
  130. package/src/server/master/server.ts +93 -97
  131. package/src/server/master/start.ts +17 -41
  132. package/src/server/master/testsManager.ts +315 -0
  133. package/src/server/playwright/reporter.ts +386 -0
  134. package/src/server/selenium/internal.ts +23 -3
  135. package/src/server/selenium/selenoid.ts +5 -0
  136. package/src/server/shutdown.ts +19 -0
  137. package/src/server/stories.ts +1 -12
  138. package/src/server/ui-update.ts +46 -0
  139. package/src/server/utils.ts +40 -9
  140. package/src/server/worker/start.ts +1 -1
  141. package/src/types.ts +14 -8
@@ -1,5 +1,5 @@
1
- import type { Renderer, StoryContextForEnhancers } from '@storybook/types';
2
- import { makeDecorator, PreviewWeb, StoryStore } from '@storybook/preview-api';
1
+ import type { Renderer } from '@storybook/types';
2
+ import type { PreviewWeb, StoryStore } from '@storybook/preview-api';
3
3
  import { Channel } from '@storybook/channels';
4
4
  import {
5
5
  CaptureOptions,
@@ -13,6 +13,7 @@ import {
13
13
  import { serializeRawStories } from '../../shared/index.js';
14
14
  import { getConnectionUrl } from '../shared/helpers.js';
15
15
  import isEqual from 'lodash/isEqual.js';
16
+ import { makeDecorator } from './makeDecorator.js';
16
17
 
17
18
  declare global {
18
19
  interface Window {
@@ -120,7 +121,6 @@ function initCreeveyState(): void {
120
121
 
121
122
  if (prevState.creeveyHost) window.__CREEVEY_SERVER_HOST__ = prevState.creeveyHost;
122
123
  if (prevState.creeveyPort) window.__CREEVEY_SERVER_PORT__ = prevState.creeveyPort;
123
- if (prevState.setStoriesCounter) setStoriesCounter = prevState.setStoriesCounter;
124
124
  if (prevState.isTestBrowser) isTestBrowser = prevState.isTestBrowser;
125
125
 
126
126
  window.addEventListener('beforeunload', () => {
@@ -129,7 +129,6 @@ function initCreeveyState(): void {
129
129
  JSON.stringify({
130
130
  creeveyHost: window.__CREEVEY_SERVER_HOST__,
131
131
  creeveyPort: window.__CREEVEY_SERVER_PORT__,
132
- setStoriesCounter,
133
132
  isTestBrowser,
134
133
  } as CreeveyTestsState),
135
134
  );
@@ -140,7 +139,6 @@ let isTestBrowser = false;
140
139
  let captureResolver: () => void;
141
140
  let waitForCreevey: Promise<void>;
142
141
  let creeveyReady: () => void;
143
- let setStoriesCounter = 0;
144
142
  let globals = {};
145
143
 
146
144
  export function withCreevey(): ReturnType<typeof makeDecorator> {
@@ -159,20 +157,9 @@ export function withCreevey(): ReturnType<typeof makeDecorator> {
159
157
  document.head.appendChild(style);
160
158
  }
161
159
 
160
+ // TODO Find a way to send stories updates to the server
162
161
  async function getStories(): Promise<StoriesRaw | undefined> {
163
162
  const stories = serializeRawStories(await window.__STORYBOOK_PREVIEW__.extract());
164
- const storiesByFiles = new Map<string, StoryContextForEnhancers[]>();
165
- Object.values(stories).forEach((story) => {
166
- const fileName = story.parameters.fileName as string;
167
- const storiesFromFile = storiesByFiles.get(fileName);
168
- if (storiesFromFile) storiesFromFile.push(story);
169
- else storiesByFiles.set(fileName, [story]);
170
- });
171
- void fetch(`http://${getConnectionUrl()}/stories`, {
172
- method: 'POST',
173
- headers: { 'Content-Type': 'application/json' },
174
- body: JSON.stringify({ setStoriesCounter, stories: [...storiesByFiles.entries()] }),
175
- });
176
163
  return stories;
177
164
  }
178
165
 
@@ -21,14 +21,14 @@ interface PageHeaderProps {
21
21
  }
22
22
 
23
23
  const Container = styled.div({
24
- margin: '24px 44px 0',
24
+ marginTop: '24px',
25
25
  });
26
26
 
27
27
  const ErrorContainer = withTheme(
28
28
  styled.div<{ theme: Theme }>(({ theme }) => ({
29
29
  marginTop: '8px',
30
30
  padding: '8px',
31
- background: theme.background.negative,
31
+ background: `${theme.background.negative}20`,
32
32
  color: theme.color.negative,
33
33
  borderRadius: '2px',
34
34
  display: 'flex',
@@ -48,8 +48,19 @@ const ErrorContainer = withTheme(
48
48
  })),
49
49
  );
50
50
 
51
+ const UpdateModeBanner = withTheme(
52
+ styled.div(({ theme }) => ({
53
+ padding: '8px 32px',
54
+ backgroundColor: `${theme.color.positive}20`,
55
+ color: theme.color.positive,
56
+ fontSize: '12px',
57
+ textAlign: 'center',
58
+ fontWeight: 'bold',
59
+ })),
60
+ );
61
+
51
62
  const H1 = styled.h1({
52
- margin: 0,
63
+ marginLeft: '44px',
53
64
  marginBottom: '8px',
54
65
  });
55
66
 
@@ -77,7 +88,7 @@ export function PageHeader({
77
88
  onImageChange,
78
89
  onViewModeChange,
79
90
  }: PageHeaderProps): JSX.Element | null {
80
- const { isReport } = useContext(CreeveyContext);
91
+ const { isReport, isUpdateMode } = useContext(CreeveyContext);
81
92
  const imageEntires = Object.entries(images) as [string, Images][];
82
93
 
83
94
  const handleViewModeChange = (mode: string): void => {
@@ -120,6 +131,9 @@ export function PageHeader({
120
131
  ))}
121
132
  </ImagesEntriesContainer>
122
133
  ) : null}
134
+ {isUpdateMode && (
135
+ <UpdateModeBanner>Update Mode: Review and approve screenshots from previous test runs</UpdateModeBanner>
136
+ )}
123
137
  {showViewModes && (
124
138
  <Tabs selected={viewMode} actions={{ onSelect: handleViewModeChange }}>
125
139
  {viewModes.map((x) => (
@@ -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
 
@@ -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) {
@@ -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,59 +1,128 @@
1
- import WebSocket from 'ws';
2
1
  import Runner from './runner.js';
3
2
  import { Request, Response, CreeveyUpdate } from '../../types.js';
4
3
  import { logger } from '../logger.js';
4
+ import { TestsManager } from './testsManager.js';
5
+ import HyperExpress from 'hyper-express';
5
6
 
6
- export interface CreeveyApi {
7
- subscribe: (wss: WebSocket.Server) => void;
8
- handleMessage: (ws: WebSocket, message: WebSocket.Data) => void;
7
+ interface CustomWSServer {
8
+ clients: Set<HyperExpress.Websocket>;
9
+ publish: (message: string) => void;
9
10
  }
10
11
 
11
- function broadcast(wss: WebSocket.Server, message: Response): void {
12
- wss.clients.forEach((ws) => {
13
- if (ws.readyState === WebSocket.OPEN) {
14
- ws.send(JSON.stringify(message));
15
- }
16
- });
12
+ // Helper function for HyperExpress WebSocket broadcasting
13
+ function broadcastHyperExpress(wss: CustomWSServer, message: Response): void {
14
+ const serializedMessage = JSON.stringify(message);
15
+ wss.publish(serializedMessage);
17
16
  }
18
17
 
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 });
18
+ // The class-based implementation of CreeveyApi
19
+ export class CreeveyApi {
20
+ private runner: Runner | null = null;
21
+ private testsManager: TestsManager;
22
+ private wss: CustomWSServer | null = null;
23
+
24
+ constructor(testsManager: TestsManager, runner?: Runner) {
25
+ this.testsManager = testsManager;
26
+
27
+ // Use the provided runner in normal mode, or keep as null in update mode
28
+ if (runner) {
29
+ this.runner = runner;
30
+ }
31
+ }
32
+
33
+ subscribe(wss: CustomWSServer): void {
34
+ this.wss = wss;
35
+
36
+ // If we have a runner, subscribe to its updates
37
+ if (this.runner) {
38
+ this.runner.on('update', (payload: CreeveyUpdate) => {
39
+ this.broadcastUpdate(payload);
40
+ });
41
+ } else {
42
+ // Subscribe to TestsManager updates
43
+ this.testsManager.on('update', (update: CreeveyUpdate) => {
44
+ this.broadcastUpdate(update);
24
45
  });
25
- },
46
+ }
47
+ }
26
48
 
27
- handleMessage(ws: WebSocket, message: WebSocket.Data) {
28
- if (typeof message != 'string') {
49
+ handleMessage(ws: HyperExpress.Websocket, message: string | Buffer): void {
50
+ if (typeof message != 'string') {
51
+ if (Buffer.isBuffer(message)) {
52
+ message = message.toString('utf-8');
53
+ } else {
29
54
  logger().info('unhandled message', message);
30
55
  return;
31
56
  }
57
+ }
32
58
 
33
- const command = JSON.parse(message) as Request;
59
+ const command = JSON.parse(message) as Request;
60
+ const sendResponse = (response: Response) => {
61
+ ws.send(JSON.stringify(response));
62
+ };
34
63
 
64
+ if (this.runner) {
65
+ // Normal mode handling with runner
35
66
  switch (command.type) {
36
67
  case 'status': {
37
- ws.send(JSON.stringify({ type: 'status', payload: runner.status }));
68
+ sendResponse({ type: 'status', payload: this.runner.status });
38
69
  return;
39
70
  }
40
71
  case 'start': {
41
- runner.start(command.payload);
72
+ this.runner.start(command.payload);
42
73
  return;
43
74
  }
44
75
  case 'stop': {
45
- runner.stop();
76
+ this.runner.stop();
77
+ return;
78
+ }
79
+ case 'approve': {
80
+ void this.runner.approve(command.payload);
81
+ return;
82
+ }
83
+ case 'approveAll': {
84
+ void this.runner.approveAll();
46
85
  return;
47
86
  }
87
+ }
88
+ } else {
89
+ // In update mode, only approve and approveAll commands are allowed
90
+ switch (command.type) {
48
91
  case 'approve': {
49
- void runner.approve(command.payload);
92
+ void this.testsManager.approve(command.payload);
50
93
  return;
51
94
  }
52
95
  case 'approveAll': {
53
- void runner.approveAll();
96
+ void this.testsManager.approveAll();
97
+ return;
98
+ }
99
+ case 'status': {
100
+ // In update mode, respond with static status including tests data
101
+ sendResponse({
102
+ type: 'status',
103
+ payload: {
104
+ isRunning: false,
105
+ tests: this.testsManager.getTestsData(),
106
+ browsers: [],
107
+ isUpdateMode: true,
108
+ },
109
+ });
110
+ return;
111
+ }
112
+ default: {
113
+ // Ignore other commands in update mode
114
+ logger().debug(`Command ${command.type} is not available in update mode`);
54
115
  return;
55
116
  }
56
117
  }
57
- },
58
- };
118
+ }
119
+ }
120
+
121
+ private broadcastUpdate(payload: CreeveyUpdate): void {
122
+ if (!this.wss) return;
123
+
124
+ const message: Response = { type: 'update', payload };
125
+
126
+ broadcastHyperExpress(this.wss, message);
127
+ }
59
128
  }