creevey 0.10.0-beta.4 → 0.10.0-beta.40

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 (211) hide show
  1. package/README.md +19 -41
  2. package/dist/client/addon/components/Addon.js +17 -7
  3. package/dist/client/addon/components/Addon.js.map +1 -1
  4. package/dist/client/addon/components/Panel.js +2 -2
  5. package/dist/client/addon/components/Panel.js.map +1 -1
  6. package/dist/client/addon/components/Tools.js +17 -7
  7. package/dist/client/addon/components/Tools.js.map +1 -1
  8. package/dist/client/addon/withCreevey.d.ts +2 -1
  9. package/dist/client/addon/withCreevey.js +11 -1
  10. package/dist/client/addon/withCreevey.js.map +1 -1
  11. package/dist/client/shared/components/ImagesView/BlendView.d.ts +1 -1
  12. package/dist/client/shared/components/ImagesView/BlendView.js +17 -7
  13. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  14. package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +1 -1
  15. package/dist/client/shared/components/ImagesView/SideBySideView.js +17 -7
  16. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  17. package/dist/client/shared/components/ImagesView/SlideView.d.ts +1 -1
  18. package/dist/client/shared/components/ImagesView/SlideView.js +17 -7
  19. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  20. package/dist/client/shared/components/ImagesView/SwapView.d.ts +1 -1
  21. package/dist/client/shared/components/ImagesView/SwapView.js +29 -7
  22. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  23. package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +1 -1
  24. package/dist/client/shared/components/PageHeader/ImagePreview.js +1 -0
  25. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  26. package/dist/client/shared/components/PageHeader/PageHeader.js +20 -8
  27. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  28. package/dist/client/shared/components/ResultsPage.d.ts +1 -1
  29. package/dist/client/shared/components/ResultsPage.js +43 -13
  30. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  31. package/dist/client/shared/creeveyClientApi.js +8 -1
  32. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  33. package/dist/client/shared/helpers.d.ts +1 -3
  34. package/dist/client/shared/helpers.js +4 -19
  35. package/dist/client/shared/helpers.js.map +1 -1
  36. package/dist/client/web/CreeveyApp.js +42 -14
  37. package/dist/client/web/CreeveyApp.js.map +1 -1
  38. package/dist/client/web/CreeveyContext.d.ts +5 -0
  39. package/dist/client/web/CreeveyContext.js +20 -7
  40. package/dist/client/web/CreeveyContext.js.map +1 -1
  41. package/dist/client/web/CreeveyLoader.js +2 -2
  42. package/dist/client/web/CreeveyLoader.js.map +1 -1
  43. package/dist/client/web/CreeveyView/SideBar/Search.js +19 -9
  44. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  45. package/dist/client/web/CreeveyView/SideBar/SideBar.js +18 -7
  46. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  47. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +60 -7
  48. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  49. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +17 -7
  50. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  51. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +2 -2
  52. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +18 -10
  53. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  54. package/dist/client/web/CreeveyView/SideBar/TestLink.js +18 -10
  55. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  56. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +1 -1
  57. package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +1 -1
  58. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  59. package/dist/client/web/KeyboardEventsContext.js +79 -64
  60. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  61. package/dist/client/web/assets/index-B0Xv0lOY.js +802 -0
  62. package/dist/client/web/index.html +1 -1
  63. package/dist/client/web/index.js +17 -7
  64. package/dist/client/web/index.js.map +1 -1
  65. package/dist/client/web/themes.d.ts +2 -0
  66. package/dist/client/web/themes.js +22 -0
  67. package/dist/client/web/themes.js.map +1 -0
  68. package/dist/creevey.js +16 -9
  69. package/dist/creevey.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/server/config.d.ts +1 -1
  72. package/dist/server/config.js +27 -5
  73. package/dist/server/config.js.map +1 -1
  74. package/dist/server/connection.d.ts +3 -0
  75. package/dist/server/connection.js +28 -0
  76. package/dist/server/connection.js.map +1 -0
  77. package/dist/server/docker.d.ts +1 -1
  78. package/dist/server/docker.js +56 -32
  79. package/dist/server/docker.js.map +1 -1
  80. package/dist/server/index.js +64 -11
  81. package/dist/server/index.js.map +1 -1
  82. package/dist/server/logger.d.ts +2 -1
  83. package/dist/server/logger.js +7 -3
  84. package/dist/server/logger.js.map +1 -1
  85. package/dist/server/master/api.js +1 -1
  86. package/dist/server/master/api.js.map +1 -1
  87. package/dist/server/master/pool.d.ts +4 -3
  88. package/dist/server/master/pool.js +13 -66
  89. package/dist/server/master/pool.js.map +1 -1
  90. package/dist/server/master/queue.d.ts +13 -0
  91. package/dist/server/master/queue.js +71 -0
  92. package/dist/server/master/queue.js.map +1 -0
  93. package/dist/server/master/runner.d.ts +3 -0
  94. package/dist/server/master/runner.js +76 -10
  95. package/dist/server/master/runner.js.map +1 -1
  96. package/dist/server/master/server.js +1 -1
  97. package/dist/server/master/server.js.map +1 -1
  98. package/dist/server/master/start.js +13 -11
  99. package/dist/server/master/start.js.map +1 -1
  100. package/dist/server/playwright/docker-file.d.ts +1 -1
  101. package/dist/server/playwright/docker-file.js +15 -6
  102. package/dist/server/playwright/docker-file.js.map +1 -1
  103. package/dist/server/playwright/docker.d.ts +2 -1
  104. package/dist/server/playwright/docker.js +10 -2
  105. package/dist/server/playwright/docker.js.map +1 -1
  106. package/dist/server/playwright/index-source.mjs +16 -0
  107. package/dist/server/playwright/internal.d.ts +6 -6
  108. package/dist/server/playwright/internal.js +143 -91
  109. package/dist/server/playwright/internal.js.map +1 -1
  110. package/dist/server/playwright/webdriver.d.ts +1 -1
  111. package/dist/server/playwright/webdriver.js +5 -8
  112. package/dist/server/playwright/webdriver.js.map +1 -1
  113. package/dist/server/providers/browser.js +6 -4
  114. package/dist/server/providers/browser.js.map +1 -1
  115. package/dist/server/providers/hybrid.js +1 -1
  116. package/dist/server/providers/hybrid.js.map +1 -1
  117. package/dist/server/reporter.d.ts +4 -19
  118. package/dist/server/reporter.js +30 -21
  119. package/dist/server/reporter.js.map +1 -1
  120. package/dist/server/selenium/internal.d.ts +3 -4
  121. package/dist/server/selenium/internal.js +127 -108
  122. package/dist/server/selenium/internal.js.map +1 -1
  123. package/dist/server/selenium/selenoid.js +8 -6
  124. package/dist/server/selenium/selenoid.js.map +1 -1
  125. package/dist/server/selenium/webdriver.d.ts +1 -1
  126. package/dist/server/selenium/webdriver.js +5 -9
  127. package/dist/server/selenium/webdriver.js.map +1 -1
  128. package/dist/server/telemetry.js +2 -2
  129. package/dist/server/testsFiles/parser.js +45 -5
  130. package/dist/server/testsFiles/parser.js.map +1 -1
  131. package/dist/server/utils.d.ts +19 -1
  132. package/dist/server/utils.js +87 -8
  133. package/dist/server/utils.js.map +1 -1
  134. package/dist/server/webdriver.d.ts +5 -4
  135. package/dist/server/webdriver.js +23 -10
  136. package/dist/server/webdriver.js.map +1 -1
  137. package/dist/server/worker/chai-image.d.ts +1 -2
  138. package/dist/server/worker/chai-image.js +4 -3
  139. package/dist/server/worker/chai-image.js.map +1 -1
  140. package/dist/server/worker/context.d.ts +3 -0
  141. package/dist/server/worker/context.js +15 -0
  142. package/dist/server/worker/context.js.map +1 -0
  143. package/dist/server/worker/match-image.d.ts +4 -4
  144. package/dist/server/worker/match-image.js +7 -4
  145. package/dist/server/worker/match-image.js.map +1 -1
  146. package/dist/server/worker/start.js +45 -73
  147. package/dist/server/worker/start.js.map +1 -1
  148. package/dist/shared/index.d.ts +1 -1
  149. package/dist/types.d.ts +40 -8
  150. package/dist/types.js +2 -0
  151. package/dist/types.js.map +1 -1
  152. package/docs/cli.md +12 -0
  153. package/docs/config.md +179 -165
  154. package/docs/storybook.md +60 -0
  155. package/docs/tests.md +50 -45
  156. package/package.json +64 -63
  157. package/src/client/addon/components/Panel.tsx +2 -2
  158. package/src/client/addon/withCreevey.ts +10 -2
  159. package/src/client/shared/components/ImagesView/SwapView.tsx +18 -0
  160. package/src/client/shared/components/PageHeader/ImagePreview.tsx +1 -0
  161. package/src/client/shared/components/PageHeader/PageHeader.tsx +4 -2
  162. package/src/client/shared/components/ResultsPage.tsx +31 -8
  163. package/src/client/shared/creeveyClientApi.ts +9 -1
  164. package/src/client/shared/helpers.ts +4 -24
  165. package/src/client/web/CreeveyApp.tsx +27 -8
  166. package/src/client/web/CreeveyContext.tsx +9 -0
  167. package/src/client/web/CreeveyLoader.tsx +1 -1
  168. package/src/client/web/CreeveyView/SideBar/Search.tsx +3 -3
  169. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +1 -0
  170. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +37 -6
  171. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +3 -5
  172. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +2 -4
  173. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  174. package/src/client/web/themes.ts +24 -0
  175. package/src/creevey.ts +16 -10
  176. package/src/server/config.ts +28 -6
  177. package/src/server/connection.ts +26 -0
  178. package/src/server/docker.ts +63 -34
  179. package/src/server/index.ts +72 -14
  180. package/src/server/logger.ts +6 -2
  181. package/src/server/master/api.ts +1 -1
  182. package/src/server/master/pool.ts +23 -59
  183. package/src/server/master/queue.ts +77 -0
  184. package/src/server/master/runner.ts +94 -10
  185. package/src/server/master/server.ts +1 -1
  186. package/src/server/master/start.ts +16 -11
  187. package/src/server/playwright/docker-file.ts +18 -6
  188. package/src/server/playwright/docker.ts +16 -3
  189. package/src/server/playwright/index-source.mjs +16 -0
  190. package/src/server/playwright/internal.ts +182 -111
  191. package/src/server/playwright/webdriver.ts +6 -9
  192. package/src/server/providers/browser.ts +6 -4
  193. package/src/server/providers/hybrid.ts +1 -1
  194. package/src/server/reporter.ts +37 -34
  195. package/src/server/selenium/internal.ts +131 -116
  196. package/src/server/selenium/selenoid.ts +8 -6
  197. package/src/server/selenium/webdriver.ts +6 -10
  198. package/src/server/telemetry.ts +2 -2
  199. package/src/server/testsFiles/parser.ts +52 -4
  200. package/src/server/utils.ts +97 -9
  201. package/src/server/webdriver.ts +24 -16
  202. package/src/server/worker/chai-image.ts +4 -4
  203. package/src/server/worker/context.ts +14 -0
  204. package/src/server/worker/match-image.ts +12 -8
  205. package/src/server/worker/start.ts +49 -86
  206. package/src/shared/index.ts +1 -1
  207. package/src/types.ts +44 -8
  208. package/types/global.d.ts +1 -0
  209. package/.yarnrc.yml +0 -1
  210. package/chromatic.config.json +0 -5
  211. package/dist/client/web/assets/index-DkmZfG9C.js +0 -591
