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.
Files changed (84) 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/runner.d.ts +2 -0
  53. package/dist/server/master/runner.js +59 -9
  54. package/dist/server/master/runner.js.map +1 -1
  55. package/dist/server/selenium/browser.js +9 -6
  56. package/dist/server/selenium/browser.js.map +1 -1
  57. package/dist/server/worker/worker.js +1 -0
  58. package/dist/server/worker/worker.js.map +1 -1
  59. package/dist/types.d.ts +4 -2
  60. package/dist/types.js.map +1 -1
  61. package/package.json +2 -1
  62. package/src/client/addon/components/Panel.tsx +7 -3
  63. package/src/client/addon/withCreevey.ts +1 -1
  64. package/src/client/shared/components/PageFooter/PageFooter.tsx +2 -20
  65. package/src/client/shared/components/PageFooter/Paging.tsx +22 -37
  66. package/src/client/shared/components/PageHeader/PageHeader.tsx +20 -14
  67. package/src/client/shared/components/ResultsPage.tsx +18 -31
  68. package/src/client/shared/creeveyClientApi.ts +4 -0
  69. package/src/client/shared/helpers.ts +22 -8
  70. package/src/client/web/CreeveyApp.tsx +66 -13
  71. package/src/client/web/CreeveyContext.tsx +7 -1
  72. package/src/client/web/CreeveyView/SideBar/Checkbox.tsx +5 -5
  73. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +29 -18
  74. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +38 -0
  75. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +1 -1
  76. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +50 -31
  77. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +8 -6
  78. package/src/client/web/CreeveyView/SideBar/TestStatusIcon.tsx +12 -6
  79. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +7 -3
  80. package/src/server/master/api.ts +4 -0
  81. package/src/server/master/runner.ts +65 -9
  82. package/src/server/selenium/browser.ts +13 -10
  83. package/src/server/worker/worker.ts +1 -0
  84. package/src/types.ts +4 -3
@@ -1,7 +1,5 @@
1
1
  import React from 'react';
2
- import { Button, Icons } from '@storybook/components';
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 | '.' | 'forward';
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
- <StyledButton disabled key={`dots${index < 5 ? 'Left' : 'Right'}`}>
25
- {'...'}
26
- </StyledButton>
27
- );
28
- }
29
- case 'forward': {
30
- const disabled = props.activePage === props.pagesCount;
31
- return (
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
- <span>
45
- Next <Icons icon="arrowright" />
46
- </span>
47
- </StyledButton>
26
+ {'...'}
27
+ </TabButton>
48
28
  );
49
29
  }
30
+
50
31
  default: {
51
32
  return (
52
- <StyledButton
53
- outline
54
- secondary={props.activePage === item}
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
- </StyledButton>
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.concat('forward');
89
+ return result;
105
90
  }
@@ -1,4 +1,4 @@
1
- import React, { useState } from '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 showTitle || error || imageEntires.length > 1 || showViewModes ? (
97
+ return (
90
98
  <Container>
91
- {showTitle && (
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={handleImageChange}
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
- ) : null;
128
+ );
123
129
  }
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
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 TestResultsProps {
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
- }: TestResultsProps): JSX.Element {
64
- const [retry, setRetry] = useState(results.length);
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={setImageName}
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
- skippedCount: number;
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 (isDefined(approved))
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 skippedCount = 0;
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.skip) skippedCount++;
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, skippedCount, pendingCount };
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
- const handleImageApprove = useCallback(
77
- (id: string, retry: number, image: string): void => api?.approve(id, retry, image),
78
- [api],
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
- const handleOpenTest = useCallback((test: CreeveyTest): void => {
92
- const testPath = getTestPath(test);
93
- setSearchParams(testPath);
94
- openTest(testPath);
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
- openedTest={openedTest}
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
- showTitle
166
- onImageApprove={handleImageApprove}
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.div({
74
- position: 'absolute',
75
- left: '64px',
76
- top: '6px',
77
- zIndex: 2,
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, 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
  );