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.
Files changed (87) hide show
  1. package/dist/client/addon/components/Panel.d.ts +1 -1
  2. package/dist/client/addon/components/Panel.js +5 -2
  3. package/dist/client/addon/components/Panel.js.map +1 -1
  4. package/dist/client/addon/withCreevey.js +1 -1
  5. package/dist/client/addon/withCreevey.js.map +1 -1
  6. package/dist/client/shared/components/PageFooter/PageFooter.d.ts +1 -3
  7. package/dist/client/shared/components/PageFooter/PageFooter.js +3 -8
  8. package/dist/client/shared/components/PageFooter/PageFooter.js.map +1 -1
  9. package/dist/client/shared/components/PageFooter/Paging.d.ts +1 -1
  10. package/dist/client/shared/components/PageFooter/Paging.js +4 -21
  11. package/dist/client/shared/components/PageFooter/Paging.js.map +1 -1
  12. package/dist/client/shared/components/PageHeader/PageHeader.d.ts +2 -2
  13. package/dist/client/shared/components/PageHeader/PageHeader.js +17 -10
  14. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  15. package/dist/client/shared/components/ResultsPage.d.ts +8 -6
  16. package/dist/client/shared/components/ResultsPage.js +5 -13
  17. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  18. package/dist/client/shared/creeveyClientApi.d.ts +1 -0
  19. package/dist/client/shared/creeveyClientApi.js +3 -0
  20. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  21. package/dist/client/shared/helpers.d.ts +2 -1
  22. package/dist/client/shared/helpers.js +23 -8
  23. package/dist/client/shared/helpers.js.map +1 -1
  24. package/dist/client/web/CreeveyApp.js +45 -8
  25. package/dist/client/web/CreeveyApp.js.map +1 -1
  26. package/dist/client/web/CreeveyContext.d.ts +3 -0
  27. package/dist/client/web/CreeveyContext.js +28 -4
  28. package/dist/client/web/CreeveyContext.js.map +1 -1
  29. package/dist/client/web/CreeveyView/SideBar/Checkbox.d.ts +1 -1
  30. package/dist/client/web/CreeveyView/SideBar/Checkbox.js +5 -5
  31. package/dist/client/web/CreeveyView/SideBar/Checkbox.js.map +1 -1
  32. package/dist/client/web/CreeveyView/SideBar/SideBar.d.ts +2 -2
  33. package/dist/client/web/CreeveyView/SideBar/SideBar.js +23 -13
  34. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  35. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.d.ts +1 -0
  36. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +32 -0
  37. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -0
  38. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +1 -1
  39. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +8 -3
  40. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +38 -23
  41. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  42. package/dist/client/web/CreeveyView/SideBar/TestLink.js +6 -5
  43. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  44. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js +13 -6
  45. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js.map +1 -1
  46. package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
  47. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +6 -4
  48. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
  49. package/dist/client/web/main.js +8 -8
  50. package/dist/server/master/api.js +4 -0
  51. package/dist/server/master/api.js.map +1 -1
  52. package/dist/server/master/pool.js +4 -2
  53. package/dist/server/master/pool.js.map +1 -1
  54. package/dist/server/master/runner.d.ts +2 -0
  55. package/dist/server/master/runner.js +61 -9
  56. package/dist/server/master/runner.js.map +1 -1
  57. package/dist/server/selenium/browser.js +9 -6
  58. package/dist/server/selenium/browser.js.map +1 -1
  59. package/dist/server/worker/worker.js +18 -12
  60. package/dist/server/worker/worker.js.map +1 -1
  61. package/dist/types.d.ts +5 -2
  62. package/dist/types.js.map +1 -1
  63. package/package.json +2 -1
  64. package/src/client/addon/components/Panel.tsx +7 -3
  65. package/src/client/addon/withCreevey.ts +1 -1
  66. package/src/client/shared/components/PageFooter/PageFooter.tsx +2 -20
  67. package/src/client/shared/components/PageFooter/Paging.tsx +22 -37
  68. package/src/client/shared/components/PageHeader/PageHeader.tsx +20 -14
  69. package/src/client/shared/components/ResultsPage.tsx +18 -31
  70. package/src/client/shared/creeveyClientApi.ts +4 -0
  71. package/src/client/shared/helpers.ts +22 -8
  72. package/src/client/web/CreeveyApp.tsx +66 -13
  73. package/src/client/web/CreeveyContext.tsx +7 -1
  74. package/src/client/web/CreeveyView/SideBar/Checkbox.tsx +5 -5
  75. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +29 -18
  76. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +38 -0
  77. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +1 -1
  78. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +50 -31
  79. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
  80. package/src/client/web/CreeveyView/SideBar/TestStatusIcon.tsx +12 -6
  81. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +7 -3
  82. package/src/server/master/api.ts +4 -0
  83. package/src/server/master/pool.ts +5 -2
  84. package/src/server/master/runner.ts +67 -9
  85. package/src/server/selenium/browser.ts +13 -10
  86. package/src/server/worker/worker.ts +17 -11
  87. package/src/types.ts +5 -3
