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.
- package/CHANGELOG.md +282 -0
- package/dist/client/addon/controller.js +1 -1
- package/dist/client/addon/controller.js.map +1 -1
- package/dist/client/addon/withCreevey.js +1 -18
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.js +13 -4
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.js +10 -0
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/web/CreeveyApp.d.ts +1 -0
- package/dist/client/web/CreeveyApp.js +1 -0
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +1 -0
- package/dist/client/web/CreeveyContext.js +1 -0
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +9 -8
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +13 -3
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +2 -3
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +2 -3
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +1 -0
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
- package/dist/client/web/assets/{index-C47njyZV.js → index-BU4jjKVC.js} +68 -68
- package/dist/client/web/index.html +1 -1
- package/dist/client/web/index.js +8 -3
- package/dist/client/web/index.js.map +1 -1
- package/dist/creevey.d.ts +1 -1
- package/dist/creevey.js +1 -22
- package/dist/creevey.js.map +1 -1
- package/dist/playwright-reporter.d.ts +2 -0
- package/dist/playwright-reporter.js +5 -0
- package/dist/playwright-reporter.js.map +1 -0
- package/dist/playwright.d.ts +1 -1
- package/dist/server/config.js +8 -1
- package/dist/server/config.js.map +1 -1
- package/dist/server/index.js +12 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/master/api.d.ts +11 -6
- package/dist/server/master/api.js +88 -25
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/handlers/capture-handler.d.ts +5 -0
- package/dist/server/master/handlers/capture-handler.js +25 -0
- package/dist/server/master/handlers/capture-handler.js.map +1 -0
- package/dist/server/master/handlers/index.d.ts +4 -0
- package/dist/server/master/handlers/index.js +21 -0
- package/dist/server/master/handlers/index.js.map +1 -0
- package/dist/server/master/handlers/ping-handler.d.ts +2 -0
- package/dist/server/master/handlers/ping-handler.js +8 -0
- package/dist/server/master/handlers/ping-handler.js.map +1 -0
- package/dist/server/master/handlers/static-handler.d.ts +1 -0
- package/dist/server/master/handlers/static-handler.js +22 -0
- package/dist/server/master/handlers/static-handler.js.map +1 -0
- package/dist/server/master/handlers/stories-handler.d.ts +4 -0
- package/dist/server/master/handlers/stories-handler.js +24 -0
- package/dist/server/master/handlers/stories-handler.js.map +1 -0
- package/dist/server/master/master.js +7 -24
- package/dist/server/master/master.js.map +1 -1
- package/dist/server/master/runner.d.ts +4 -6
- package/dist/server/master/runner.js +30 -127
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/master/server.js +191 -89
- package/dist/server/master/server.js.map +1 -1
- package/dist/server/master/start.d.ts +1 -2
- package/dist/server/master/start.js +11 -29
- package/dist/server/master/start.js.map +1 -1
- package/dist/server/master/testsManager.d.ts +81 -0
- package/dist/server/master/testsManager.js +281 -0
- package/dist/server/master/testsManager.js.map +1 -0
- package/dist/server/playwright/docker-file.js +2 -2
- package/dist/server/playwright/docker-file.js.map +1 -1
- package/dist/server/playwright/reporter.d.ts +87 -0
- package/dist/server/playwright/reporter.js +351 -0
- package/dist/server/playwright/reporter.js.map +1 -0
- package/dist/server/selenium/internal.js +20 -2
- package/dist/server/selenium/internal.js.map +1 -1
- package/dist/server/selenium/selenoid.js +4 -0
- package/dist/server/selenium/selenoid.js.map +1 -1
- package/dist/server/shutdown.d.ts +1 -0
- package/dist/server/shutdown.js +23 -0
- package/dist/server/shutdown.js.map +1 -0
- package/dist/server/stories.d.ts +0 -1
- package/dist/server/stories.js +0 -12
- package/dist/server/stories.js.map +1 -1
- package/dist/server/ui-update.d.ts +10 -0
- package/dist/server/ui-update.js +39 -0
- package/dist/server/ui-update.js.map +1 -0
- package/dist/server/utils.d.ts +6 -0
- package/dist/server/utils.js +39 -8
- package/dist/server/utils.js.map +1 -1
- package/dist/server/worker/start.js +1 -1
- package/dist/server/worker/start.js.map +1 -1
- package/dist/types.d.ts +14 -8
- package/dist/types.js.map +1 -1
- package/docs/examples/playwright-reporter-example.ts +202 -0
- package/docs/migration-0.9-to-0.10.md +144 -0
- package/docs/playwright-reporter.md +357 -0
- package/package.json +10 -14
- package/src/client/addon/controller.ts +1 -1
- package/src/client/addon/withCreevey.ts +2 -16
- package/src/client/shared/components/PageHeader/PageHeader.tsx +18 -4
- package/src/client/shared/creeveyClientApi.ts +10 -0
- package/src/client/web/CreeveyApp.tsx +2 -0
- package/src/client/web/CreeveyContext.tsx +2 -0
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +19 -17
- package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +18 -3
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +9 -7
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
- package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +1 -0
- package/src/client/web/index.tsx +8 -3
- package/src/creevey.ts +1 -24
- package/src/playwright-reporter.ts +3 -0
- package/src/server/config.ts +9 -1
- package/src/server/index.ts +13 -6
- package/src/server/master/api.ts +94 -28
- package/src/server/master/handlers/capture-handler.ts +20 -0
- package/src/server/master/handlers/index.ts +4 -0
- package/src/server/master/handlers/ping-handler.ts +6 -0
- package/src/server/master/handlers/static-handler.ts +18 -0
- package/src/server/master/handlers/stories-handler.ts +20 -0
- package/src/server/master/master.ts +10 -27
- package/src/server/master/runner.ts +38 -132
- package/src/server/master/server.ts +210 -98
- package/src/server/master/start.ts +17 -41
- package/src/server/master/testsManager.ts +315 -0
- package/src/server/playwright/docker-file.ts +2 -2
- package/src/server/playwright/reporter.ts +386 -0
- package/src/server/selenium/internal.ts +23 -3
- package/src/server/selenium/selenoid.ts +5 -0
- package/src/server/shutdown.ts +19 -0
- package/src/server/stories.ts +1 -12
- package/src/server/ui-update.ts +46 -0
- package/src/server/utils.ts +40 -9
- package/src/server/worker/start.ts +1 -1
- 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
|
-
|
52
|
-
<
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
60
|
-
|
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="
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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}>
|
package/src/client/web/index.tsx
CHANGED
@@ -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={{
|
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'],
|
package/src/server/config.ts
CHANGED
@@ -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
|
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
|
|
package/src/server/index.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import cluster from 'cluster';
|
2
2
|
import path from 'path';
|
3
|
-
import
|
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
|
-
|
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}`);
|
package/src/server/master/api.ts
CHANGED
@@ -1,14 +1,10 @@
|
|
1
|
-
import WebSocket from 'ws';
|
2
|
-
import
|
3
|
-
import {
|
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
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
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
|
89
|
+
void this.testsManager.approve(command.payload);
|
50
90
|
return;
|
51
91
|
}
|
52
92
|
case 'approveAll': {
|
53
|
-
void
|
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,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
|
+
}
|