package/docs/tests.md CHANGED
@@ -1,63 +1,68 @@
1
- ## Write tests
1
+ ## Write interactive screenshot tests
2
2
 
3
- By default Creevey generate for each story very simple screenshot test. In most cases it would be enough to test your UI. But you may want to do some interactions and capture one or multiple screenshots with different states of your story. For this case you could write custom tests, like this
3
+ In most cases following Storybook's ideology of [writing stories](https://storybook.js.org/docs/get-started/whats-a-story) is enough to test your UI components. Where each component has a separate stories file with its different states. But sometimes you might have pretty complicated components with a lot of interactions and internal states. In this case, you can write tests for your stories.
4
4
 
5
- ```tsx
6
- import React from 'react';
7
- import { Story } from '@storybook/react';
8
- import { CreeveyStory } from 'creevey';
9
- import MyComponent from './src/components/MyComponent';
5
+ There are two different ways how to write interactive tests with Creevey:
10
6
 
11
- export default { title: 'MyComponent' };
7
+ ### Write tests in `*.creevey.ts` files
12
8
 
13
- export const Basic: Story & CreeveyStory = () => <MyComponent />;
14
- Basic.parameters = {
15
- creevey: {
16
- captureElement: '#storybook-root',
17
- tests: {
18
- async click() {
19
- await this.browser.actions().click(this.captureElement).perform();
9
+ It's the recommended way to write tests. It allows you to run these tests by Creevey itself and utilize webdriver benefits. The crucial part of it is webdriver action calls are more close to real user interactions and mitigate flakiness and false-negative results. Here is a simple example of how to write tests in `*.creevey.ts` files
20
10
 
21
- await this.expect(await this.takeScreenshot()).to.matchImage('clicked component');
22
- },
23
- },
24
- },
25
- };
11
+ ```ts
12
+ // stories/MyComponent.creevey.ts
13
+ import { kind, story, test } from 'creevey';
14
+
15
+ kind('MyComponent', () => {
16
+ story('Story', ({ setStoryParameters }) => {
17
+ // It's possible to pass Creevey parameters to story
18
+ setStoryParameters({
19
+ captureElement: 'span[data-test-id~="x"]',
20
+ ignoreElements: [],
21
+ });
22
+
23
+ test('idle', async (context) => {
24
+ await context.matchImage(await context.takeScreenshot());
25
+ });
26
+
27
+ test('input', async (context) => {
28
+ await context.webdriver.keyboard.press('Tab');
29
+ const focus = await context.takeScreenshot();
30
+ await context.webdriver.keyboard.type('Hello Creevey');
31
+ const input = await context.takeScreenshot();
32
+ await context.matchImages({ focus, input });
33
+ });
34
+ });
35
+ });
26
36
  ```
27
37
 
28
- NOTE: Here you define story parameters with simple test `click`. Where you setup capturing element `#storybook-root` then click on that element and taking screenshot to assert it. `this.browser` allow you to access to native selenium webdriver instance you could check [API here](https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html).
38
+ In the example above, we used Playwright API to interact with the story. But Creevey also supports Selenium webdriver. And in that case `context.webdriver` will be an instance of Selenium webdriver. Obviously Selenium API is different from Playwright.
29
39
 