@@ -1,7 +1,7 @@
1
- import React, { createContext, useContext } from 'react';
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 { CreeveyContext } from '../../CreeveyContext.js';
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 - 165px)',
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: '40px',
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: 3,
81
+ zIndex: 4,
74
82
  background: theme.background.content,
75
83
  })),
76
84
  );
77
85
 
78
- export function SideBar({ rootSuite, openedTest, onOpenTest, filter, setFilter }: SideBarProps): JSX.Element {
79
- const { onStart, onStop } = useContext(CreeveyContext);
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
- <ScrollArea vertical>
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 == openedTest?.id} test={suite} />
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
- </ScrollArea>
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
+ }
@@ -18,7 +18,7 @@ interface SideBarHeaderProps {
18
18
 
19
19
  const Sticky = withTheme(
20
20
  styled.div(({ theme }) => ({
21
- padding: '24px 32px 8px',
21
+ padding: '24px 36px 8px',
22
22
  background: theme.background.content,
23
23
  height: '130px',
24
24
  zIndex: 5,
@@ -14,49 +14,67 @@ export interface SuiteLinkProps {
14
14
  }
15
15
 
16
16
  export const Container = withTheme(
17
- styled.div<{ theme: Theme; disabled?: boolean }>(({ theme, disabled }) => ({
18
- position: 'relative',
19
- width: '100%',
20
- ...(disabled ? { color: theme.color.mediumdark, pointerEvents: 'none' } : {}),
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; focused?: boolean }>(({ theme, active, focused }) => ({
26
- width: '100%',
40
+ styled.button<{ theme: Theme; active?: boolean }>(({ theme, active }) => ({
41
+ flexGrow: 1,
27
42
  boxSizing: 'border-box',
28
43
  appearance: 'none',
29
- padding: '6px 36px',
30
- lineHeight: '20px',
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: active ? theme.color.secondary : focused ? theme.background.hoverable : 'none',
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: '8px',
57
+ paddingRight: '4px',
50
58
  display: 'inline-block',
51
- width: '16px',
52
- height: '11px',
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: 10,
15
- height: 10,
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: '1.5px',
22
+ top: '1px',
23
23
  verticalAlign: 'baseline',
24
24
  });
25
25
 
26
26
  const Spinner = styled(Loader)({
27
- top: '14px',
28
- left: '42px',
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="timer" />;
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
- skippedCount,
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="timer" /> {skippedCount}
103
+ <Icons icon="thumbsup" /> {approvedCount}
100
104
  </IconContainer>
101
105
  </Button>
102
106
  </Container>
@@ -49,6 +49,10 @@ export default function creeveyApi(runner: Runner): CreeveyApi {
49
49
  void runner.approve(command.payload);
50
50
  return;
51
51
  }
52
+ case 'approveAll': {
53
+ void runner.approveAll();
54
+ return;
55
+ }
52
56
  }
53
57
  },
54
58
  };
@@ -96,11 +96,13 @@ export default class Pool extends EventEmitter {
96
96
  }
97
97
 
98
98
  private getFreeWorker(): Worker | undefined {
99
- return this.freeWorkers[Math.floor(Math.random() * this.freeWorkers.length)];
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: { [id]: { id, browser, testName, storyPath, status: test.status, results: [result], storyId } },
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
- const restPath = [...storyPath, testName].filter(isDefined);
176
- const testPath = path.join(...restPath, image == browser ? '' : browser);
177
- const srcImagePath = path.join(this.reportDir, testPath, images.actual);
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: test.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<string> {
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
- const creeveyHost = await resolveCreeveyHost(browser, options.port);
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
- creeveyHost,
703
- options.port,
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 { closeBrowser, getBrowser, switchStory } from '../selenium/index.js';
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
- if ((await getBrowser(config, options)) == null) return;
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
- try {
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?