creevey 0.9.0 → 0.9.2
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/pool.js +4 -2
- package/dist/server/master/pool.js.map +1 -1
- package/dist/server/master/runner.d.ts +2 -0
- package/dist/server/master/runner.js +61 -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 +18 -12
- package/dist/server/worker/worker.js.map +1 -1
- package/dist/types.d.ts +5 -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/pool.ts +5 -2
- package/src/server/master/runner.ts +67 -9
- package/src/server/selenium/browser.ts +13 -10
- package/src/server/worker/worker.ts +17 -11
- package/src/types.ts +5 -3
@@ -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
|
);
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { styled, withTheme } from '@storybook/theming';
|
3
|
+
import { Button, Icons } from '@storybook/components';
|
4
|
+
import { useCreeveyContext } from '../../CreeveyContext.js';
|
5
|
+
|
6
|
+
const Sticky = withTheme(
|
7
|
+
styled.div(({ theme }) => ({
|
8
|
+
padding: '24px 16px 8px 16px',
|
9
|
+
background: theme.background.content,
|
10
|
+
height: '50px',
|
11
|
+
zIndex: 5,
|
12
|
+
position: 'sticky',
|
13
|
+
bottom: '0',
|
14
|
+
})),
|
15
|
+
);
|
16
|
+
|
17
|
+
const Container = styled.div({
|
18
|
+
display: 'flex',
|
19
|
+
justifyContent: 'space-between',
|
20
|
+
});
|
21
|
+
|
22
|
+
export function SideBarFooter(): JSX.Element {
|
23
|
+
const { onApproveAll, onImageApprove } = useCreeveyContext();
|
24
|
+
|
25
|
+
return (
|
26
|
+
<Sticky>
|
27
|
+
<Container>
|
28
|
+
<Button secondary onClick={onImageApprove} disabled={!onImageApprove} style={{ paddingRight: 8 }}>
|
29
|
+
Approve
|
30
|
+
<Icons icon="arrowright" style={{ paddingLeft: 4 }} />
|
31
|
+
</Button>
|
32
|
+
<Button secondary outline onClick={onApproveAll}>
|
33
|
+
Approve all
|
34
|
+
</Button>
|
35
|
+
</Container>
|
36
|
+
</Sticky>
|
37
|
+
);
|
38
|
+
}
|
@@ -14,49 +14,67 @@ export interface SuiteLinkProps {
|
|
14
14
|
}
|
15
15
|
|
16
16
|
export const Container = withTheme(
|
17
|
-
styled.div<{ theme: Theme; disabled?: boolean
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
styled.div<{ theme: Theme; disabled?: boolean; active?: boolean; focused?: boolean }>(
|
18
|
+
({ theme, disabled, active, focused }) => ({
|
19
|
+
position: 'relative',
|
20
|
+
width: '100%',
|
21
|
+
height: '28px',
|
22
|
+
lineHeight: '20px',
|
23
|
+
display: 'flex',
|
24
|
+
background: active ? theme.color.secondary : focused ? theme.background.hoverable : 'none',
|
25
|
+
color: active ? theme.color.inverseText : 'inherit',
|
26
|
+
outline: focused ? `1px solid ${theme.color.ancillary}` : 'none',
|
27
|
+
...(disabled ? { color: theme.color.mediumdark, pointerEvents: 'none' } : {}),
|
28
|
+
|
29
|
+
// NOTE There is no way to trigger hover from js, so we add `.hover` class for testing purpose
|
30
|
+
'&:hover, &.hover': active
|
31
|
+
? {}
|
32
|
+
: {
|
33
|
+
background: theme.background.hoverable,
|
34
|
+
},
|
35
|
+
}),
|
36
|
+
),
|
22
37
|
);
|
23
38
|
|
24
39
|
export const Button = withTheme(
|
25
|
-
styled.button<{ theme: Theme; active?: boolean
|
26
|
-
|
40
|
+
styled.button<{ theme: Theme; active?: boolean }>(({ theme, active }) => ({
|
41
|
+
flexGrow: 1,
|
27
42
|
boxSizing: 'border-box',
|
28
43
|
appearance: 'none',
|
29
|
-
padding: '
|
30
|
-
lineHeight: '
|
44
|
+
padding: '4px 16px 4px 8px',
|
45
|
+
lineHeight: '18px',
|
31
46
|
cursor: 'pointer',
|
32
47
|
border: 'none',
|
33
48
|
zIndex: 1,
|
34
49
|
textAlign: 'left',
|
35
|
-
background:
|
50
|
+
background: 'none',
|
51
|
+
outline: 'none',
|
36
52
|
color: active ? theme.color.inverseText : 'inherit',
|
37
|
-
outline: focused ? `1px solid ${theme.color.ancillary}` : 'none',
|
38
|
-
|
39
|
-
// NOTE There is no way to trigger hover from js, so we add `.hover` class for testing purpose
|
40
|
-
'&:hover, &.hover': active
|
41
|
-
? {}
|
42
|
-
: {
|
43
|
-
background: theme.background.hoverable,
|
44
|
-
},
|
45
53
|
})),
|
46
54
|
);
|
47
55
|
|
48
56
|
const ArrowIcon = styled(Icons)({
|
49
|
-
paddingRight: '
|
57
|
+
paddingRight: '4px',
|
50
58
|
display: 'inline-block',
|
51
|
-
width: '
|
52
|
-
height: '
|
59
|
+
width: '12px',
|
60
|
+
height: '18px',
|
61
|
+
verticalAlign: 'unset',
|
53
62
|
});
|
54
63
|
|
55
64
|
export const SuiteContainer = styled.span<{ padding: number }>(({ padding }) => ({
|
56
65
|
paddingLeft: padding,
|
57
66
|
whiteSpace: 'normal',
|
67
|
+
display: 'grid',
|
68
|
+
gridTemplateColumns: 'repeat(2, min-content) auto',
|
58
69
|
}));
|
59
70
|
|
71
|
+
export const SuiteTitle = styled.span({
|
72
|
+
paddingLeft: '4px',
|
73
|
+
whiteSpace: 'nowrap',
|
74
|
+
overflowX: 'hidden',
|
75
|
+
textOverflow: 'ellipsis',
|
76
|
+
});
|
77
|
+
|
60
78
|
export function SuiteLink({ title, suite, 'data-testid': dataTid }: SuiteLinkProps): JSX.Element {
|
61
79
|
const { onSuiteOpen, onSuiteToggle } = useContext(CreeveyContext);
|
62
80
|
const { sidebarFocusedItem, setSidebarFocusedItem } = useContext(KeyboardEventsContext);
|
@@ -70,6 +88,7 @@ export function SuiteLink({ title, suite, 'data-testid': dataTid }: SuiteLinkPro
|
|
70
88
|
sidebarFocusedItem.every((x) => suite.path.includes(x)),
|
71
89
|
[suite, sidebarFocusedItem],
|
72
90
|
);
|
91
|
+
|
73
92
|
useEffect(
|
74
93
|
() => (suite.indeterminate ? checkboxRef.current?.setIndeterminate() : checkboxRef.current?.resetIndeterminate()),
|
75
94
|
[suite.indeterminate],
|
@@ -95,16 +114,7 @@ export function SuiteLink({ title, suite, 'data-testid': dataTid }: SuiteLinkPro
|
|
95
114
|
};
|
96
115
|
|
97
116
|
return (
|
98
|
-
<Container>
|
99
|
-
<Button onClick={handleOpen} onFocus={handleFocus} data-testid={dataTid} focused={isSuiteFocused} ref={buttonRef}>
|
100
|
-
<TestStatusIcon status={suite.status} skip={suite.skip} />
|
101
|
-
<SuiteContainer padding={Math.max(48, (suite.path.length + 5) * 8)}>
|
102
|
-
{isTest(suite) ||
|
103
|
-
(Boolean(suite.path.length) &&
|
104
|
-
(suite.opened ? <ArrowIcon icon="arrowdown" /> : <ArrowIcon icon="arrowright" />))}
|
105
|
-
{title}
|
106
|
-
</SuiteContainer>
|
107
|
-
</Button>
|
117
|
+
<Container focused={isSuiteFocused}>
|
108
118
|
<CheckboxContainer>
|
109
119
|
<Checkbox
|
110
120
|
ref={checkboxRef}
|
@@ -113,6 +123,15 @@ export function SuiteLink({ title, suite, 'data-testid': dataTid }: SuiteLinkPro
|
|
113
123
|
onValueChange={handleCheck}
|
114
124
|
/>
|
115
125
|
</CheckboxContainer>
|
126
|
+
<Button onClick={handleOpen} onFocus={handleFocus} data-testid={dataTid} ref={buttonRef}>
|
127
|
+
<SuiteContainer padding={(suite.path.length - 1) * 8}>
|
128
|
+
{isTest(suite) ||
|
129
|
+
(Boolean(suite.path.length) &&
|
130
|
+
(suite.opened ? <ArrowIcon icon="arrowdown" /> : <ArrowIcon icon="arrowright" />))}
|
131
|
+
<TestStatusIcon status={suite.status} skip={suite.skip} />
|
132
|
+
<SuiteTitle>{title}</SuiteTitle>
|
133
|
+
</SuiteContainer>
|
134
|
+
</Button>
|
116
135
|
</Container>
|
117
136
|
);
|
118
137
|
}
|
@@ -4,7 +4,7 @@ import { TestStatusIcon } from './TestStatusIcon.js';
|
|
4
4
|
import { CreeveyContext } from '../../CreeveyContext.js';
|
5
5
|
import { SideBarContext } from './SideBar.js';
|
6
6
|
import { KeyboardEventsContext } from '../../KeyboardEventsContext.js';
|
7
|
-
import { Button, Container, SuiteContainer } from './SuiteLink.js';
|
7
|
+
import { Button, Container, SuiteContainer, SuiteTitle } from './SuiteLink.js';
|
8
8
|
import { Checkbox, CheckboxContainer } from './Checkbox.js';
|
9
9
|
import { getTestPath } from '../../../shared/helpers.js';
|
10
10
|
|
@@ -48,11 +48,7 @@ export function TestLink({ title, opened, test }: TestLinkProps): JSX.Element {
|
|
48
48
|
}, [test, onOpenTest, setSidebarFocusedItem]);
|
49
49
|
|
50
50
|
return (
|
51
|
-
<Container disabled={emptyResults}>
|
52
|
-
<Button onClick={handleOpen} active={opened} focused={isTestFocused} disabled={emptyResults} ref={buttonRef}>
|
53
|
-
<TestStatusIcon inverted={opened} status={test.status} skip={test.skip} />
|
54
|
-
<SuiteContainer padding={(testPath.length + 8) * 8}>{title}</SuiteContainer>
|
55
|
-
</Button>
|
51
|
+
<Container disabled={emptyResults} active={opened} focused={isTestFocused}>
|
56
52
|
{/* NOTE Little hack to allow click on checkbox and don't trigger Button click */}
|
57
53
|
{/* We can use other approach, but checkbox has vertical-align: top */}
|
58
54
|
<CheckboxContainer>
|
@@ -62,6 +58,12 @@ export function TestLink({ title, opened, test }: TestLinkProps): JSX.Element {
|
|
62
58
|
onValueChange={handleCheck}
|
63
59
|
/>
|
64
60
|
</CheckboxContainer>
|
61
|
+
<Button onClick={handleOpen} disabled={emptyResults} ref={buttonRef}>
|
62
|
+
<SuiteContainer padding={(testPath.length + 1) * 8}>
|
63
|
+
<TestStatusIcon inverted={opened} status={test.status} skip={test.skip} />
|
64
|
+
<SuiteTitle>{title}</SuiteTitle>
|
65
|
+
</SuiteContainer>
|
66
|
+
</Button>
|
65
67
|
</Container>
|
66
68
|
);
|
67
69
|
}
|
@@ -11,21 +11,22 @@ export interface TestStatusIconProps {
|
|
11
11
|
}
|
12
12
|
|
13
13
|
const Container = styled.span({
|
14
|
-
width:
|
15
|
-
height:
|
14
|
+
width: '16px',
|
15
|
+
height: '13px',
|
16
16
|
padding: 1,
|
17
17
|
display: 'inline-block',
|
18
18
|
});
|
19
19
|
|
20
20
|
const Icon = styled(Icons)({
|
21
21
|
position: 'relative',
|
22
|
-
top: '
|
22
|
+
top: '1px',
|
23
23
|
verticalAlign: 'baseline',
|
24
24
|
});
|
25
25
|
|
26
26
|
const Spinner = styled(Loader)({
|
27
|
-
top: '
|
28
|
-
left: '
|
27
|
+
top: '12px',
|
28
|
+
left: 'unset',
|
29
|
+
marginLeft: '0px',
|
29
30
|
});
|
30
31
|
|
31
32
|
export const TestStatusIcon = withTheme(
|
@@ -40,6 +41,10 @@ export const TestStatusIcon = withTheme(
|
|
40
41
|
icon = <Icon color={inverted ? theme.color.lightest : theme.color.green} icon="check" />;
|
41
42
|
break;
|
42
43
|
}
|
44
|
+
case 'approved': {
|
45
|
+
icon = <Icon color={inverted ? theme.color.lightest : theme.color.mediumdark} icon="thumbsup" />;
|
46
|
+
break;
|
47
|
+
}
|
43
48
|
case 'running': {
|
44
49
|
icon = <Spinner size={10} />;
|
45
50
|
break;
|
@@ -49,7 +54,8 @@ export const TestStatusIcon = withTheme(
|
|
49
54
|
break;
|
50
55
|
}
|
51
56
|
default: {
|
52
|
-
if (skip) icon = <Icon color={inverted ? theme.color.lightest : undefined} icon="
|
57
|
+
if (skip) icon = <Icon color={inverted ? theme.color.lightest : undefined} icon="alert" />;
|
58
|
+
else icon = <Icon color={inverted ? theme.color.lightest : undefined} icon="circlehollow" />;
|
53
59
|
break;
|
54
60
|
}
|
55
61
|
}
|
@@ -53,7 +53,7 @@ export const TestsStatus = withTheme(
|
|
53
53
|
successCount,
|
54
54
|
failedCount,
|
55
55
|
pendingCount,
|
56
|
-
|
56
|
+
approvedCount,
|
57
57
|
onClickByStatus,
|
58
58
|
theme,
|
59
59
|
}: TestsStatusProps): JSX.Element => {
|
@@ -94,9 +94,13 @@ export const TestsStatus = withTheme(
|
|
94
94
|
</Button>
|
95
95
|
<Divider />
|
96
96
|
{/* @ts-expect-error Fixed in https://github.com/storybookjs/storybook/pull/26623 */}
|
97
|
-
<Button
|
97
|
+
<Button
|
98
|
+
onClick={() => {
|
99
|
+
onClickByStatus('approved');
|
100
|
+
}}
|
101
|
+
>
|
98
102
|
<IconContainer color={theme?.color.defaultText}>
|
99
|
-
<Icons icon="
|
103
|
+
<Icons icon="thumbsup" /> {approvedCount}
|
100
104
|
</IconContainer>
|
101
105
|
</Button>
|
102
106
|
</Container>
|
package/src/server/master/api.ts
CHANGED
@@ -96,11 +96,13 @@ export default class Pool extends EventEmitter {
|
|
96
96
|
}
|
97
97
|
|
98
98
|
private getFreeWorker(): Worker | undefined {
|
99
|
-
|
99
|
+
const freeWorkers = this.freeWorkers;
|
100
|
+
|
101
|
+
return freeWorkers[Math.floor(Math.random() * freeWorkers.length)];
|
100
102
|
}
|
101
103
|
|
102
104
|
private get aliveWorkers(): Worker[] {
|
103
|
-
return this.workers.filter((worker) => !worker.exitedAfterDisconnect);
|
105
|
+
return this.workers.filter((worker) => !worker.exitedAfterDisconnect && !worker.isShuttingDown);
|
104
106
|
}
|
105
107
|
|
106
108
|
private get freeWorkers(): Worker[] {
|
@@ -146,6 +148,7 @@ export default class Pool extends EventEmitter {
|
|
146
148
|
}
|
147
149
|
|
148
150
|
private gracefullyKill(worker: Worker): void {
|
151
|
+
worker.isShuttingDown = true;
|
149
152
|
const timeout = setTimeout(() => {
|
150
153
|
worker.kill();
|
151
154
|
}, 10000);
|
@@ -45,6 +45,7 @@ export default class Runner extends EventEmitter {
|
|
45
45
|
// TODO Handle 'retrying' status
|
46
46
|
test.status = status == 'retrying' ? 'failed' : status;
|
47
47
|
if (!result) {
|
48
|
+
// NOTE: Running status
|
48
49
|
this.sendUpdate({ tests: { [id]: { id, browser, testName, storyPath, status: test.status, storyId } } });
|
49
50
|
return;
|
50
51
|
}
|
@@ -53,8 +54,23 @@ export default class Runner extends EventEmitter {
|
|
53
54
|
}
|
54
55
|
test.results.push(result);
|
55
56
|
|
57
|
+
if (status == 'failed') {
|
58
|
+
test.approved = null;
|
59
|
+
}
|
60
|
+
|
56
61
|
this.sendUpdate({
|
57
|
-
tests: {
|
62
|
+
tests: {
|
63
|
+
[id]: {
|
64
|
+
id,
|
65
|
+
browser,
|
66
|
+
testName,
|
67
|
+
storyPath,
|
68
|
+
status: test.status,
|
69
|
+
approved: test.approved,
|
70
|
+
results: [result],
|
71
|
+
storyId,
|
72
|
+
},
|
73
|
+
},
|
58
74
|
});
|
59
75
|
|
60
76
|
if (this.failFast && status == 'failed') this.stop();
|
@@ -161,6 +177,46 @@ export default class Runner extends EventEmitter {
|
|
161
177
|
};
|
162
178
|
}
|
163
179
|
|
180
|
+
private async copyImage(test: ServerTest, image: string, actual: string): Promise<void> {
|
181
|
+
const { browser, testName, storyPath } = test;
|
182
|
+
const restPath = [...storyPath, testName].filter(isDefined);
|
183
|
+
const testPath = path.join(...restPath, image == browser ? '' : browser);
|
184
|
+
const srcImagePath = path.join(this.reportDir, testPath, actual);
|
185
|
+
const dstImagePath = path.join(this.screenDir, testPath, `${image}.png`);
|
186
|
+
await mkdir(path.join(this.screenDir, testPath), { recursive: true });
|
187
|
+
await copyFile(srcImagePath, dstImagePath);
|
188
|
+
}
|
189
|
+
|
190
|
+
public async approveAll(): Promise<void> {
|
191
|
+
const updatedTests: NonNullable<CreeveyUpdate['tests']> = {};
|
192
|
+
for (const test of Object.values(this.tests)) {
|
193
|
+
if (!test?.results) continue;
|
194
|
+
const retry = test.results.length - 1;
|
195
|
+
const { images, status } = test.results.at(retry) ?? {};
|
196
|
+
if (!images || status != 'failed') continue;
|
197
|
+
for (const [name, image] of Object.entries(images)) {
|
198
|
+
if (!image) continue;
|
199
|
+
await this.copyImage(test, name, image.actual);
|
200
|
+
|
201
|
+
if (!test.approved) {
|
202
|
+
test.approved = {};
|
203
|
+
}
|
204
|
+
test.approved[name] = retry;
|
205
|
+
test.status = 'approved';
|
206
|
+
|
207
|
+
updatedTests[test.id] = {
|
208
|
+
id: test.id,
|
209
|
+
browser: test.browser,
|
210
|
+
storyPath: test.storyPath,
|
211
|
+
storyId: test.storyId,
|
212
|
+
status: test.status,
|
213
|
+
approved: { [name]: retry },
|
214
|
+
};
|
215
|
+
}
|
216
|
+
}
|
217
|
+
this.sendUpdate({ tests: updatedTests });
|
218
|
+
}
|
219
|
+
|
164
220
|
public async approve({ id, retry, image }: ApprovePayload): Promise<void> {
|
165
221
|
const test = this.tests[id];
|
166
222
|
if (!test?.results) return;
|
@@ -171,16 +227,18 @@ export default class Runner extends EventEmitter {
|
|
171
227
|
if (!test.approved) {
|
172
228
|
test.approved = {};
|
173
229
|
}
|
174
|
-
const { browser, testName, storyPath } = test;
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
const dstImagePath = path.join(this.screenDir, testPath, `${image}.png`);
|
179
|
-
await mkdir(path.join(this.screenDir, testPath), { recursive: true });
|
180
|
-
await copyFile(srcImagePath, dstImagePath);
|
230
|
+
const { browser, testName, storyPath, storyId } = test;
|
231
|
+
|
232
|
+
await this.copyImage(test, image, images.actual);
|
233
|
+
|
181
234
|
test.approved[image] = retry;
|
235
|
+
|
236
|
+
if (Object.keys(result.images).every((name) => typeof test.approved?.[name] == 'number')) {
|
237
|
+
test.status = 'approved';
|
238
|
+
}
|
239
|
+
|
182
240
|
this.sendUpdate({
|
183
|
-
tests: { [id]: { id, browser, testName, storyPath, approved: { [image]: retry }, storyId
|
241
|
+
tests: { [id]: { id, browser, testName, storyPath, status: test.status, approved: { [image]: retry }, storyId } },
|
184
242
|
});
|
185
243
|
}
|
186
244
|
private sendUpdate(data: CreeveyUpdate): void {
|
@@ -55,6 +55,7 @@ let browserName = '';
|
|
55
55
|
let browser: WebDriver | null = null;
|
56
56
|
// let context: UnPromise<ReturnType<typeof BrowsingContext>> | null = null;
|
57
57
|
let creeveyServerHost: string | null = null;
|
58
|
+
let creeveyServerPort: number | null = null;
|
58
59
|
|
59
60
|
function getSessionData(grid: string, sessionId = ''): Promise<Record<string, unknown>> {
|
60
61
|
const gridUrl = new URL(grid);
|
@@ -520,11 +521,10 @@ async function openStorybookPage(
|
|
520
521
|
}
|
521
522
|
}
|
522
523
|
|
523
|
-
async function resolveCreeveyHost(browser: WebDriver, port: number): Promise<
|
524
|
-
if (creeveyServerHost != null) return creeveyServerHost;
|
525
|
-
|
524
|
+
async function resolveCreeveyHost(browser: WebDriver, port: number): Promise<void> {
|
526
525
|
const addresses = getAddresses();
|
527
526
|
|
527
|
+
creeveyServerPort = port;
|
528
528
|
creeveyServerHost = await browser.executeAsyncScript(
|
529
529
|
function (hosts: string[], port: number, callback: (host?: string | null) => void) {
|
530
530
|
void Promise.all(
|
@@ -558,8 +558,6 @@ async function resolveCreeveyHost(browser: WebDriver, port: number): Promise<str
|
|
558
558
|
);
|
559
559
|
|
560
560
|
if (creeveyServerHost == null) throw new Error("Can't reach creevey server from a browser");
|
561
|
-
|
562
|
-
return creeveyServerHost;
|
563
561
|
}
|
564
562
|
|
565
563
|
export async function loadStoriesFromBrowser(): Promise<StoriesRaw> {
|
@@ -690,8 +688,14 @@ export async function getBrowser(config: Config, options: Options & { browser: s
|
|
690
688
|
await updateStorybookGlobals(browser, _storybookGlobals);
|
691
689
|
}
|
692
690
|
|
693
|
-
|
691
|
+
await resolveCreeveyHost(browser, options.port);
|
692
|
+
|
693
|
+
await updateBrowserGlobalVariables(browser);
|
694
|
+
|
695
|
+
return browser;
|
696
|
+
}
|
694
697
|
|
698
|
+
async function updateBrowserGlobalVariables(browser: WebDriver) {
|
695
699
|
await browser.executeScript(
|
696
700
|
function (workerId: number, creeveyHost: string, creeveyPort: number) {
|
697
701
|
window.__CREEVEY_WORKER_ID__ = workerId;
|
@@ -699,11 +703,9 @@ export async function getBrowser(config: Config, options: Options & { browser: s
|
|
699
703
|
window.__CREEVEY_SERVER_PORT__ = creeveyPort;
|
700
704
|
},
|
701
705
|
process.pid,
|
702
|
-
|
703
|
-
|
706
|
+
creeveyServerHost,
|
707
|
+
creeveyServerPort,
|
704
708
|
);
|
705
|
-
|
706
|
-
return browser;
|
707
709
|
}
|
708
710
|
|
709
711
|
async function updateStoryArgs(browser: WebDriver, story: StoryInput, updatedArgs: Args): Promise<void> {
|
@@ -799,6 +801,7 @@ export async function switchStory(this: Context): Promise<void> {
|
|
799
801
|
});
|
800
802
|
});
|
801
803
|
|
804
|
+
await updateBrowserGlobalVariables(this.browser);
|
802
805
|
await resetMousePosition(this.browser);
|
803
806
|
const isCaptureCalled = await selectStory(this.browser, id, waitForReady);
|
804
807
|
|
@@ -9,7 +9,7 @@ import { Key, until } from 'selenium-webdriver';
|
|
9
9
|
import { Config, Images, Options, TestMessage, isImageError } from '../../types.js';
|
10
10
|
import { subscribeOn, emitTestMessage, emitWorkerMessage } from '../messages.js';
|
11
11
|
import chaiImage from './chai-image.js';
|
12
|
-
import {
|
12
|
+
import { getBrowser, switchStory } from '../selenium/index.js';
|
13
13
|
import { CreeveyReporter, TeamcityReporter } from './reporter.js';
|
14
14
|
import { addTestsFromStories } from './helpers.js';
|
15
15
|
import { logger } from '../logger.js';
|
@@ -136,7 +136,19 @@ export async function start(config: Config, options: Options & { browser: string
|
|
136
136
|
|
137
137
|
chai.use(chaiImage(getExpected, config.diffOptions));
|
138
138
|
|
139
|
-
|
139
|
+
const browser = await (async () => {
|
140
|
+
try {
|
141
|
+
return await getBrowser(config, options);
|
142
|
+
} catch (error) {
|
143
|
+
emitWorkerMessage({
|
144
|
+
type: 'error',
|
145
|
+
payload: { error: error instanceof Error ? error.message : ((error ?? 'Unknown error') as string) },
|
146
|
+
});
|
147
|
+
return null;
|
148
|
+
}
|
149
|
+
})();
|
150
|
+
|
151
|
+
if (browser == null) return;
|
140
152
|
|
141
153
|
await addTestsFromStories(mocha.suite, config, {
|
142
154
|
browser: options.browser,
|
@@ -145,18 +157,11 @@ export async function start(config: Config, options: Options & { browser: string
|
|
145
157
|
port: options.port,
|
146
158
|
});
|
147
159
|
|
148
|
-
|
149
|
-
await (await getBrowser(config, options))?.getCurrentUrl();
|
150
|
-
} catch {
|
151
|
-
await closeBrowser();
|
152
|
-
}
|
153
|
-
const browser = await getBrowser(config, options);
|
154
|
-
const sessionId = (await browser?.getSession())?.getId();
|
155
|
-
|
156
|
-
if (browser == null) return;
|
160
|
+
const sessionId = (await browser.getSession()).getId();
|
157
161
|
|
158
162
|
const interval = setInterval(
|
159
163
|
() =>
|
164
|
+
// NOTE Simple way to keep session alive
|
160
165
|
void browser.getCurrentUrl().then((url) => {
|
161
166
|
logger.debug(`${options.browser}:${chalk.gray(sessionId)}`, 'current url', chalk.magenta(url));
|
162
167
|
}),
|
@@ -208,6 +213,7 @@ export async function start(config: Config, options: Options & { browser: string
|
|
208
213
|
retries = test.retries;
|
209
214
|
|
210
215
|
mocha.grep(new RegExp(`^${testPath}$`));
|
216
|
+
mocha.unloadFiles();
|
211
217
|
const runner = mocha.run(runHandler);
|
212
218
|
|
213
219
|
// TODO How handle browser corruption?
|