creevey 0.9.0 → 0.9.1
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/dist/client/addon/components/Panel.d.ts +1 -1
- package/dist/client/addon/components/Panel.js +5 -2
- package/dist/client/addon/components/Panel.js.map +1 -1
- package/dist/client/addon/withCreevey.js +1 -1
- package/dist/client/addon/withCreevey.js.map +1 -1
- package/dist/client/shared/components/PageFooter/PageFooter.d.ts +1 -3
- package/dist/client/shared/components/PageFooter/PageFooter.js +3 -8
- package/dist/client/shared/components/PageFooter/PageFooter.js.map +1 -1
- package/dist/client/shared/components/PageFooter/Paging.d.ts +1 -1
- package/dist/client/shared/components/PageFooter/Paging.js +4 -21
- package/dist/client/shared/components/PageFooter/Paging.js.map +1 -1
- package/dist/client/shared/components/PageHeader/PageHeader.d.ts +2 -2
- package/dist/client/shared/components/PageHeader/PageHeader.js +17 -10
- package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
- package/dist/client/shared/components/ResultsPage.d.ts +8 -6
- package/dist/client/shared/components/ResultsPage.js +5 -13
- package/dist/client/shared/components/ResultsPage.js.map +1 -1
- package/dist/client/shared/creeveyClientApi.d.ts +1 -0
- package/dist/client/shared/creeveyClientApi.js +3 -0
- package/dist/client/shared/creeveyClientApi.js.map +1 -1
- package/dist/client/shared/helpers.d.ts +2 -1
- package/dist/client/shared/helpers.js +23 -8
- package/dist/client/shared/helpers.js.map +1 -1
- package/dist/client/web/CreeveyApp.js +45 -8
- package/dist/client/web/CreeveyApp.js.map +1 -1
- package/dist/client/web/CreeveyContext.d.ts +3 -0
- package/dist/client/web/CreeveyContext.js +28 -4
- package/dist/client/web/CreeveyContext.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/Checkbox.d.ts +1 -1
- package/dist/client/web/CreeveyView/SideBar/Checkbox.js +5 -5
- package/dist/client/web/CreeveyView/SideBar/Checkbox.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBar.d.ts +2 -2
- package/dist/client/web/CreeveyView/SideBar/SideBar.js +23 -13
- package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.d.ts +1 -0
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +32 -0
- package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -0
- package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +1 -1
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +8 -3
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +38 -23
- package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestLink.js +6 -5
- package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js +13 -6
- package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js.map +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +6 -4
- package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
- package/dist/client/web/main.js +8 -8
- package/dist/server/master/api.js +4 -0
- package/dist/server/master/api.js.map +1 -1
- package/dist/server/master/runner.d.ts +2 -0
- package/dist/server/master/runner.js +59 -9
- package/dist/server/master/runner.js.map +1 -1
- package/dist/server/selenium/browser.js +9 -6
- package/dist/server/selenium/browser.js.map +1 -1
- package/dist/server/worker/worker.js +1 -0
- package/dist/server/worker/worker.js.map +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.js.map +1 -1
- package/package.json +2 -1
- package/src/client/addon/components/Panel.tsx +7 -3
- package/src/client/addon/withCreevey.ts +1 -1
- package/src/client/shared/components/PageFooter/PageFooter.tsx +2 -20
- package/src/client/shared/components/PageFooter/Paging.tsx +22 -37
- package/src/client/shared/components/PageHeader/PageHeader.tsx +20 -14
- package/src/client/shared/components/ResultsPage.tsx +18 -31
- package/src/client/shared/creeveyClientApi.ts +4 -0
- package/src/client/shared/helpers.ts +22 -8
- package/src/client/web/CreeveyApp.tsx +66 -13
- package/src/client/web/CreeveyContext.tsx +7 -1
- package/src/client/web/CreeveyView/SideBar/Checkbox.tsx +5 -5
- package/src/client/web/CreeveyView/SideBar/SideBar.tsx +29 -18
- package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +38 -0
- package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +1 -1
- package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +50 -31
- package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
- package/src/client/web/CreeveyView/SideBar/TestStatusIcon.tsx +12 -6
- package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +7 -3
- package/src/server/master/api.ts +4 -0
- package/src/server/master/runner.ts +65 -9
- package/src/server/selenium/browser.ts +13 -10
- package/src/server/worker/worker.ts +1 -0
- package/src/types.ts +4 -3
@@ -1,7 +1,5 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import {
|
3
|
-
import { styled } from '@storybook/theming';
|
4
|
-
import { noop } from '../../../../types.js';
|
2
|
+
import { TabButton } from '@storybook/components';
|
5
3
|
|
6
4
|
export interface PagingProps {
|
7
5
|
activePage: number;
|
@@ -9,56 +7,43 @@ export interface PagingProps {
|
|
9
7
|
pagesCount: number;
|
10
8
|
}
|
11
9
|
|
12
|
-
export type ItemType = number | '.'
|
13
|
-
|
14
|
-
const StyledButton = styled(Button)({
|
15
|
-
transform: 'none',
|
16
|
-
marginLeft: '8px',
|
17
|
-
});
|
10
|
+
export type ItemType = number | '.';
|
18
11
|
|
19
12
|
export function Paging(props: PagingProps): JSX.Element {
|
20
13
|
const renderItem = (item: ItemType, index: number): JSX.Element => {
|
21
14
|
switch (item) {
|
22
15
|
case '.': {
|
23
16
|
return (
|
24
|
-
<
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
<StyledButton
|
33
|
-
outline
|
34
|
-
disabled={disabled}
|
35
|
-
onClick={
|
36
|
-
disabled
|
37
|
-
? noop
|
38
|
-
: () => {
|
39
|
-
goToPage(props.activePage + 1);
|
40
|
-
}
|
41
|
-
}
|
42
|
-
key="forward"
|
17
|
+
<TabButton
|
18
|
+
disabled
|
19
|
+
key={`dots${index < 5 ? 'Left' : 'Right'}`}
|
20
|
+
autoFocus={false}
|
21
|
+
content={''}
|
22
|
+
nonce={''}
|
23
|
+
rel={''}
|
24
|
+
rev={''}
|
43
25
|
>
|
44
|
-
|
45
|
-
|
46
|
-
</span>
|
47
|
-
</StyledButton>
|
26
|
+
{'...'}
|
27
|
+
</TabButton>
|
48
28
|
);
|
49
29
|
}
|
30
|
+
|
50
31
|
default: {
|
51
32
|
return (
|
52
|
-
<
|
53
|
-
|
54
|
-
|
33
|
+
<TabButton
|
34
|
+
rel={item}
|
35
|
+
rev={item}
|
36
|
+
autoFocus={false}
|
37
|
+
nonce={item}
|
38
|
+
content={item}
|
55
39
|
key={item}
|
56
40
|
onClick={() => {
|
57
41
|
goToPage(item);
|
58
42
|
}}
|
43
|
+
active={props.activePage === item}
|
59
44
|
>
|
60
45
|
{item}
|
61
|
-
</
|
46
|
+
</TabButton>
|
62
47
|
);
|
63
48
|
}
|
64
49
|
}
|
@@ -101,5 +86,5 @@ function getItems(active: number, total: number): ItemType[] {
|
|
101
86
|
result.push(total);
|
102
87
|
}
|
103
88
|
|
104
|
-
return result
|
89
|
+
return result;
|
105
90
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, {
|
1
|
+
import React, { useEffect } from 'react';
|
2
2
|
import { Icons, Tabs } from '@storybook/components';
|
3
3
|
import { styled, withTheme, Theme } from '@storybook/theming';
|
4
4
|
import { ImagesViewMode, Images } from '../../../../types.js';
|
@@ -8,10 +8,10 @@ import { viewModes } from '../../viewMode.js';
|
|
8
8
|
|
9
9
|
interface PageHeaderProps {
|
10
10
|
title: string[];
|
11
|
+
imageName: string;
|
11
12
|
images?: Partial<Record<string, Images>>;
|
12
13
|
errorMessage?: string | null;
|
13
14
|
showViewModes: boolean;
|
14
|
-
showTitle?: boolean;
|
15
15
|
viewMode: ImagesViewMode;
|
16
16
|
imagesWithError?: string[];
|
17
17
|
onImageChange: (name: string) => void;
|
@@ -63,34 +63,40 @@ const ImagesEntriesContainer = styled.div({
|
|
63
63
|
margin: '16px 0 8px',
|
64
64
|
});
|
65
65
|
|
66
|
+
// TODO Move images to sidebar
|
66
67
|
export function PageHeader({
|
67
68
|
title,
|
69
|
+
imageName,
|
68
70
|
images = {},
|
69
71
|
errorMessage,
|
70
72
|
showViewModes,
|
71
|
-
showTitle,
|
72
73
|
viewMode,
|
73
74
|
imagesWithError = [],
|
74
75
|
onImageChange,
|
75
76
|
onViewModeChange,
|
76
77
|
}: PageHeaderProps): JSX.Element | null {
|
77
78
|
const imageEntires = Object.entries(images) as [string, Images][];
|
78
|
-
const [imageName, setImageName] = useState(imageEntires.at(0)?.[0] ?? '');
|
79
79
|
|
80
|
-
const handleImageChange = (name: string): void => {
|
81
|
-
setImageName(name);
|
82
|
-
onImageChange(name);
|
83
|
-
};
|
84
80
|
const handleViewModeChange = (mode: string): void => {
|
85
81
|
onViewModeChange(mode as ImagesViewMode);
|
86
82
|
};
|
83
|
+
|
84
|
+
useEffect(() => {
|
85
|
+
if (imageName === '') {
|
86
|
+
if (imagesWithError.length > 0) {
|
87
|
+
onImageChange(imagesWithError[0]);
|
88
|
+
return;
|
89
|
+
}
|
90
|
+
const firstImage = Object.keys(images).at(0);
|
91
|
+
if (firstImage) onImageChange(firstImage);
|
92
|
+
}
|
93
|
+
}, [imageName, images, imagesWithError, onImageChange]);
|
94
|
+
|
87
95
|
const error = errorMessage || imagesWithError.includes(imageName) ? (images[imageName]?.error ?? errorMessage) : null;
|
88
96
|
|
89
|
-
return
|
97
|
+
return (
|
90
98
|
<Container>
|
91
|
-
{
|
92
|
-
<H1>{title.flatMap((token) => [token, <HeaderDivider key={token}>/</HeaderDivider>]).slice(0, -1)}</H1>
|
93
|
-
)}
|
99
|
+
<H1>{title.flatMap((token) => [token, <HeaderDivider key={token}>/</HeaderDivider>]).slice(0, -1)}</H1>
|
94
100
|
{error && (
|
95
101
|
<ErrorContainer>
|
96
102
|
<Icons icon="closeAlt" />
|
@@ -105,7 +111,7 @@ export function PageHeader({
|
|
105
111
|
imageName={name}
|
106
112
|
url={`${getImageUrl(title, name)}/${image.actual}`}
|
107
113
|
isActive={name === imageName}
|
108
|
-
onClick={
|
114
|
+
onClick={onImageChange}
|
109
115
|
error={imagesWithError.includes(name)}
|
110
116
|
/>
|
111
117
|
))}
|
@@ -119,5 +125,5 @@ export function PageHeader({
|
|
119
125
|
</Tabs>
|
120
126
|
)}
|
121
127
|
</Container>
|
122
|
-
)
|
128
|
+
);
|
123
129
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { useState
|
1
|
+
import React, { useState } from 'react';
|
2
2
|
import { Placeholder, ScrollArea } from '@storybook/components';
|
3
3
|
import { styled, withTheme, Theme } from '@storybook/theming';
|
4
4
|
import { ImagesView } from './ImagesView/ImagesView.js';
|
@@ -8,15 +8,17 @@ import { getImageUrl } from '../helpers.js';
|
|
8
8
|
import { getViewMode, VIEW_MODE_KEY } from '../viewMode.js';
|
9
9
|
import { ImagesViewMode, TestResult } from '../../../types.js';
|
10
10
|
|
11
|
-
interface
|
12
|
-
id: string;
|
11
|
+
interface ResultsPageProps {
|
13
12
|
path: string[];
|
14
13
|
results?: TestResult[];
|
15
|
-
approved?: Partial<Record<string, number
|
14
|
+
approved?: Partial<Record<string, number>> | null;
|
16
15
|
showTitle?: boolean;
|
17
|
-
onImageApprove: (id: string, retry: number, image: string) => void;
|
18
16
|
theme: Theme;
|
19
17
|
height?: string;
|
18
|
+
retry: number;
|
19
|
+
imageName: string;
|
20
|
+
onImageChange: (image: string) => void;
|
21
|
+
onRetryChange: (retry: number) => void;
|
20
22
|
}
|
21
23
|
|
22
24
|
const Wrapper = styled.div({
|
@@ -52,38 +54,29 @@ const Container = styled.div<{ height?: string }>(({ height = '100vh' }) => ({
|
|
52
54
|
}));
|
53
55
|
|
54
56
|
export function ResultsPageInternal({
|
55
|
-
id,
|
56
57
|
path,
|
57
58
|
results = [],
|
58
|
-
approved
|
59
|
+
approved,
|
59
60
|
theme,
|
60
|
-
onImageApprove,
|
61
|
-
showTitle = false,
|
62
61
|
height,
|
63
|
-
|
64
|
-
|
62
|
+
retry,
|
63
|
+
imageName,
|
64
|
+
onImageChange,
|
65
|
+
onRetryChange,
|
66
|
+
}: ResultsPageProps): JSX.Element {
|
65
67
|
const result = results[retry - 1] ?? {};
|
66
|
-
const [imageName, setImageName] = useState(Object.keys(result.images ?? {})[0] ?? '');
|
67
68
|
const [viewMode, setViewMode] = useState<ImagesViewMode>(getViewMode());
|
68
|
-
|
69
|
-
useEffect(() => {
|
70
|
-
setRetry(results.length);
|
71
|
-
}, [results.length]);
|
72
|
-
|
73
69
|
const url = getImageUrl(path, imageName);
|
74
70
|
const image = result.images?.[imageName];
|
75
|
-
const canApprove = Boolean(image && approved[imageName] != retry - 1 && result.status != 'success');
|
71
|
+
const canApprove = Boolean(image && approved?.[imageName] != retry - 1 && result.status != 'success');
|
76
72
|
const hasDiffAndExpect = canApprove && Boolean(image?.diff && image.expect);
|
77
73
|
const imagesWithError = result.images
|
78
74
|
? Object.keys(result.images).filter(
|
79
75
|
(imageName) =>
|
80
|
-
result.status != 'success' && approved[imageName] != retry - 1 && result.images?.[imageName]?.error != null,
|
76
|
+
result.status != 'success' && approved?.[imageName] != retry - 1 && result.images?.[imageName]?.error != null,
|
81
77
|
)
|
82
78
|
: [];
|
83
79
|
|
84
|
-
const handleApprove = (): void => {
|
85
|
-
onImageApprove(id, retry - 1, imageName);
|
86
|
-
};
|
87
80
|
const handleChangeViewMode = (mode: ImagesViewMode): void => {
|
88
81
|
localStorage.setItem(VIEW_MODE_KEY, mode);
|
89
82
|
setViewMode(mode);
|
@@ -94,13 +87,13 @@ export function ResultsPageInternal({
|
|
94
87
|
<HeaderContainer>
|
95
88
|
<PageHeader
|
96
89
|
title={path}
|
90
|
+
imageName={imageName}
|
97
91
|
images={result.images}
|
98
92
|
errorMessage={result.error}
|
99
93
|
showViewModes={hasDiffAndExpect}
|
100
94
|
viewMode={viewMode}
|
101
95
|
onViewModeChange={handleChangeViewMode}
|
102
|
-
onImageChange={
|
103
|
-
showTitle={showTitle}
|
96
|
+
onImageChange={onImageChange}
|
104
97
|
imagesWithError={imagesWithError}
|
105
98
|
/>
|
106
99
|
</HeaderContainer>
|
@@ -119,13 +112,7 @@ export function ResultsPageInternal({
|
|
119
112
|
</BodyContainer>
|
120
113
|
{results.length ? (
|
121
114
|
<FooterContainer>
|
122
|
-
<PageFooter
|
123
|
-
canApprove={canApprove}
|
124
|
-
retry={retry}
|
125
|
-
retriesCount={results.length}
|
126
|
-
onRetryChange={setRetry}
|
127
|
-
onApprove={handleApprove}
|
128
|
-
/>
|
115
|
+
<PageFooter retry={retry} retriesCount={results.length} onRetryChange={onRetryChange} />
|
129
116
|
</FooterContainer>
|
130
117
|
) : null}
|
131
118
|
</Container>
|
@@ -5,6 +5,7 @@ export interface CreeveyClientApi {
|
|
5
5
|
start: (ids: string[]) => void;
|
6
6
|
stop: () => void;
|
7
7
|
approve: (id: string, retry: number, image: string) => void;
|
8
|
+
approveAll: () => void;
|
8
9
|
onUpdate: (fn: (update: CreeveyUpdate) => void) => () => void;
|
9
10
|
readonly status: Promise<CreeveyStatus>;
|
10
11
|
}
|
@@ -32,6 +33,9 @@ export async function initCreeveyClientApi(): Promise<CreeveyClientApi> {
|
|
32
33
|
approve(id: string, retry: number, image: string) {
|
33
34
|
send({ type: 'approve', payload: { id, retry, image } });
|
34
35
|
},
|
36
|
+
approveAll() {
|
37
|
+
send({ type: 'approveAll' });
|
38
|
+
},
|
35
39
|
onUpdate(fn: (update: CreeveyUpdate) => void) {
|
36
40
|
updateListeners.add(fn);
|
37
41
|
return () => updateListeners.delete(fn);
|
@@ -12,13 +12,14 @@ export interface CreeveyTestsStatus {
|
|
12
12
|
successCount: number;
|
13
13
|
failedCount: number;
|
14
14
|
pendingCount: number;
|
15
|
-
|
15
|
+
approvedCount: number;
|
16
16
|
}
|
17
17
|
|
18
18
|
const statusUpdatesMap = new Map<TestStatus | undefined, RegExp>([
|
19
|
-
[undefined, /(unknown|success|failed|pending|running)/],
|
20
|
-
['unknown', /(success|failed|pending|running)/],
|
21
|
-
['success', /(failed|pending|running)/],
|
19
|
+
[undefined, /(unknown|success|approved|failed|pending|running)/],
|
20
|
+
['unknown', /(success|approved|failed|pending|running)/],
|
21
|
+
['success', /(approved|failed|pending|running)/],
|
22
|
+
['approved', /(failed|pending|running)/],
|
22
23
|
['failed', /(pending|running)/],
|
23
24
|
['pending', /running/],
|
24
25
|
]);
|
@@ -153,6 +154,16 @@ export function getCheckedTests(suite: CreeveySuite): CreeveyTest[] {
|
|
153
154
|
});
|
154
155
|
}
|
155
156
|
|
157
|
+
export function getFailedTests(suite: CreeveySuite): CreeveyTest[] {
|
158
|
+
return Object.values(suite.children)
|
159
|
+
.filter(isDefined)
|
160
|
+
.flatMap((suiteOrTest) => {
|
161
|
+
if (isTest(suiteOrTest)) return suiteOrTest.status === 'failed' ? suiteOrTest : [];
|
162
|
+
|
163
|
+
return getFailedTests(suiteOrTest);
|
164
|
+
});
|
165
|
+
}
|
166
|
+
|
156
167
|
export function updateTestStatus(suite: CreeveySuite, path: string[], update: Partial<TestData>): void {
|
157
168
|
const title = path.shift();
|
158
169
|
|
@@ -173,7 +184,8 @@ export function updateTestStatus(suite: CreeveySuite, path: string[], update: Pa
|
|
173
184
|
if (test.results) test.results.push(...results);
|
174
185
|
else test.results = results;
|
175
186
|
}
|
176
|
-
if (
|
187
|
+
if (approved === null) test.approved = null;
|
188
|
+
else if (approved !== undefined)
|
177
189
|
Object.entries(approved).forEach(
|
178
190
|
([image, retry]) => retry !== undefined && ((test.approved = test.approved ?? {})[image] = retry),
|
179
191
|
);
|
@@ -215,6 +227,8 @@ export function removeTests(suite: CreeveySuite, path: string[]): void {
|
|
215
227
|
.reduce(calcStatus);
|
216
228
|
}
|
217
229
|
|
230
|
+
// TODO Include images to test suite
|
231
|
+
// TODO If only one image in test, don't include it
|
218
232
|
export function filterTests(suite: CreeveySuite, filter: CreeveyViewFilter): CreeveySuite {
|
219
233
|
const { status, subStrings } = filter;
|
220
234
|
if (!status && !subStrings.length) return suite;
|
@@ -258,14 +272,14 @@ export function flattenSuite(suite: CreeveySuite): { title: string; suite: Creev
|
|
258
272
|
export function countTestsStatus(suite: CreeveySuite): CreeveyTestsStatus {
|
259
273
|
let successCount = 0;
|
260
274
|
let failedCount = 0;
|
261
|
-
let
|
275
|
+
let approvedCount = 0;
|
262
276
|
let pendingCount = 0;
|
263
277
|
|
264
278
|
const cases: (CreeveySuite | CreeveyTest)[] = Object.values(suite.children).filter(isDefined);
|
265
279
|
let suiteOrTest;
|
266
280
|
while ((suiteOrTest = cases.pop())) {
|
267
281
|
if (isTest(suiteOrTest)) {
|
268
|
-
if (suiteOrTest.
|
282
|
+
if (suiteOrTest.status === 'approved') approvedCount++;
|
269
283
|
if (suiteOrTest.status === 'success') successCount++;
|
270
284
|
if (suiteOrTest.status === 'failed') failedCount++;
|
271
285
|
if (suiteOrTest.status === 'pending') pendingCount++;
|
@@ -274,7 +288,7 @@ export function countTestsStatus(suite: CreeveySuite): CreeveyTestsStatus {
|
|
274
288
|
}
|
275
289
|
}
|
276
290
|
|
277
|
-
return { successCount, failedCount,
|
291
|
+
return { approvedCount, successCount, failedCount, pendingCount };
|
278
292
|
}
|
279
293
|
|
280
294
|
export function getConnectionUrl(): string {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
2
2
|
import { useImmer } from 'use-immer';
|
3
3
|
import { ensure, styled, ThemeProvider, themes, withTheme } from '@storybook/theming';
|
4
4
|
import { CreeveyUpdate, CreeveySuite, isDefined, CreeveyTest } from '../../types.js';
|
@@ -15,6 +15,7 @@ import {
|
|
15
15
|
setSearchParams,
|
16
16
|
getTestPathFromSearch,
|
17
17
|
CreeveyViewFilter,
|
18
|
+
getFailedTests,
|
18
19
|
} from '../shared/helpers.js';
|
19
20
|
import { CreeveyContext } from './CreeveyContext.js';
|
20
21
|
import { KeyboardEvents } from './KeyboardEventsContext.js';
|
@@ -55,6 +56,21 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
55
56
|
const [theme, setTheme] = useTheme();
|
56
57
|
|
57
58
|
const openedTest = getTestByPath(tests, openedTestPath);
|
59
|
+
const failedTests = useMemo(() => getFailedTests(tests), [tests]);
|
60
|
+
|
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] ?? '');
|
64
|
+
const canApprove = useMemo(
|
65
|
+
() =>
|
66
|
+
Boolean(
|
67
|
+
openedTest?.results?.[retry - 1]?.images &&
|
68
|
+
openedTest.approved?.[imageName] != retry - 1 &&
|
69
|
+
openedTest.results[retry - 1].status != 'success',
|
70
|
+
),
|
71
|
+
[imageName, openedTest, retry],
|
72
|
+
);
|
73
|
+
|
58
74
|
if (openedTestPath.length > 0 && !isDefined(openedTest)) openTest([]);
|
59
75
|
|
60
76
|
const handleSuiteOpen = useCallback(
|
@@ -73,10 +89,42 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
73
89
|
},
|
74
90
|
[updateTests],
|
75
91
|
);
|
76
|
-
|
77
|
-
|
78
|
-
|
92
|
+
|
93
|
+
const handleOpenTest = useCallback(
|
94
|
+
(test: CreeveyTest): void => {
|
95
|
+
const testPath = getTestPath(test);
|
96
|
+
setSearchParams(testPath);
|
97
|
+
updateTests((draft) => {
|
98
|
+
openSuite(draft, testPath, true);
|
99
|
+
openTest(testPath);
|
100
|
+
});
|
101
|
+
},
|
102
|
+
[updateTests],
|
79
103
|
);
|
104
|
+
|
105
|
+
const handleGoToNextFailedTest = useCallback(() => {
|
106
|
+
if (failedTests.length <= 1) return;
|
107
|
+
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]);
|
111
|
+
|
112
|
+
const handleImageApproveNew = useCallback((): void => {
|
113
|
+
const id = openedTest?.id;
|
114
|
+
|
115
|
+
if (!id) return;
|
116
|
+
api?.approve(id, retry - 1, imageName);
|
117
|
+
}, [api, imageName, openedTest?.id, retry]);
|
118
|
+
|
119
|
+
const handleImageApproveAndGoNext = useCallback((): void => {
|
120
|
+
handleImageApproveNew();
|
121
|
+
handleGoToNextFailedTest();
|
122
|
+
}, [handleImageApproveNew, handleGoToNextFailedTest]);
|
123
|
+
|
124
|
+
const handleApproveAll = useCallback(() => {
|
125
|
+
api?.approveAll();
|
126
|
+
}, [api]);
|
127
|
+
|
80
128
|
const handleStart = useCallback(
|
81
129
|
(tests: CreeveySuite): void => api?.start(getCheckedTests(tests).map((test) => test.id)),
|
82
130
|
[api],
|
@@ -88,11 +136,13 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
88
136
|
},
|
89
137
|
[setTheme],
|
90
138
|
);
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
139
|
+
|
140
|
+
useEffect(() => {
|
141
|
+
const retry = openedTest?.results?.length ?? 0;
|
142
|
+
const result = openedTest?.results?.[retry - 1] ?? { images: {} };
|
143
|
+
setImageName(Object.keys(result.images ?? {})[0] ?? '');
|
144
|
+
setRetry(retry);
|
145
|
+
}, [openedTest?.results]);
|
96
146
|
|
97
147
|
useEffect(() => {
|
98
148
|
window.addEventListener('popstate', (event) => {
|
@@ -139,6 +189,8 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
139
189
|
value={{
|
140
190
|
isReport: initialState.isReport,
|
141
191
|
isRunning,
|
192
|
+
onImageApprove: canApprove ? handleImageApproveAndGoNext : undefined,
|
193
|
+
onApproveAll: handleApproveAll,
|
142
194
|
onStart: handleStart,
|
143
195
|
onStop: handleStop,
|
144
196
|
onSuiteOpen: handleSuiteOpen,
|
@@ -150,7 +202,7 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
150
202
|
<FlexContainer>
|
151
203
|
<SideBar
|
152
204
|
rootSuite={tests}
|
153
|
-
|
205
|
+
testId={openedTest?.id}
|
154
206
|
onOpenTest={handleOpenTest}
|
155
207
|
filter={filter}
|
156
208
|
setFilter={setFilter}
|
@@ -158,12 +210,13 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
|
|
158
210
|
{openedTest && (
|
159
211
|
<ResultsPage
|
160
212
|
key={`${openedTest.id}_${openedTest.results?.length ?? 0}`}
|
161
|
-
id={openedTest.id}
|
162
213
|
path={openedTestPath}
|
163
214
|
results={openedTest.results}
|
164
215
|
approved={openedTest.approved}
|
165
|
-
|
166
|
-
|
216
|
+
retry={retry}
|
217
|
+
imageName={imageName}
|
218
|
+
onImageChange={setImageName}
|
219
|
+
onRetryChange={setRetry}
|
167
220
|
/>
|
168
221
|
)}
|
169
222
|
<ToggleContainer>
|
@@ -1,10 +1,12 @@
|
|
1
|
-
import React from 'react';
|
1
|
+
import React, { useContext } from 'react';
|
2
2
|
import { CreeveySuite, noop } from '../../types.js';
|
3
3
|
|
4
4
|
export interface CreeveyContextType {
|
5
5
|
isReport: boolean;
|
6
6
|
isRunning: boolean;
|
7
7
|
onStop: () => void;
|
8
|
+
onImageApprove?: () => void;
|
9
|
+
onApproveAll: () => void;
|
8
10
|
onStart: (rootSuite: CreeveySuite) => void;
|
9
11
|
onSuiteOpen: (path: string[], opened: boolean) => void;
|
10
12
|
onSuiteToggle: (path: string[], checked: boolean) => void;
|
@@ -13,8 +15,12 @@ export interface CreeveyContextType {
|
|
13
15
|
export const CreeveyContext = React.createContext<CreeveyContextType>({
|
14
16
|
isReport: true,
|
15
17
|
isRunning: false,
|
18
|
+
onImageApprove: noop,
|
19
|
+
onApproveAll: noop,
|
16
20
|
onStop: noop,
|
17
21
|
onStart: noop,
|
18
22
|
onSuiteOpen: noop,
|
19
23
|
onSuiteToggle: noop,
|
20
24
|
});
|
25
|
+
|
26
|
+
export const useCreeveyContext = () => useContext(CreeveyContext);
|
@@ -70,11 +70,11 @@ interface CheckboxState {
|
|
70
70
|
indeterminate: boolean;
|
71
71
|
}
|
72
72
|
|
73
|
-
export const CheckboxContainer = styled.
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
73
|
+
export const CheckboxContainer = styled.span({
|
74
|
+
paddingLeft: '8px',
|
75
|
+
verticalAlign: 'middle',
|
76
|
+
alignSelf: 'center',
|
77
|
+
lineHeight: '18px',
|
78
78
|
});
|
79
79
|
|
80
80
|
export class Checkbox extends React.Component<CheckboxProps, CheckboxState> {
|
@@ -1,7 +1,7 @@
|
|
1
|
-
import React, { createContext
|
1
|
+
import React, { createContext } from 'react';
|
2
2
|
import { transparentize } from 'polished';
|
3
3
|
import { ScrollArea } from '@storybook/components';
|
4
|
-
import { styled, withTheme } from '@storybook/theming';
|
4
|
+
import { styled, Theme, withTheme } from '@storybook/theming';
|
5
5
|
import { SideBarHeader } from './SideBarHeader.js';
|
6
6
|
import { CreeveySuite, CreeveyTest, noop, isTest } from '../../../../types.js';
|
7
7
|
import {
|
@@ -11,17 +11,18 @@ import {
|
|
11
11
|
countTestsStatus,
|
12
12
|
getCheckedTests,
|
13
13
|
} from '../../../shared/helpers.js';
|
14
|
-
import {
|
14
|
+
import { useCreeveyContext } from '../../CreeveyContext.js';
|
15
15
|
import { SuiteLink } from './SuiteLink.js';
|
16
16
|
import { TestLink } from './TestLink.js';
|
17
|
+
import { SideBarFooter } from './SideBarFooter.js';
|
17
18
|
|
18
19
|
export const SideBarContext = createContext<{ onOpenTest: (test: CreeveyTest) => void }>({
|
19
20
|
onOpenTest: noop,
|
20
21
|
});
|
21
22
|
|
22
23
|
export interface SideBarProps {
|
24
|
+
testId?: string;
|
23
25
|
rootSuite: CreeveySuite;
|
24
|
-
openedTest: CreeveyTest | null;
|
25
26
|
onOpenTest: (test: CreeveyTest) => void;
|
26
27
|
filter: CreeveyViewFilter;
|
27
28
|
setFilter: (filter: CreeveyViewFilter) => void;
|
@@ -37,7 +38,7 @@ const Container = withTheme(
|
|
37
38
|
);
|
38
39
|
|
39
40
|
const ScrollContainer = styled.div({
|
40
|
-
height: 'calc(100vh -
|
41
|
+
height: 'calc(100vh - 245px)',
|
41
42
|
width: 300,
|
42
43
|
flex: 'none',
|
43
44
|
overflowY: 'auto',
|
@@ -46,10 +47,16 @@ const ScrollContainer = styled.div({
|
|
46
47
|
left: '0',
|
47
48
|
});
|
48
49
|
|
50
|
+
const StyledScrollArea = styled(ScrollArea)({
|
51
|
+
'& > div > div': {
|
52
|
+
height: 'calc(100% - 8px)',
|
53
|
+
},
|
54
|
+
});
|
55
|
+
|
49
56
|
const Shadow = withTheme(
|
50
|
-
styled.div(({ theme }) => ({
|
57
|
+
styled.div<{ theme: Theme; position: 'top' | 'bottom' }>(({ theme, position }) => ({
|
58
|
+
[position]: '0px',
|
51
59
|
position: 'sticky',
|
52
|
-
top: '0px',
|
53
60
|
boxShadow: `0 0 5px 2.5px ${transparentize(0.8, theme.color.defaultText)}`,
|
54
61
|
zIndex: 3,
|
55
62
|
})),
|
@@ -62,21 +69,22 @@ const SelectAllContainer = styled.div({
|
|
62
69
|
|
63
70
|
const TestsContainer = styled.div({
|
64
71
|
position: 'relative',
|
65
|
-
paddingBottom: '
|
72
|
+
paddingBottom: '8px',
|
73
|
+
height: '100%',
|
66
74
|
});
|
67
75
|
|
68
76
|
const Divider = withTheme(
|
69
|
-
styled.div(({ theme }) => ({
|
70
|
-
position: 'absolute',
|
77
|
+
styled.div<{ theme: Theme; position: 'top' | 'bottom' }>(({ theme, position }) => ({
|
78
|
+
...(position === 'top' ? { position: 'absolute' } : { position: 'relative', bottom: '8px', marginBottom: '-8px' }),
|
71
79
|
height: '8px',
|
72
80
|
width: '100%',
|
73
|
-
zIndex:
|
81
|
+
zIndex: 4,
|
74
82
|
background: theme.background.content,
|
75
83
|
})),
|
76
84
|
);
|
77
85
|
|
78
|
-
export function SideBar({ rootSuite,
|
79
|
-
const { onStart, onStop } =
|
86
|
+
export function SideBar({ rootSuite, testId, onOpenTest, filter, setFilter }: SideBarProps): JSX.Element {
|
87
|
+
const { onStart, onStop } = useCreeveyContext();
|
80
88
|
|
81
89
|
// TODO Maybe need to do flatten first?
|
82
90
|
const suite = filterTests(rootSuite, filter);
|
@@ -100,10 +108,10 @@ export function SideBar({ rootSuite, openedTest, onOpenTest, filter, setFilter }
|
|
100
108
|
canStart={countCheckedTests !== 0}
|
101
109
|
/>
|
102
110
|
<ScrollContainer>
|
103
|
-
<
|
104
|
-
<Shadow />
|
111
|
+
<StyledScrollArea vertical>
|
112
|
+
<Shadow position="top" />
|
105
113
|
<TestsContainer>
|
106
|
-
<Divider />
|
114
|
+
<Divider position="top" />
|
107
115
|
{/* TODO Output message when nothing found */}
|
108
116
|
<SelectAllContainer>
|
109
117
|
<SuiteLink title="Select all" suite={rootSuite} data-testid="selectAll" />
|
@@ -111,14 +119,17 @@ export function SideBar({ rootSuite, openedTest, onOpenTest, filter, setFilter }
|
|
111
119
|
{suiteList.map(({ title, suite }) =>
|
112
120
|
// TODO Update components without re-mount
|
113
121
|
isTest(suite) ? (
|
114
|
-
<TestLink key={suite.id} title={title} opened={suite.id ==
|
122
|
+
<TestLink key={suite.id} title={title} opened={suite.id == testId} test={suite} />
|
115
123
|
) : (
|
116
124
|
<SuiteLink key={suite.path.join('/')} title={title} suite={suite} data-testid={title} />
|
117
125
|
),
|
118
126
|
)}
|
119
127
|
</TestsContainer>
|
120
|
-
|
128
|
+
<Divider position="bottom" />
|
129
|
+
</StyledScrollArea>
|
130
|
+
<Shadow position="bottom" />
|
121
131
|
</ScrollContainer>
|
132
|
+
<SideBarFooter />
|
122
133
|
</Container>
|
123
134
|
</SideBarContext.Provider>
|
124
135
|
);
|