30
- You also could write more powerful tests with asserting multiple screenshots
40
+ ### Using Storybook's `play` function
31
41
 
32
- ```tsx
33
- import React from 'react';
34
- import { CSFStory } from 'creevey';
35
- import MyForm from './src/components/MyForm';
42
+ Storybook allows you to write tests in the story file itself by using [`play` function](https://storybook.js.org/docs/writing-tests/component-testing). It's a good way to write simple tests. But there are couple drawbacks of this approach:
36
43
 
37
- export default { title: 'MyForm' };
44
+ - You can have only one test per story. Which is not a big deal, but sometimes you might not want to have multiple stories with the same markup.
45
+ - Tests are running in browser environment and use https://testing-library.com API under the hood. It's good for unit tests, but might not be suitable for visual regression tests, because testing-library relies on DOM API and not even close to real user interactions. For example, you might have a button that could be visible for user, but it's covered by some other transparent element. With testing-library the button easily accessible and clickable, but the user can't interact with it.
38
46
 
39
- export const Basic: CSFStory<JSX.Element> = () => <MyForm />;
40
- Basic.story = {
41
- parameters: {
42
- creevey: {
43
- captureElement: '#storybook-root',
44
- delay: 1000,
45
- tests: {
46
- async submit() {
47
- const input = await this.browser.findElement({ css: '.my-input' });
47
+ Here is an example of how to write tests using Storybook's `play` function:
48
48
 
49
- const empty = await this.takeScreenshot();
49
+ ```tsx
50
+ // stories/MyComponent.stories.tsx
51
+ import React from 'react';
52
+ import { Meta, StoryObj } from '@storybook/react';
53
+ import { fireEvent, within } from '@storybook/test';
54
+ import MyComponent from './src/components/MyComponent';
50
55
 
