creevey 0.10.0-beta.1 → 0.10.0-beta.10

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 (137) hide show
  1. package/dist/client/addon/components/Panel.js +2 -2
  2. package/dist/client/addon/components/Panel.js.map +1 -1
  3. package/dist/client/addon/controller.js +4 -5
  4. package/dist/client/addon/controller.js.map +1 -1
  5. package/dist/client/addon/withCreevey.js +18 -34
  6. package/dist/client/addon/withCreevey.js.map +1 -1
  7. package/dist/client/shared/components/ImagesView/SwapView.js +12 -0
  8. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  9. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  10. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  11. package/dist/client/shared/components/ResultsPage.js +23 -5
  12. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  13. package/dist/client/web/CreeveyApp.js +22 -6
  14. package/dist/client/web/CreeveyApp.js.map +1 -1
  15. package/dist/client/web/CreeveyContext.d.ts +5 -0
  16. package/dist/client/web/CreeveyContext.js +3 -0
  17. package/dist/client/web/CreeveyContext.js.map +1 -1
  18. package/dist/client/web/CreeveyView/SideBar/Search.js +2 -2
  19. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  20. package/dist/client/web/CreeveyView/SideBar/SideBar.js +1 -0
  21. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  22. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +49 -6
  23. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  24. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +1 -3
  25. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  26. package/dist/client/web/CreeveyView/SideBar/TestLink.js +1 -3
  27. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  28. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  29. package/dist/client/web/KeyboardEventsContext.js +62 -57
  30. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  31. package/dist/client/web/assets/{index-DkmZfG9C.js → index-BE9CL5_G.js} +94 -94
  32. package/dist/client/web/index.html +1 -1
  33. package/dist/creevey.js +13 -5
  34. package/dist/creevey.js.map +1 -1
  35. package/dist/server/config.js +4 -3
  36. package/dist/server/config.js.map +1 -1
  37. package/dist/server/docker.js +2 -2
  38. package/dist/server/docker.js.map +1 -1
  39. package/dist/server/index.js +29 -3
  40. package/dist/server/index.js.map +1 -1
  41. package/dist/server/logger.d.ts +2 -1
  42. package/dist/server/logger.js +7 -3
  43. package/dist/server/logger.js.map +1 -1
  44. package/dist/server/master/api.js +1 -1
  45. package/dist/server/master/api.js.map +1 -1
  46. package/dist/server/master/pool.d.ts +3 -3
  47. package/dist/server/master/pool.js +10 -63
  48. package/dist/server/master/pool.js.map +1 -1
  49. package/dist/server/master/queue.d.ts +13 -0
  50. package/dist/server/master/queue.js +64 -0
  51. package/dist/server/master/queue.js.map +1 -0
  52. package/dist/server/master/runner.d.ts +1 -0
  53. package/dist/server/master/runner.js +4 -1
  54. package/dist/server/master/runner.js.map +1 -1
  55. package/dist/server/master/server.js +1 -1
  56. package/dist/server/master/server.js.map +1 -1
  57. package/dist/server/master/start.js +4 -4
  58. package/dist/server/master/start.js.map +1 -1
  59. package/dist/server/playwright/docker-file.js +12 -2
  60. package/dist/server/playwright/docker-file.js.map +1 -1
  61. package/dist/server/playwright/internal.d.ts +2 -2
  62. package/dist/server/playwright/internal.js +56 -44
  63. package/dist/server/playwright/internal.js.map +1 -1
  64. package/dist/server/playwright/webdriver.js +1 -1
  65. package/dist/server/playwright/webdriver.js.map +1 -1
  66. package/dist/server/providers/browser.js +2 -1
  67. package/dist/server/providers/browser.js.map +1 -1
  68. package/dist/server/providers/hybrid.js +1 -1
  69. package/dist/server/providers/hybrid.js.map +1 -1
  70. package/dist/server/reporter.js +4 -4
  71. package/dist/server/reporter.js.map +1 -1
  72. package/dist/server/selenium/internal.d.ts +2 -3
  73. package/dist/server/selenium/internal.js +116 -90
  74. package/dist/server/selenium/internal.js.map +1 -1
  75. package/dist/server/selenium/selenoid.js +2 -2
  76. package/dist/server/selenium/selenoid.js.map +1 -1
  77. package/dist/server/selenium/webdriver.js +1 -1
  78. package/dist/server/selenium/webdriver.js.map +1 -1
  79. package/dist/server/telemetry.js +7 -3
  80. package/dist/server/telemetry.js.map +1 -1
  81. package/dist/server/utils.d.ts +2 -1
  82. package/dist/server/utils.js +13 -3
  83. package/dist/server/utils.js.map +1 -1
  84. package/dist/server/webdriver.d.ts +2 -3
  85. package/dist/server/webdriver.js +10 -9
  86. package/dist/server/webdriver.js.map +1 -1
  87. package/dist/server/worker/chai-image.d.ts +1 -2
  88. package/dist/server/worker/chai-image.js +4 -3
  89. package/dist/server/worker/chai-image.js.map +1 -1
  90. package/dist/server/worker/start.js +24 -14
  91. package/dist/server/worker/start.js.map +1 -1
  92. package/dist/types.d.ts +30 -11
  93. package/dist/types.js +13 -1
  94. package/dist/types.js.map +1 -1
  95. package/package.json +36 -42
  96. package/src/client/addon/components/Panel.tsx +2 -2
  97. package/src/client/addon/controller.ts +13 -6
  98. package/src/client/addon/withCreevey.ts +25 -13
  99. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  100. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  101. package/src/client/shared/components/ResultsPage.tsx +28 -7
  102. package/src/client/web/CreeveyApp.tsx +25 -7
  103. package/src/client/web/CreeveyContext.tsx +9 -0
  104. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  105. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  106. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  107. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  108. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  109. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  110. package/src/creevey.ts +13 -6
  111. package/src/server/config.ts +4 -3
  112. package/src/server/docker.ts +2 -2
  113. package/src/server/index.ts +27 -4
  114. package/src/server/logger.ts +6 -2
  115. package/src/server/master/api.ts +1 -1
  116. package/src/server/master/pool.ts +18 -56
  117. package/src/server/master/queue.ts +64 -0
  118. package/src/server/master/runner.ts +4 -1
  119. package/src/server/master/server.ts +1 -1
  120. package/src/server/master/start.ts +7 -4
  121. package/src/server/playwright/docker-file.ts +14 -2
  122. package/src/server/playwright/internal.ts +76 -49
  123. package/src/server/playwright/webdriver.ts +1 -1
  124. package/src/server/providers/browser.ts +2 -1
  125. package/src/server/providers/hybrid.ts +1 -1
  126. package/src/server/reporter.ts +4 -3
  127. package/src/server/selenium/internal.ts +119 -92
  128. package/src/server/selenium/selenoid.ts +2 -2
  129. package/src/server/selenium/webdriver.ts +1 -1
  130. package/src/server/telemetry.ts +7 -3
  131. package/src/server/utils.ts +14 -4
  132. package/src/server/webdriver.ts +10 -15
  133. package/src/server/worker/chai-image.ts +4 -4
  134. package/src/server/worker/start.ts +25 -16
  135. package/src/types.ts +32 -13
  136. package/.yarnrc.yml +0 -1
  137. package/chromatic.config.json +0 -5