51
- await this.browser.actions().click(input).sendKeys('Hello Creevey').sendKeys(this.keys.ENTER).perform();
56
+ export default {
57
+ title: 'MyComponent',
58
+ component: MyComponent,
59
+ };
52
60
 
53
- const submitted = await this.takeScreenshot();
61
+ export const Basic: StoryObj<typeof MyComponent> = {
62
+ play: async ({ canvasElement }) => {
63
+ const slider = await within(canvasElement).findByTestId('slider');
54
64
 
55
- await this.expect({ empty, submitted }).to.matchImages();
56
- },
57
- },
58
- },
65
+ await fireEvent.change(slider, { target: { value: 50 } });
59
66
  },
60
67
  };
61
68
  ```
62
-
63
- NOTE: In this example I fill some simple form and submit it. Also as you could see, I taking two different screenshots `empty` and `submitted` and assert these in the end.
package/package.json CHANGED
@@ -1,7 +1,7 @@
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.4",
4
+ "version": "0.10.0-beta.40",
5
5
  "type": "commonjs",
6
6
  "bin": "dist/cli.js",
7
7
  "main": "./dist/index.js",
@@ -64,7 +64,7 @@
64
64
  "build": "yarn prebuild && yarn build:client && yarn build:creevey && yarn postbuild",
65
65
  "build:client": "vite build",
66
66
  "build:creevey": "tsc --build tsconfig.prod.json",
67
- "postbuild": "cp \"\"scripts/dist/*.d.ts\"\" dist/",
67
+ "postbuild": "cp \"\"scripts/dist/*.d.ts\"\" dist/ && cp \"\"src/server/playwright/index-source.mjs\"\" dist/server/playwright/index-source.mjs",
68
68
  "version": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md",
69
69
  "build-storybook": "storybook build",
70
70
  "chromatic": "chromatic --project-token=chpt_80df83ca94e6fb4",
@@ -74,11 +74,11 @@
74
74
  "node": ">=18.0"
75
75
  },
76
76
  "peerDependencies": {
77
- "playwright": "*",
77
+ "playwright-core": "*",
78
78
  "selenium-webdriver": "*"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
- "playwright": {
81
+ "playwright-core": {
82
82
  "optional": true
83
83
  },
84
84
  "selenium-webdriver": {
@@ -87,33 +87,33 @@
87
87
  },
88
88
  "dependencies": {
89
89
  "@koa/cors": "^5.0.0",
90
- "@octokit/core": "^6.1.2",
91
- "@storybook/icons": "^1.2.12",
90
+ "@octokit/core": "^6.1.4",
91
+ "@storybook/icons": "^1.4.0",
92
92
  "@types/chai": "^4.3.20",
93
- "@types/dockerode": "^3.3.31",
93
+ "@types/dockerode": "^3.3.37",
94
94
  "@types/koa": "^2.15.0",
95
95
  "@types/koa-bodyparser": "^4.3.12",
96
96
  "@types/koa-mount": "^4.0.5",
97
97
  "@types/koa-static": "^4.0.4",
98
98
  "@types/koa__cors": "^5.0.0",
99
- "@types/lodash": "^4.17.13",
99
+ "@types/lodash": "^4.17.16",
100
100
  "@types/micromatch": "^4.0.9",
101
101
  "@types/minimist": "^1.2.5",
102
102
  "@types/pixelmatch": "^5.2.6",
103
103
  "@types/pngjs": "^6.0.5",
104
- "@types/qs": "^6.9.16",
105
- "@types/react": "^18.3.12",
106
- "@types/react-dom": "^18.3.1",
107
- "@types/selenium-webdriver": "^4.1.27",
104
+ "@types/qs": "^6.9.18",
105
+ "@types/react": "^18.3.20",
106
+ "@types/react-dom": "^18.3.6",
107
+ "@types/selenium-webdriver": "^4.1.28",
108
108
  "@types/shelljs": "^0.8.15",
109
- "@types/ws": "^8.5.13",
109
+ "@types/ws": "^8.18.1",
110
110
  "chai": "^4.5.0",
111
111
  "chalk": "^4.1.2",
112
- "chokidar": "^4.0.1",
113
- "dockerode": "^4.0.2",
112
+ "chokidar": "^4.0.3",
113
+ "dockerode": "^4.0.5",
114
114
  "find-cache-dir": "^5.0.0",
115
115
  "get-port": "^7.1.0",
116
- "koa": "^2.15.3",
116
+ "koa": "^2.16.0",
117
117
  "koa-bodyparser": "^4.4.1",
118
118
  "koa-mount": "^4.0.0",
119
119
  "koa-static": "^5.0.0",
@@ -122,72 +122,73 @@
122
122
  "loglevel-plugin-prefix": "^0.8.4",
123
123
  "micromatch": "^4.0.8",
124
124
  "minimist": "^1.2.8",
125
- "odiff-bin": "^3.1.2",
125
+ "odiff-bin": "^3.2.1",
126
+ "package-manager-detector": "^0.2.11",
127
+ "pidtree": "^0.6.0",
126
128
  "pixelmatch": "^6.0.0",
127
129
  "pngjs": "^7.0.0",
128
130
  "polished": "^4.3.1",
129
- "qs": "^6.13.0",
130
- "semver": "^7.6.3",
131
- "shelljs": "^0.8.5",
131
+ "qs": "^6.14.0",
132
+ "semver": "^7.7.1",
133
+ "shelljs": "^0.9.2",
132
134
  "tar-stream": "^3.1.7",
133
- "tsx": "^4.19.2",
134
- "uuid": "^11.0.2",
135
- "ws": "^8.18.0",
136
- "yocto-spinner": "^0.1.1"
135
+ "tsx": "^4.19.3",
136
+ "uuid": "^11.1.0",
137
+ "ws": "^8.18.1",
138
+ "yocto-spinner": "^0.2.1"
137
139
  },
138
140
  "devDependencies": {
139
- "@chromatic-com/storybook": "^3.2.2",
140
- "@eslint/js": "^9.14.0",
141
- "@storybook/addon-essentials": "^8.4.1",
142
- "@storybook/addon-interactions": "^8.4.1",
143
- "@storybook/blocks": "^8.4.1",
144
- "@storybook/channels": "^8.4.1",
145
- "@storybook/components": "^8.4.1",
146
- "@storybook/csf": "^0.1.11",
147
- "@storybook/manager-api": "^8.4.1",
148
- "@storybook/preview-api": "^8.4.1",
149
- "@storybook/react": "^8.4.1",
150
- "@storybook/react-vite": "^8.4.1",
151
- "@storybook/test": "^8.4.1",
152
- "@storybook/theming": "^8.4.1",
153
- "@storybook/types": "^8.4.1",
141
+ "@chromatic-com/storybook": "^3.2.6",
142
+ "@eslint/js": "^9.23.0",
143
+ "@storybook/addon-essentials": "^8.6.12",
144
+ "@storybook/addon-interactions": "^8.6.12",
145
+ "@storybook/blocks": "^8.6.12",
146
+ "@storybook/channels": "^8.6.12",
147
+ "@storybook/components": "^8.6.12",
148
+ "@storybook/manager-api": "^8.6.12",
149
+ "@storybook/preview-api": "^8.6.12",
150
+ "@storybook/react": "^8.6.12",
151
+ "@storybook/react-vite": "^8.6.12",
152
+ "@storybook/test": "^8.6.12",
153
+ "@storybook/theming": "^8.6.12",
154
+ "@storybook/types": "^8.6.12",
154
155
  "@types/eslint": "^9.6.1",
155
156
  "@types/eslint__js": "^8.42.3",
156
- "@types/node": "^18.19.64",
157
+ "@types/node": "^18.19.86",
157
158
  "@types/resize-observer-browser": "^0.1.11",
158
- "@types/semver": "^7",
159
+ "@types/semver": "^7.7.0",
159
160
  "@types/tar-stream": "^3.1.3",
160
161
  "@types/tmp": "^0.2.6",
161
- "@vitejs/plugin-react-swc": "^3.7.1",
162
- "browserstack-local": "^1.5.5",
163
- "chromatic": "^11.16.3",
164
- "concurrently": "^9.0.1",
162
+ "@vitejs/plugin-react-swc": "^3.8.1",
163
+ "browserstack-local": "^1.5.6",
164
+ "chromatic": "^11.28.0",
165
+ "concurrently": "^9.1.2",
165
166
  "conventional-changelog-cli": "^5.0.0",
166
- "dotenv": "^16.4.5",
167
- "eslint": "^9.14.0",
168
- "eslint-config-prettier": "^9.1.0",
169
- "eslint-plugin-prettier": "^5.2.1",
170
- "eslint-plugin-react": "^7.37.2",
171
- "eslint-plugin-react-hooks": "^5.0.0",
167
+ "dotenv": "^16.4.7",
168
+ "eslint": "^9.23.0",
169
+ "eslint-config-prettier": "^10.1.1",
170
+ "eslint-plugin-prettier": "^5.2.6",
171
+ "eslint-plugin-react": "^7.37.5",
172
+ "eslint-plugin-react-hooks": "^5.2.0",
172
173
  "git-cz": "^4.9.0",
173
- "globals": "^15.11.0",
174
- "husky": "^9.1.6",
174
+ "globals": "^16.0.0",
175
+ "husky": "^9.1.7",
175
176
  "immer": "^10.1.1",
176
- "lint-staged": "^15.2.10",
177
+ "lint-staged": "^15.5.0",
177
178
  "pinst": "^3.0.0",
178
- "playwright-core": "^1.48.0",
179
- "prettier": "^3.3.3",
179
+ "playwright-core": "^1.51.1",
180
+ "prettier": "^3.5.3",
180
181
  "react": "^18.3.1",
181
182
  "react-dom": "^18.3.1",
182
183
  "react-is": "^18.3.1",
183
- "selenium-webdriver": "^4.26.0",
184
- "storybook": "^8.4.1",
184
+ "selenium-webdriver": "^4.30.0",
185
+ "storybook": "^8.6.12",
185
186
  "tmp": "^0.2.3",
186
- "typescript": "^5.6.3",
187
- "typescript-eslint": "^8.12.2",
188
- "use-immer": "^0.10.0",
189
- "vite": "^5.4.10",
190
- "vitest": "^2.1.4"
187
+ "typescript": "^5.8.2",
188
+ "typescript-eslint": "^8.29.0",
189
+ "use-immer": "^0.11.0",
190
+ "vite": "^5.4.17",
191
+ "vitest": "^2.1.9"
191
192
  },
192
193
  "config": {
193
194
  "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,4 +1,4 @@
1
- import type { Renderer, StoryContextForEnhancers } from '@storybook/csf';
1
+ import type { Renderer, StoryContextForEnhancers } from '@storybook/types';
2
2
  import { makeDecorator, PreviewWeb, StoryStore } from '@storybook/preview-api';
3
3
  import { Channel } from '@storybook/channels';
4
4
  import {
@@ -12,9 +12,11 @@ import {
12
12
  } from '../../types.js';
13
13
  import { serializeRawStories } from '../../shared/index.js';
14
14
  import { getConnectionUrl } from '../shared/helpers.js';
15
+ import isEqual from 'lodash/isEqual.js';
15
16
 
16
17
  declare global {
17
18
  interface Window {
19
+ __CREEVEY_ENV__: boolean;
18
20
  __CREEVEY_SERVER_HOST__: string;
19
21
  __CREEVEY_SERVER_PORT__: number;
20
22
  __CREEVEY_WORKER_ID__: number;
@@ -139,6 +141,7 @@ let captureResolver: () => void;
139
141
  let waitForCreevey: Promise<void>;
140
142
  let creeveyReady: () => void;
141
143
  let setStoriesCounter = 0;
144
+ let globals = {};
142
145
 
143
146
  export function withCreevey(): ReturnType<typeof makeDecorator> {
144
147
  const addonsChannel = (): Channel => window.__STORYBOOK_ADDONS_CHANNEL__;
@@ -225,7 +228,10 @@ export function withCreevey(): ReturnType<typeof makeDecorator> {
225
228
  }
226
229
  }
227
230
 
228
- function updateGlobals(globals: StorybookGlobals): void {
231
+ function updateGlobals(newGlobals: StorybookGlobals): void {
232
+ if (isEqual(globals, newGlobals)) return;
233
+
234
+ globals = newGlobals;
229
235
  addonsChannel().emit(StorybookEvents.UPDATE_GLOBALS, { globals });
230
236
  }
231
237
 
@@ -274,6 +280,7 @@ export function withCreevey(): ReturnType<typeof makeDecorator> {
274
280
  });
275
281
  }
276
282
 
283
+ window.__CREEVEY_ENV__ = false;
277
284
  window.__CREEVEY_GET_STORIES__ = getStories;
278
285
  window.__CREEVEY_SELECT_STORY__ = selectStory;
279
286
  window.__CREEVEY_UPDATE_GLOBALS__ = updateGlobals;
@@ -315,6 +322,7 @@ export function withCreevey(): ReturnType<typeof makeDecorator> {
315
322
  });
316
323
  }
317
324
 
325
+ // TODO It's not accessible from the outside the package
318
326
  export async function capture(options?: CaptureOptions): Promise<void> {
319
327
  if (!isTestBrowser) return;
320
328
 
@@ -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,4 +1,4 @@
1
- import React, { JSX, useEffect } from 'react';
1
+ import React, { JSX, useContext, useEffect } from 'react';
2
2
  import { Tabs } from '@storybook/components';
3
3
  import { CloseAltIcon } from '@storybook/icons';
4
4
  import { styled, withTheme, Theme } from '@storybook/theming';
@@ -6,6 +6,7 @@ import { ImagesViewMode, Images } from '../../../../types.js';
6
6
  import { getImageUrl } from '../../helpers.js';
7
7
  import { ImagePreview } from './ImagePreview.js';
8
8
  import { viewModes } from '../../viewMode.js';
9
+ import { CreeveyContext } from '../../../web/CreeveyContext.js';
9
10
 
10
11
  interface PageHeaderProps {
11
12
  title: string[];
@@ -76,6 +77,7 @@ export function PageHeader({
76
77
  onImageChange,
77
78
  onViewModeChange,
78
79
  }: PageHeaderProps): JSX.Element | null {
80
+ const { isReport } = useContext(CreeveyContext);
79
81
  const imageEntires = Object.entries(images) as [string, Images][];
80
82
 
81
83
  const handleViewModeChange = (mode: string): void => {
@@ -110,7 +112,7 @@ export function PageHeader({
110
112
  <ImagePreview
111
113
  key={name}
112
114
  imageName={name}
113
- url={`${getImageUrl(title, name)}/${image.actual}`}
115
+ url={`${getImageUrl(title, name, isReport)}/${image.actual}`}
114
116
  isActive={name === imageName}
115
117
  onClick={onImageChange}
116
118
  error={imagesWithError.includes(name)}
@@ -1,12 +1,13 @@
1
- import React, { JSX, useState } from 'react';
1
+ import React, { JSX, useCallback, useContext, 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
+ import { CreeveyContext } from '../../web/CreeveyContext.js';
10
11
 
11
12
  interface ResultsPageProps {
12
13
  path: string[];
@@ -65,8 +66,9 @@ export function ResultsPageInternal({
65
66
  onRetryChange,
66
67
  }: ResultsPageProps): JSX.Element {
67
68
  const result = results[retry - 1] ?? {};
69
+ const { isReport } = useContext(CreeveyContext);
68
70
  const [viewMode, setViewMode] = useState<ImagesViewMode>(getViewMode());
69
- const url = getImageUrl(path, imageName);
71
+ const url = getImageUrl(path, imageName, isReport);
70
72
  const image = result.images?.[imageName];
71
73
  const canApprove = Boolean(image && approved?.[imageName] != retry - 1 && result.status != 'success');
72
74
  const hasDiffAndExpect = canApprove && Boolean(image?.diff && image.expect);
@@ -77,10 +79,31 @@ export function ResultsPageInternal({
77
79
  )
78
80
  : [];
79
81
 
80
- const handleChangeViewMode = (mode: ImagesViewMode): void => {
81
- localStorage.setItem(VIEW_MODE_KEY, mode);
82
- setViewMode(mode);
83
- };
82
+ const handleKeyDown = useCallback(
83
+ (e: KeyboardEvent) => {
84
+ if (!canApprove) return;
85
+ if (e.code === 'Tab') {
86
+ e.preventDefault();
87
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
88
+ if (e.shiftKey) setViewMode((mode) => viewModes.at((viewModes.indexOf(mode) - 1) % viewModes.length)!);
89
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
90
+ else setViewMode((mode) => viewModes.at((viewModes.indexOf(mode) + 1) % viewModes.length)!);
91
+ }
92
+ },
93
+ [canApprove],
94
+ );
95
+
96
+ useEffect(() => {
97
+ localStorage.setItem(VIEW_MODE_KEY, viewMode);
98
+ }, [viewMode]);
99
+
100
+ useEffect(() => {
101
+ document.addEventListener('keydown', handleKeyDown, false);
102
+
103
+ return () => {
104
+ document.removeEventListener('keydown', handleKeyDown, false);
105
+ };
106
+ }, [handleKeyDown]);
84
107
 
85
108
  return (
86
109
  <Container height={height}>
@@ -92,7 +115,7 @@ export function ResultsPageInternal({
92
115
  errorMessage={result.error}
93
116
  showViewModes={hasDiffAndExpect}
94
117
  viewMode={viewMode}
95
- onViewModeChange={handleChangeViewMode}
118
+ onViewModeChange={setViewMode}
96
119
  onImageChange={onImageChange}
97
120
  imagesWithError={imagesWithError}
98
121
  />
@@ -12,6 +12,7 @@ export interface CreeveyClientApi {
12
12
 
13
13
  export async function initCreeveyClientApi(): Promise<CreeveyClientApi> {
14
14
  let clientApiResolver: (api: CreeveyClientApi) => void = noop;
15
+ let clientApiRejecter: (error: Error | Event) => void = noop;
15
16
  const updateListeners = new Set<(update: CreeveyUpdate) => void>();
16
17
  let statusRequest: Promise<CreeveyStatus> | null = null;
17
18
  let statusResolver: (status: CreeveyStatus) => void = noop;
@@ -22,6 +23,10 @@ export async function initCreeveyClientApi(): Promise<CreeveyClientApi> {
22
23
  ws.send(JSON.stringify(request));
23
24
  }
24
25
 
26
+ ws.addEventListener('error', (event) => {
27
+ clientApiRejecter(event);
28
+ });
29
+
25
30
  ws.addEventListener('open', () => {
26
31
  clientApiResolver({
27
32
  start(ids: string[]) {
@@ -64,5 +69,8 @@ export async function initCreeveyClientApi(): Promise<CreeveyClientApi> {
64
69
  });
65
70
  // TODO Reconnect
66
71
 
67
- return new Promise((resolve) => (clientApiResolver = resolve));
72
+ return new Promise((resolve, reject) => {
73
+ clientApiResolver = resolve;
74
+ clientApiRejecter = reject;
75
+ });
68
76
  }
@@ -1,4 +1,3 @@
1
- import { themes, ThemeVars } from '@storybook/theming';
2
1
  import { parse, stringify } from 'qs';
3
2
  import { RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
4
3
  import { TestData, isTest, isDefined, TestStatus, CreeveySuite, CreeveyTest, CreeveyStatus } from '../../types.js';
@@ -300,12 +299,14 @@ export function getConnectionUrl(): string {
300
299
  .join(':');
301
300
  }
302
301
 
303
- export function getImageUrl(path: string[], imageName: string): string {
302
+ export function getImageUrl(path: string[], imageName: string, isReport?: boolean): string {
304
303
  // path => [title, story, test, browser]
305
304
  const browser = path.slice(-1)[0];
306
305
  const imagesUrl = window.location.host
307
306
  ? `${window.location.protocol}//${getConnectionUrl()}${
308
- window.location.pathname == '/' ? '/report' : window.location.pathname.split('/').slice(0, -1).join('/')
307
+ window.location.pathname == '/' && !isReport
308
+ ? '/report'
309
+ : window.location.pathname.split('/').slice(0, -1).join('/')
309
310
  }/${encodeURI(path.slice(0, -1).join('/'))}`
310
311
  : encodeURI(path.slice(0, -1).join('/'));
311
312
 
@@ -393,27 +394,6 @@ export function useCalcScale(diffImageRef: RefObject<HTMLImageElement>, loaded:
393
394
  return scale;
394
395
  }
395
396
 
396
- const CREEVEY_THEME = 'Creevey_theme';
397
-
398
- function isTheme(theme?: string | null): theme is ThemeVars['base'] {
399
- return isDefined(theme) && Object.prototype.hasOwnProperty.call(themes, theme);
400
- }
401
-
402
- function initialTheme(): ThemeVars['base'] {
403
- const theme = localStorage.getItem(CREEVEY_THEME);
404
- return isTheme(theme) ? theme : 'light';
405
- }
406
-
407
- export function useTheme(): [ThemeVars['base'], (theme: ThemeVars['base']) => void] {
408
- const [theme, setTheme] = useState<ThemeVars['base']>(initialTheme());
409
-
410
- useEffect(() => {
411
- localStorage.setItem(CREEVEY_THEME, theme);
412
- }, [theme]);
413
-
414
- return [theme, setTheme];
415
- }
416
-
417
397
  export function setSearchParams(testPath: string[]): void {
418
398
  const pageUrl = `?${stringify({ testPath })}`;
419
399
  window.history.pushState({ testPath }, '', pageUrl);