package/package.json CHANGED
@@ -1,22 +1,20 @@
1
1
  {
2
2
  "name": "creevey",
3
3
  "description": "Cross-browser screenshot testing tool for Storybook with fancy UI Runner",
4
- "version": "0.10.0-beta.1",
4
+ "version": "0.10.0-beta.10",
5
5
  "type": "commonjs",
6
- "bin": {
7
- "creevey": "dist/cli.js"
8
- },
6
+ "bin": "dist/cli.js",
9
7
  "main": "./dist/index.js",
10
8
  "types": "./dist/index.d.ts",
11
9
  "exports": {
12
10
  ".": "./dist/index.js",
13
11
  "./playwright": {
14
- "default": "./dist/playwright.js",
15
- "types": "./dist/playwright.d.ts"
12
+ "types": "./dist/playwright.d.ts",
13
+ "default": "./dist/playwright.js"
16
14
  },
17
15
  "./selenium": {
18
- "default": "./dist/selenium.js",
19
- "types": "./dist/selenium.d.ts"
16
+ "types": "./dist/selenium.d.ts",
17
+ "default": "./dist/selenium.js"
20
18
  },
21
19
  "./manager": "./dist/client/addon/manager.js",
22
20
  "./preview": "./dist/client/addon/preview.js",
@@ -90,16 +88,9 @@
90
88
  "dependencies": {
91
89
  "@koa/cors": "^5.0.0",
92
90
  "@octokit/core": "^6.1.2",
93
- "@storybook/components": "^8.4.1",
94
- "@storybook/core-events": "^8.4.1",
95
- "@storybook/csf": "^0.1.11",
96
91
  "@storybook/icons": "^1.2.12",
97
- "@storybook/manager-api": "^8.4.1",
98
- "@storybook/preview-api": "^8.4.1",
99
- "@storybook/theming": "^8.4.1",
100
- "@storybook/types": "^8.4.1",
101
92
  "@types/chai": "^4.3.20",
102
- "@types/dockerode": "^3.3.31",
93
+ "@types/dockerode": "^3.3.32",
103
94
  "@types/koa": "^2.15.0",
104
95
  "@types/koa-bodyparser": "^4.3.12",
105
96
  "@types/koa-mount": "^4.0.5",
@@ -110,7 +101,7 @@
110
101
  "@types/minimist": "^1.2.5",
111
102
  "@types/pixelmatch": "^5.2.6",
112
103
  "@types/pngjs": "^6.0.5",
113
- "@types/qs": "^6.9.16",
104
+ "@types/qs": "^6.9.17",
114
105
  "@types/react": "^18.3.12",
115
106
  "@types/react-dom": "^18.3.1",
116
107
  "@types/selenium-webdriver": "^4.1.27",
@@ -131,69 +122,72 @@
131
122
  "loglevel-plugin-prefix": "^0.8.4",
132
123
  "micromatch": "^4.0.8",
133
124
  "minimist": "^1.2.8",
134
- "odiff-bin": "^3.1.2",
125
+ "odiff-bin": "^3.2.0",
135
126
  "pixelmatch": "^6.0.0",
136
127
  "pngjs": "^7.0.0",
137
128
  "polished": "^4.3.1",
138
- "qs": "^6.13.0",
129
+ "qs": "^6.13.1",
139
130
  "semver": "^7.6.3",
140
131
  "shelljs": "^0.8.5",
141
132
  "tar-stream": "^3.1.7",
142
133
  "tsx": "^4.19.2",
143
- "uuid": "^11.0.2",
134
+ "uuid": "^11.0.3",
144
135
  "ws": "^8.18.0",
145
136
  "yocto-spinner": "^0.1.1"
146
137
  },
147
138
  "devDependencies": {
148
139
  "@chromatic-com/storybook": "^3.2.2",
149
- "@eslint/js": "^9.14.0",
150
- "@storybook/addon-essentials": "^8.4.1",
151
- "@storybook/addon-interactions": "^8.4.1",
152
- "@storybook/blocks": "^8.4.1",
153
- "@storybook/channels": "^8.4.1",
154
- "@storybook/client-logger": "^8.4.1",
155
- "@storybook/core-common": "^8.4.1",
156
- "@storybook/core-server": "^8.4.1",
157
- "@storybook/react": "^8.4.1",
158
- "@storybook/react-vite": "^8.4.1",
159
- "@storybook/test": "^8.4.1",
140
+ "@eslint/js": "^9.15.0",
141
+ "@storybook/addon-essentials": "^8.4.5",
142
+ "@storybook/addon-interactions": "^8.4.5",
143
+ "@storybook/blocks": "^8.4.5",
144
+ "@storybook/channels": "^8.4.5",
145
+ "@storybook/components": "^8.4.5",
146
+ "@storybook/csf": "^0.1.11",
147
+ "@storybook/manager-api": "^8.4.5",
148
+ "@storybook/preview-api": "^8.4.5",
149
+ "@storybook/react": "^8.4.5",
150
+ "@storybook/react-vite": "^8.4.5",
151
+ "@storybook/test": "^8.4.5",
152
+ "@storybook/theming": "^8.4.5",
153
+ "@storybook/types": "^8.4.5",
160
154
  "@types/eslint": "^9.6.1",
161
155
  "@types/eslint__js": "^8.42.3",
162
156
  "@types/node": "^18.19.64",
163
157
  "@types/resize-observer-browser": "^0.1.11",
164
- "@types/semver": "^7",
158
+ "@types/semver": "^7.5.8",
165
159
  "@types/tar-stream": "^3.1.3",
166
160
  "@types/tmp": "^0.2.6",
167
161
  "@vitejs/plugin-react-swc": "^3.7.1",
168
162
  "browserstack-local": "^1.5.5",
169
- "chromatic": "^11.16.3",
170
- "concurrently": "^9.0.1",
163
+ "chromatic": "^11.18.1",
164
+ "concurrently": "^9.1.0",
171
165
  "conventional-changelog-cli": "^5.0.0",
172
166
  "dotenv": "^16.4.5",
173
- "eslint": "^9.14.0",
167
+ "eslint": "^9.15.0",
174
168
  "eslint-config-prettier": "^9.1.0",
175
169
  "eslint-plugin-prettier": "^5.2.1",
176
170
  "eslint-plugin-react": "^7.37.2",
177
171
  "eslint-plugin-react-hooks": "^5.0.0",
178
172
  "git-cz": "^4.9.0",
179
- "globals": "^15.11.0",
180
- "husky": "^9.1.6",
173
+ "globals": "^15.12.0",
174
+ "husky": "^9.1.7",
181
175
  "immer": "^10.1.1",
182
176
  "lint-staged": "^15.2.10",
183
177
  "pinst": "^3.0.0",
184
- "playwright-core": "^1.48.0",
178
+ "playwright-core": "^1.49.0",
185
179
  "prettier": "^3.3.3",
186
180
  "react": "^18.3.1",
187
181
  "react-dom": "^18.3.1",
188
182
  "react-is": "^18.3.1",
189
183
  "selenium-webdriver": "^4.26.0",
190
- "storybook": "^8.4.1",
184
+ "storybook": "^8.4.5",
191
185
  "tmp": "^0.2.3",
192
186
  "typescript": "^5.6.3",
193
- "typescript-eslint": "^8.12.2",
187
+ "typescript-eslint": "^8.15.0",
194
188
  "use-immer": "^0.10.0",
195
- "vite": "^5.4.10",
196
- "vitest": "^2.1.4"
189
+ "vite": "^5.4.11",
190
+ "vitest": "^2.1.5"
197
191
  },
198
192
  "config": {
199
193
  "commitizen": {
@@ -1,11 +1,11 @@
1
1
  import React, { JSX } from 'react';
2
2
  import { Loader } from '@storybook/components';
3
3
  import { styled } from '@storybook/theming';
4
- import { TestData } from '../../../types.js';
4
+ import { noop, TestData } from '../../../types.js';
5
5
  import { ResultsPage } from '../../shared/components/ResultsPage.js';
6
6
  import { getTestPath } from '../../shared/helpers.js';
7
7
  import TestSelect from './TestSelect.js';
8
- import { noop } from 'lodash';
8
+
9
9
  interface PanelProps {
10
10
  tests: TestData[];
11
11
  selectedTestId: string;
@@ -1,7 +1,14 @@
1
1
  import { Addon_TypesEnum } from '@storybook/types';
2
- import { SET_STORIES, STORY_RENDERED } from '@storybook/core-events';
3
2
  import { denormalizeStoryParameters } from '../../shared/index.js';
4
- import { CreeveyStatus, CreeveyUpdate, isDefined, TestData, TestStatus, StoriesRaw } from '../../types.js';
3
+ import {
4
+ CreeveyStatus,
5
+ CreeveyUpdate,
6
+ isDefined,
7
+ TestData,
8
+ TestStatus,
9
+ StoriesRaw,
10
+ StorybookEvents,
11
+ } from '../../types.js';
5
12
  import { initCreeveyClientApi, CreeveyClientApi } from '../shared/creeveyClientApi.js';
6
13
  import { calcStatus } from '../shared/helpers.js';
7
14
  import { getEmojiByTestStatus } from './utils.js';
@@ -25,8 +32,8 @@ export class CreeveyController {
25
32
  this.storybookApi = storybookApi;
26
33
  }
27
34
  initAll = async (): Promise<void> => {
28
- this.storybookApi.on(STORY_RENDERED, this.onStoryRendered);
29
- this.storybookApi.on(SET_STORIES, this.onSetStories);
35
+ this.storybookApi.on(StorybookEvents.STORY_RENDERED, this.onStoryRendered);
36
+ this.storybookApi.on(StorybookEvents.SET_STORIES, this.onSetStories);
30
37
  this.creeveyApi = await initCreeveyClientApi();
31
38
  this.creeveyApi.onUpdate(this.handleCreeveyUpdate);
32
39
  this.status = await this.creeveyApi.status;
@@ -93,7 +100,7 @@ export class CreeveyController {
93
100
  this.stories = prevStories;
94
101
  this.setPanelsTitle();
95
102
  // TODO Check setStories method in 6.x and migrate properly
96
- this.storybookApi.emit(SET_STORIES, this.stories);
103
+ this.storybookApi.emit(StorybookEvents.SET_STORIES, this.stories);
97
104
  }
98
105
  this.updateStatusListeners.forEach((x) => {
99
106
  x(update);
@@ -213,7 +220,7 @@ export class CreeveyController {
213
220
  });
214
221
 
215
222
  // TODO Check setStories method in 6.x and migrate properly
216
- this.storybookApi.emit(SET_STORIES, this.stories);
223
+ this.storybookApi.emit(StorybookEvents.SET_STORIES, this.stories);
217
224
  }
218
225
 
219
226
  addStatusToStoryName(name: string, status: TestStatus | undefined, skip: string | boolean): string {
@@ -1,10 +1,18 @@
1
- import * as Events from '@storybook/core-events';
2
1
  import type { Renderer, StoryContextForEnhancers } from '@storybook/csf';
3
2
  import { makeDecorator, PreviewWeb, StoryStore } from '@storybook/preview-api';
4
3
  import { Channel } from '@storybook/channels';
5
- import { CaptureOptions, CreeveyStoryParams, isObject, noop, StoriesRaw, StorybookGlobals } from '../../types.js';
4
+ import {
5
+ CaptureOptions,
6
+ CreeveyStoryParams,
7
+ isObject,
8
+ noop,
9
+ StoriesRaw,
10
+ StorybookEvents,
11
+ StorybookGlobals,
12
+ } from '../../types.js';
6
13
  import { serializeRawStories } from '../../shared/index.js';
7
14
  import { getConnectionUrl } from '../shared/helpers.js';
15
+ import isEqual from 'lodash/isEqual.js';
8
16
 
9
17
  declare global {
10
18
  interface Window {
@@ -62,12 +70,12 @@ function catchRenderError(channel: Channel): Promise<void> & { cancel: () => voi
62
70
  rejectCallback(exception);
63
71
  }
64
72
  function removeHandlers(): void {
65
- channel.off(Events.STORY_ERRORED, errorHandler);
66
- channel.off(Events.STORY_THREW_EXCEPTION, errorHandler);
73
+ channel.off(StorybookEvents.STORY_ERRORED, errorHandler);
74
+ channel.off(StorybookEvents.STORY_THREW_EXCEPTION, errorHandler);
67
75
  }
68
76
 
69
- channel.once(Events.STORY_ERRORED, errorHandler);
70
- channel.once(Events.STORY_THREW_EXCEPTION, exceptionHandler);
77
+ channel.once(StorybookEvents.STORY_ERRORED, errorHandler);
78
+ channel.once(StorybookEvents.STORY_THREW_EXCEPTION, exceptionHandler);
71
79
 
72
80
  return Object.assign(promise, { cancel: removeHandlers });
73
81
  }
@@ -79,10 +87,10 @@ function waitForStoryRendered(channel: Channel): Promise<void> & { cancel: () =>
79
87
  resolveCallback();
80
88
  }
81
89
  function removeHandlers(): void {
82
- channel.off(Events.STORY_RENDERED, renderHandler);
90
+ channel.off(StorybookEvents.STORY_RENDERED, renderHandler);
83
91
  }
84
92
 
85
- channel.once(Events.STORY_RENDERED, renderHandler);
93
+ channel.once(StorybookEvents.STORY_RENDERED, renderHandler);
86
94
 
87
95
  return Object.assign(promise, { cancel: removeHandlers });
88
96
  }
@@ -132,6 +140,7 @@ let captureResolver: () => void;
132
140
  let waitForCreevey: Promise<void>;
133
141
  let creeveyReady: () => void;
134
142
  let setStoriesCounter = 0;
143
+ let globals = {};
135
144
 
136
145
  export function withCreevey(): ReturnType<typeof makeDecorator> {
137
146
  const addonsChannel = (): Channel => window.__STORYBOOK_ADDONS_CHANNEL__;
@@ -166,7 +175,7 @@ export function withCreevey(): ReturnType<typeof makeDecorator> {
166
175
  return stories;
167
176
  }
168
177
 
169
- // TODO Use Events.STORY_RENDER_PHASE_CHANGED: `loading/rendering/completed` with storyId
178
+ // TODO Use StorybookEvents.STORY_RENDER_PHASE_CHANGED: `loading/rendering/completed` with storyId
170
179
  // TODO Check other statuses and statuses with play function
171
180
  async function selectStory(
172
181
  storyId: string,
@@ -188,8 +197,8 @@ export function withCreevey(): ReturnType<typeof makeDecorator> {
188
197
  const capturePromise = waitForCaptureCall().then(() => (isCaptureCalled = true));
189
198
 
190
199
  setTimeout(() => {
191
- if (storyId == currentStory) channel.emit(Events.FORCE_REMOUNT, { storyId });
192
- else channel.emit(Events.SET_CURRENT_STORY, { storyId });
200
+ if (storyId == currentStory) channel.emit(StorybookEvents.FORCE_REMOUNT, { storyId });
201
+ else channel.emit(StorybookEvents.SET_CURRENT_STORY, { storyId });
193
202
  }, 0);
194
203
 
195
204
  try {
@@ -218,8 +227,11 @@ export function withCreevey(): ReturnType<typeof makeDecorator> {
218
227
  }
219
228
  }
220
229
 
221
- function updateGlobals(globals: StorybookGlobals): void {
222
- addonsChannel().emit(Events.UPDATE_GLOBALS, { globals });
230
+ function updateGlobals(newGlobals: StorybookGlobals): void {
231
+ if (isEqual(globals, newGlobals)) return;
232
+
233
+ globals = newGlobals;
234
+ addonsChannel().emit(StorybookEvents.UPDATE_GLOBALS, { globals });
223
235
  }
224
236
 
225
237
  function insertIgnoreStyles(ignoreSelectors: string[]): HTMLStyleElement {
@@ -46,6 +46,24 @@ export const SwapView = withTheme(({ theme, expect, actual, diff }: ViewPropsWit
46
46
  setImage((prevImage) => (prevImage == 'actual' ? 'expect' : 'actual'));
47
47
  }, []);
48
48
 
49
+ const handleKeyDown = useCallback(
50
+ (e: KeyboardEvent) => {
51
+ if (e.code === 'Space' && e.altKey) {
52
+ e.preventDefault();
53
+ handleChangeView();
54
+ }
55
+ },
56
+ [handleChangeView],
57
+ );
58
+
59
+ useEffect(() => {
60
+ document.addEventListener('keydown', handleKeyDown, false);
61
+
62
+ return () => {
63
+ document.removeEventListener('keydown', handleKeyDown, false);
64
+ };
65
+ }, [handleKeyDown]);
66
+
49
67
  useEffect(() => {
50
68
  if (loaded) readyForCapture();
51
69
  }, [loaded]);
@@ -70,6 +70,7 @@ export const ImagePreview = withTheme(
70
70
  onClick(imageName);
71
71
  };
72
72
 
73
+ // TODO Add image name as a title
73
74
  return (
74
75
  <Button
75
76
  onClick={handleClick}
@@ -1,11 +1,11 @@
1
- import React, { JSX, useState } from 'react';
1
+ import React, { JSX, useCallback, useEffect, 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';
5
5
  import { PageHeader } from './PageHeader/PageHeader.js';
6
6
  import { PageFooter } from './PageFooter/PageFooter.js';
7
7
  import { getImageUrl } from '../helpers.js';
8
- import { getViewMode, VIEW_MODE_KEY } from '../viewMode.js';
8
+ import { getViewMode, VIEW_MODE_KEY, viewModes } from '../viewMode.js';
9
9
  import { ImagesViewMode, TestResult } from '../../../types.js';
10
10
 
11
11
  interface ResultsPageProps {
@@ -77,10 +77,31 @@ export function ResultsPageInternal({
77
77
  )
78
78
  : [];
79
79
 
80
- const handleChangeViewMode = (mode: ImagesViewMode): void => {
81
- localStorage.setItem(VIEW_MODE_KEY, mode);
82
- setViewMode(mode);
83
- };
80
+ const handleKeyDown = useCallback(
81
+ (e: KeyboardEvent) => {
82
+ if (!canApprove) return;
83
+ if (e.code === 'Tab') {
84
+ e.preventDefault();
85
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
86
+ if (e.shiftKey) setViewMode((mode) => viewModes.at((viewModes.indexOf(mode) - 1) % viewModes.length)!);
87
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
88
+ else setViewMode((mode) => viewModes.at((viewModes.indexOf(mode) + 1) % viewModes.length)!);
89
+ }
90
+ },
91
+ [canApprove],
92
+ );
93
+
94
+ useEffect(() => {
95
+ localStorage.setItem(VIEW_MODE_KEY, viewMode);
96
+ }, [viewMode]);
97
+
98
+ useEffect(() => {
99
+ document.addEventListener('keydown', handleKeyDown, false);
100
+
101
+ return () => {
102
+ document.removeEventListener('keydown', handleKeyDown, false);
103
+ };
104
+ }, [handleKeyDown]);
84
105
 
85
106
  return (
86
107
  <Container height={height}>
@@ -92,7 +113,7 @@ export function ResultsPageInternal({
92
113
  errorMessage={result.error}
93
114
  showViewModes={hasDiffAndExpect}
94
115
  viewMode={viewMode}
95
- onViewModeChange={handleChangeViewMode}
116
+ onViewModeChange={setViewMode}
96
117
  onImageChange={onImageChange}
97
118
  imagesWithError={imagesWithError}
98
119
  />
@@ -17,7 +17,7 @@ import {
17
17
  CreeveyViewFilter,
18
18
  getFailedTests,
19
19
  } from '../shared/helpers.js';
20
- import { CreeveyContext } from './CreeveyContext.js';
20
+ import { CreeveyContext, FocusableItem } from './CreeveyContext.js';
21
21
  import { KeyboardEvents } from './KeyboardEventsContext.js';
22
22
  import { SideBar } from './CreeveyView/SideBar/index.js';
23
23
  import { ResultsPage } from '../shared/components/ResultsPage.js';
@@ -59,8 +59,9 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
59
59
  const failedTests = useMemo(() => getFailedTests(tests), [tests]);
60
60
 
61
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] ?? '');
62
+ const result = useMemo(() => openedTest?.results?.[retry - 1], [openedTest, retry]);
63
+ const [imageName, setImageName] = useState(Object.keys(result?.images ?? {})[0] ?? '');
64
+ const [sidebarFocusedItem, setSidebarFocusedItem] = useState<FocusableItem>([]);
64
65
  const canApprove = useMemo(
65
66
  () =>
66
67
  Boolean(
@@ -94,6 +95,7 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
94
95
  (test: CreeveyTest): void => {
95
96
  const testPath = getTestPath(test);
96
97
  setSearchParams(testPath);
98
+ setSidebarFocusedItem(testPath);
97
99
  updateTests((draft) => {
98
100
  openSuite(draft, testPath, true);
99
101
  openTest(testPath);
@@ -103,11 +105,24 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
103
105
  );
104
106
 
105
107
  const handleGoToNextFailedTest = useCallback(() => {
106
- if (failedTests.length <= 1) return;
108
+ if (failedTests.length == 0) return;
107
109
  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]);
110
+ const failedImages = Object.entries(result?.images ?? {})
111
+ .filter(([name, image]) =>
112
+ // TODO Move to helpers, it duplicates in a few places
113
+ Boolean(image?.error != null && openedTest?.approved?.[name] != retry - 1 && result?.status != 'success'),
114
+ )
115
+ .map(([name]) => name);
116
+ if (
117
+ failedImages.length > 1 &&
118
+ (failedTests.length == 1 || failedImages.indexOf(imageName) < failedImages.length - 1)
119
+ ) {
120
+ setImageName((name) => failedImages[failedImages.indexOf(name) + 1] ?? failedImages[0]);
121
+ } else {
122
+ const nextFailedTest = failedTests[currentTest + 1] ?? failedTests[0];
123
+ handleOpenTest(nextFailedTest);
124
+ }
125
+ }, [failedTests, handleOpenTest, openedTest, retry, result, imageName]);
111
126
 
112
127
  const handleImageApproveNew = useCallback((): void => {
113
128
  const id = openedTest?.id;
@@ -189,12 +204,15 @@ export function CreeveyApp({ api, initialState }: CreeveyAppProps): JSX.Element
189
204
  value={{
190
205
  isReport: initialState.isReport,
191
206
  isRunning,
207
+ onImageNext: canApprove ? handleGoToNextFailedTest : undefined,
192
208
  onImageApprove: canApprove ? handleImageApproveAndGoNext : undefined,
193
209
  onApproveAll: handleApproveAll,
194
210
  onStart: handleStart,
195
211
  onStop: handleStop,
196
212
  onSuiteOpen: handleSuiteOpen,
197
213
  onSuiteToggle: handleSuiteToggle,
214
+ sidebarFocusedItem,
215
+ setSidebarFocusedItem,
198
216
  }}
199
217
  >
200
218
  <ThemeProvider theme={ensure(themes[theme])}>
@@ -1,26 +1,35 @@
1
1
  import React, { useContext } from 'react';
2
2
  import { CreeveySuite, noop } from '../../types.js';
3
3
 
4
+ export type SuitePath = string[];
5
+ export type FocusableItem = null | SuitePath;
6
+
4
7
  export interface CreeveyContextType {
5
8
  isReport: boolean;
6
9
  isRunning: boolean;
7
10
  onStop: () => void;
11
+ onImageNext?: () => void;
8
12
  onImageApprove?: () => void;
9
13
  onApproveAll: () => void;
10
14
  onStart: (rootSuite: CreeveySuite) => void;
11
15
  onSuiteOpen: (path: string[], opened: boolean) => void;
12
16
  onSuiteToggle: (path: string[], checked: boolean) => void;
17
+ sidebarFocusedItem: FocusableItem;
18
+ setSidebarFocusedItem: (item: FocusableItem) => void;
13
19
  }
14
20
 
15
21
  export const CreeveyContext = React.createContext<CreeveyContextType>({
16
22
  isReport: true,
17
23
  isRunning: false,
24
+ onImageNext: noop,
18
25
  onImageApprove: noop,
19
26
  onApproveAll: noop,
20
27
  onStop: noop,
21
28
  onStart: noop,
22
29
  onSuiteOpen: noop,
23
30
  onSuiteToggle: noop,
31
+ sidebarFocusedItem: [],
32
+ setSidebarFocusedItem: noop,
24
33
  });
25
34
 
26
35
  export const useCreeveyContext = () => useContext(CreeveyContext);
@@ -1,7 +1,7 @@
1
- import React, { JSX, ChangeEvent, useContext, useRef, useState } from 'react';
1
+ import React, { JSX, ChangeEvent, useRef, useState } from 'react';
2
2
  import { SearchIcon, CloseAltIcon } from '@storybook/icons';
3
3
  import { styled, Theme, withTheme } from '@storybook/theming';
4
- import { KeyboardEventsContext } from '../../KeyboardEventsContext.js';
4
+ import { useCreeveyContext } from '../../CreeveyContext.js';
5
5
 
6
6
  interface SearchProps {
7
7
  onChange: (arg: string) => void;
@@ -100,7 +100,7 @@ const FilterForm = withTheme(
100
100
  );
101
101
 
102
102
  export const Search = ({ onChange, value }: SearchProps): JSX.Element => {
103
- const { setSidebarFocusedItem } = useContext(KeyboardEventsContext);
103
+ const { setSidebarFocusedItem } = useCreeveyContext();
104
104
  const [focussed, onSetFocussed] = useState(false);
105
105
  const searchRef = useRef<HTMLInputElement>(null);
106
106
 
@@ -62,6 +62,7 @@ const SelectAllContainer = styled.div({
62
62
  });
63
63
 
64
64
  const TestsContainer = styled.div({
65
+ marginBottom: '8px',
65
66
  position: 'relative',
66
67
  height: '100%',
67
68
  });
@@ -1,4 +1,4 @@
1
- import React, { JSX } from 'react';
1
+ import React, { JSX, useCallback, useEffect, useState } from 'react';
2
2
  import { styled, withTheme } from '@storybook/theming';
3
3
  import { Button } from '@storybook/components';
4
4
  import { ChevronRightIcon } from '@storybook/icons';
@@ -21,15 +21,46 @@ const Container = styled.div({
21
21
  });
22
22
 
23
23
  export function SideBarFooter(): JSX.Element {
24
- const { onApproveAll, onImageApprove } = useCreeveyContext();
24
+ const { onApproveAll, onImageApprove, onImageNext } = useCreeveyContext();
25
+ const [isAlt, setIsAlt] = useState(false);
26
+
27
+ const handleKeyDown = useCallback((e: KeyboardEvent) => {
28
+ if (e.code === 'AltLeft') {
29
+ e.preventDefault();
30
+ setIsAlt(true);
31
+ }
32
+ }, []);
33
+ const handleKeyUp = useCallback((e: KeyboardEvent) => {
34
+ if (e.code === 'AltLeft') {
35
+ e.preventDefault();
36
+ setIsAlt(false);
37
+ }
38
+ }, []);
39
+
40
+ useEffect(() => {
41
+ document.addEventListener('keydown', handleKeyDown, false);
42
+ document.addEventListener('keyup', handleKeyUp, false);
43
+
44
+ return () => {
45
+ document.removeEventListener('keydown', handleKeyDown, false);
46
+ document.removeEventListener('keyup', handleKeyUp, false);
47
+ };
48
+ }, [handleKeyDown, handleKeyUp]);
25
49
 
26
50
  return (
27
51
  <Sticky>
28
52
  <Container>
29
- <Button variant="solid" size="medium" onClick={onImageApprove} disabled={!onImageApprove}>
30
- Approve
31
- <ChevronRightIcon />
32
- </Button>
53
+ {isAlt ? (
54
+ <Button variant="outline" size="medium" onClick={onImageNext} disabled={!onImageApprove}>
55
+ Next
56
+ <ChevronRightIcon />
57
+ </Button>
58
+ ) : (
59
+ <Button variant="solid" size="medium" onClick={onImageApprove} disabled={!onImageApprove}>
60
+ Approve
61
+ <ChevronRightIcon />
62
+ </Button>
63
+ )}
33
64
  <Button variant="outline" size="medium" onClick={onApproveAll}>
34
65
  Approve all
35
66
  </Button>
@@ -1,11 +1,10 @@
1
- import React, { JSX, useRef, useContext, useEffect, useMemo } from 'react';
1
+ import React, { JSX, useRef, useEffect, useMemo } from 'react';
2
2
  import { ChevronDownIcon, ChevronRightIcon } from '@storybook/icons';
3
3
  import { styled, withTheme, Theme } from '@storybook/theming';
4
4
  import { Checkbox, CheckboxContainer } from './Checkbox.js';
5
5
  import { TestStatusIcon } from './TestStatusIcon.js';
6
6
  import { CreeveySuite, isTest } from '../../../../types.js';
7
- import { CreeveyContext } from '../../CreeveyContext.js';
8
- import { KeyboardEventsContext } from '../../KeyboardEventsContext.js';
7
+ import { useCreeveyContext } from '../../CreeveyContext.js';
9
8
 
10
9
  export interface SuiteLinkProps {
11
10
  title: string;
@@ -79,8 +78,7 @@ export const SuiteTitle = styled.span({
79
78
  });
80
79
 
81
80
  export function SuiteLink({ title, suite, 'data-testid': dataTid }: SuiteLinkProps): JSX.Element {
82
- const { onSuiteOpen, onSuiteToggle } = useContext(CreeveyContext);
83
- const { sidebarFocusedItem, setSidebarFocusedItem } = useContext(KeyboardEventsContext);
81
+ const { onSuiteOpen, onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem } = useCreeveyContext();
84
82
  const checkboxRef = useRef<Checkbox>(null);
85
83
  const buttonRef = useRef<HTMLButtonElement | null>(null);
86
84
 
@@ -1,9 +1,8 @@
1
1
  import React, { JSX, useEffect, useCallback, useContext, useMemo, useRef } from 'react';
2
2
  import { CreeveyTest } from '../../../../types.js';
3
3
  import { TestStatusIcon } from './TestStatusIcon.js';
4
- import { CreeveyContext } from '../../CreeveyContext.js';
4
+ import { useCreeveyContext } from '../../CreeveyContext.js';
5
5
  import { SideBarContext } from './SideBar.js';
6
- import { KeyboardEventsContext } from '../../KeyboardEventsContext.js';
7
6
  import { Button, Container, SuiteContainer, SuiteTitle } from './SuiteLink.js';
8
7
  import { Checkbox, CheckboxContainer } from './Checkbox.js';
9
8
  import { getTestPath } from '../../../shared/helpers.js';
@@ -20,9 +19,8 @@ const TestContainer = styled(SuiteContainer)({
20
19
  });
21
20
 
22
21
  export function TestLink({ title, opened, test }: TestLinkProps): JSX.Element {
23
- const { onSuiteToggle } = useContext(CreeveyContext);
22
+ const { onSuiteToggle, sidebarFocusedItem, setSidebarFocusedItem } = useCreeveyContext();
24
23
  const { onOpenTest } = useContext(SideBarContext);
25
- const { sidebarFocusedItem, setSidebarFocusedItem } = useContext(KeyboardEventsContext);
26
24
  const buttonRef = useRef<HTMLButtonElement | null>(null);
27
25
 
28
26
  const emptyResults = (test.results?.length ?? 0) == 0;