creevey 0.10.0-beta.8 → 0.10.0-rc.0

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 (350) hide show
  1. package/AUTHORS +2 -0
  2. package/CHANGELOG.md +281 -0
  3. package/README.md +19 -41
  4. package/dist/client/addon/components/Addon.js +18 -8
  5. package/dist/client/addon/components/Addon.js.map +1 -1
  6. package/dist/client/addon/components/Panel.js +4 -4
  7. package/dist/client/addon/components/Panel.js.map +1 -1
  8. package/dist/client/addon/components/TestSelect.js +2 -2
  9. package/dist/client/addon/components/TestSelect.js.map +1 -1
  10. package/dist/client/addon/components/Tools.js +19 -9
  11. package/dist/client/addon/components/Tools.js.map +1 -1
  12. package/dist/client/addon/controller.d.ts +1 -1
  13. package/dist/client/addon/controller.js +3 -3
  14. package/dist/client/addon/controller.js.map +1 -1
  15. package/dist/client/addon/decorator.d.ts +1 -1
  16. package/dist/client/addon/makeDecorator.d.ts +9 -0
  17. package/dist/client/addon/makeDecorator.js +48 -0
  18. package/dist/client/addon/makeDecorator.js.map +1 -0
  19. package/dist/client/addon/manager.js +38 -39
  20. package/dist/client/addon/manager.js.map +1 -1
  21. package/dist/client/addon/preset.d.ts +0 -1
  22. package/dist/client/addon/preset.js +3 -2
  23. package/dist/client/addon/preset.js.map +1 -1
  24. package/dist/client/addon/preview.d.ts +1 -1
  25. package/dist/client/addon/withCreevey.d.ts +5 -3
  26. package/dist/client/addon/withCreevey.js +14 -21
  27. package/dist/client/addon/withCreevey.js.map +1 -1
  28. package/dist/client/shared/components/ImagesView/BlendView.d.ts +2 -2
  29. package/dist/client/shared/components/ImagesView/BlendView.js +18 -8
  30. package/dist/client/shared/components/ImagesView/BlendView.js.map +1 -1
  31. package/dist/client/shared/components/ImagesView/ImagesView.js +1 -1
  32. package/dist/client/shared/components/ImagesView/ImagesView.js.map +1 -1
  33. package/dist/client/shared/components/ImagesView/SideBySideView.d.ts +2 -2
  34. package/dist/client/shared/components/ImagesView/SideBySideView.js +19 -9
  35. package/dist/client/shared/components/ImagesView/SideBySideView.js.map +1 -1
  36. package/dist/client/shared/components/ImagesView/SlideView.d.ts +2 -2
  37. package/dist/client/shared/components/ImagesView/SlideView.js +19 -9
  38. package/dist/client/shared/components/ImagesView/SlideView.js.map +1 -1
  39. package/dist/client/shared/components/ImagesView/SwapView.d.ts +2 -2
  40. package/dist/client/shared/components/ImagesView/SwapView.js +31 -9
  41. package/dist/client/shared/components/ImagesView/SwapView.js.map +1 -1
  42. package/dist/client/shared/components/ImagesView/common.d.ts +1 -1
  43. package/dist/client/shared/components/PageFooter/PageFooter.js +1 -1
  44. package/dist/client/shared/components/PageFooter/PageFooter.js.map +1 -1
  45. package/dist/client/shared/components/PageFooter/Paging.js +1 -1
  46. package/dist/client/shared/components/PageFooter/Paging.js.map +1 -1
  47. package/dist/client/shared/components/PageHeader/ImagePreview.d.ts +2 -2
  48. package/dist/client/shared/components/PageHeader/ImagePreview.js +2 -1
  49. package/dist/client/shared/components/PageHeader/ImagePreview.js.map +1 -1
  50. package/dist/client/shared/components/PageHeader/PageHeader.js +34 -13
  51. package/dist/client/shared/components/PageHeader/PageHeader.js.map +1 -1
  52. package/dist/client/shared/components/ResultsPage.d.ts +2 -2
  53. package/dist/client/shared/components/ResultsPage.js +45 -15
  54. package/dist/client/shared/components/ResultsPage.js.map +1 -1
  55. package/dist/client/shared/creeveyClientApi.js +18 -1
  56. package/dist/client/shared/creeveyClientApi.js.map +1 -1
  57. package/dist/client/shared/helpers.d.ts +1 -3
  58. package/dist/client/shared/helpers.js +4 -19
  59. package/dist/client/shared/helpers.js.map +1 -1
  60. package/dist/client/web/CreeveyApp.d.ts +1 -0
  61. package/dist/client/web/CreeveyApp.js +44 -15
  62. package/dist/client/web/CreeveyApp.js.map +1 -1
  63. package/dist/client/web/CreeveyContext.d.ts +6 -0
  64. package/dist/client/web/CreeveyContext.js +21 -7
  65. package/dist/client/web/CreeveyContext.js.map +1 -1
  66. package/dist/client/web/CreeveyLoader.d.ts +1 -1
  67. package/dist/client/web/CreeveyLoader.js +3 -3
  68. package/dist/client/web/CreeveyLoader.js.map +1 -1
  69. package/dist/client/web/CreeveyView/SideBar/Checkbox.d.ts +4 -4
  70. package/dist/client/web/CreeveyView/SideBar/Checkbox.js +36 -6
  71. package/dist/client/web/CreeveyView/SideBar/Checkbox.js.map +1 -1
  72. package/dist/client/web/CreeveyView/SideBar/Search.js +20 -10
  73. package/dist/client/web/CreeveyView/SideBar/Search.js.map +1 -1
  74. package/dist/client/web/CreeveyView/SideBar/SideBar.js +26 -12
  75. package/dist/client/web/CreeveyView/SideBar/SideBar.js.map +1 -1
  76. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js +67 -13
  77. package/dist/client/web/CreeveyView/SideBar/SideBarFooter.js.map +1 -1
  78. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js +32 -12
  79. package/dist/client/web/CreeveyView/SideBar/SideBarHeader.js.map +1 -1
  80. package/dist/client/web/CreeveyView/SideBar/SuiteLink.d.ts +6 -6
  81. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js +20 -13
  82. package/dist/client/web/CreeveyView/SideBar/SuiteLink.js.map +1 -1
  83. package/dist/client/web/CreeveyView/SideBar/TestLink.js +20 -13
  84. package/dist/client/web/CreeveyView/SideBar/TestLink.js.map +1 -1
  85. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.d.ts +2 -2
  86. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js +2 -2
  87. package/dist/client/web/CreeveyView/SideBar/TestStatusIcon.js.map +1 -1
  88. package/dist/client/web/CreeveyView/SideBar/TestsStatus.d.ts +2 -2
  89. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js +3 -2
  90. package/dist/client/web/CreeveyView/SideBar/TestsStatus.js.map +1 -1
  91. package/dist/client/web/CreeveyView/SideBar/Toggle.js +1 -1
  92. package/dist/client/web/CreeveyView/SideBar/Toggle.js.map +1 -1
  93. package/dist/client/web/KeyboardEventsContext.d.ts +1 -8
  94. package/dist/client/web/KeyboardEventsContext.js +79 -64
  95. package/dist/client/web/KeyboardEventsContext.js.map +1 -1
  96. package/dist/client/web/assets/index-CtSq3IhG.js +518 -0
  97. package/dist/client/web/index.html +1 -1
  98. package/dist/client/web/index.js +26 -11
  99. package/dist/client/web/index.js.map +1 -1
  100. package/dist/client/web/themes.d.ts +2 -0
  101. package/dist/client/web/themes.js +22 -0
  102. package/dist/client/web/themes.js.map +1 -0
  103. package/dist/creevey.d.ts +1 -1
  104. package/dist/creevey.js +122 -41
  105. package/dist/creevey.js.map +1 -1
  106. package/dist/index.d.ts +1 -0
  107. package/dist/playwright/generator.d.ts +25 -0
  108. package/dist/playwright/generator.js +243 -0
  109. package/dist/playwright/generator.js.map +1 -0
  110. package/dist/playwright/helpers.d.ts +2 -0
  111. package/dist/playwright/helpers.js +29 -0
  112. package/dist/playwright/helpers.js.map +1 -0
  113. package/dist/playwright/reporter.d.ts +83 -0
  114. package/dist/playwright/reporter.js +334 -0
  115. package/dist/playwright/reporter.js.map +1 -0
  116. package/dist/playwright/setup.d.ts +3 -0
  117. package/dist/playwright/setup.js +72 -0
  118. package/dist/playwright/setup.js.map +1 -0
  119. package/dist/playwright.d.ts +1 -0
  120. package/dist/playwright.js +3 -1
  121. package/dist/playwright.js.map +1 -1
  122. package/dist/server/compare.d.ts +18 -0
  123. package/dist/server/compare.js +182 -0
  124. package/dist/server/compare.js.map +1 -0
  125. package/dist/server/config.d.ts +3 -3
  126. package/dist/server/config.js +75 -8
  127. package/dist/server/config.js.map +1 -1
  128. package/dist/server/connection.d.ts +3 -0
  129. package/dist/server/connection.js +28 -0
  130. package/dist/server/connection.js.map +1 -0
  131. package/dist/server/docker.d.ts +1 -1
  132. package/dist/server/docker.js +54 -32
  133. package/dist/server/docker.js.map +1 -1
  134. package/dist/server/index.d.ts +2 -2
  135. package/dist/server/index.js +165 -64
  136. package/dist/server/index.js.map +1 -1
  137. package/dist/server/master/api.d.ts +11 -6
  138. package/dist/server/master/api.js +88 -25
  139. package/dist/server/master/api.js.map +1 -1
  140. package/dist/server/master/handlers/capture-handler.d.ts +5 -0
  141. package/dist/server/master/handlers/capture-handler.js +25 -0
  142. package/dist/server/master/handlers/capture-handler.js.map +1 -0
  143. package/dist/server/master/handlers/index.d.ts +4 -0
  144. package/dist/server/master/handlers/index.js +21 -0
  145. package/dist/server/master/handlers/index.js.map +1 -0
  146. package/dist/server/master/handlers/ping-handler.d.ts +2 -0
  147. package/dist/server/master/handlers/ping-handler.js +8 -0
  148. package/dist/server/master/handlers/ping-handler.js.map +1 -0
  149. package/dist/server/master/handlers/static-handler.d.ts +1 -0
  150. package/dist/server/master/handlers/static-handler.js +20 -0
  151. package/dist/server/master/handlers/static-handler.js.map +1 -0
  152. package/dist/server/master/handlers/stories-handler.d.ts +4 -0
  153. package/dist/server/master/handlers/stories-handler.js +24 -0
  154. package/dist/server/master/handlers/stories-handler.js.map +1 -0
  155. package/dist/server/master/master.js +7 -24
  156. package/dist/server/master/master.js.map +1 -1
  157. package/dist/server/master/pool.d.ts +1 -0
  158. package/dist/server/master/pool.js +5 -3
  159. package/dist/server/master/pool.js.map +1 -1
  160. package/dist/server/master/queue.d.ts +1 -1
  161. package/dist/server/master/queue.js +14 -6
  162. package/dist/server/master/queue.js.map +1 -1
  163. package/dist/server/master/runner.d.ts +6 -6
  164. package/dist/server/master/runner.js +98 -130
  165. package/dist/server/master/runner.js.map +1 -1
  166. package/dist/server/master/server.d.ts +1 -1
  167. package/dist/server/master/server.js +193 -88
  168. package/dist/server/master/server.js.map +1 -1
  169. package/dist/server/master/start.d.ts +1 -2
  170. package/dist/server/master/start.js +13 -29
  171. package/dist/server/master/start.js.map +1 -1
  172. package/dist/server/master/testsManager.d.ts +81 -0
  173. package/dist/server/master/testsManager.js +282 -0
  174. package/dist/server/master/testsManager.js.map +1 -0
  175. package/dist/server/playwright/docker-file.d.ts +1 -1
  176. package/dist/server/playwright/docker-file.js +17 -8
  177. package/dist/server/playwright/docker-file.js.map +1 -1
  178. package/dist/server/playwright/docker.d.ts +2 -1
  179. package/dist/server/playwright/docker.js +10 -2
  180. package/dist/server/playwright/docker.js.map +1 -1
  181. package/dist/server/playwright/index-source.mjs +16 -0
  182. package/dist/server/playwright/internal.d.ts +7 -7
  183. package/dist/server/playwright/internal.js +144 -84
  184. package/dist/server/playwright/internal.js.map +1 -1
  185. package/dist/server/playwright/webdriver.d.ts +3 -3
  186. package/dist/server/playwright/webdriver.js +0 -6
  187. package/dist/server/playwright/webdriver.js.map +1 -1
  188. package/dist/server/providers/browser.js +4 -3
  189. package/dist/server/providers/browser.js.map +1 -1
  190. package/dist/server/providers/hybrid.js +2 -2
  191. package/dist/server/providers/hybrid.js.map +1 -1
  192. package/dist/server/report.d.ts +10 -0
  193. package/dist/server/report.js +45 -0
  194. package/dist/server/report.js.map +1 -0
  195. package/dist/server/reporters/creevey.d.ts +7 -0
  196. package/dist/server/reporters/creevey.js +63 -0
  197. package/dist/server/reporters/creevey.js.map +1 -0
  198. package/dist/server/reporters/index.d.ts +2 -0
  199. package/dist/server/reporters/index.js +16 -0
  200. package/dist/server/reporters/index.js.map +1 -0
  201. package/dist/server/reporters/junit.d.ts +16 -0
  202. package/dist/server/reporters/junit.js +167 -0
  203. package/dist/server/reporters/junit.js.map +1 -0
  204. package/dist/server/reporters/teamcity.d.ts +7 -0
  205. package/dist/server/reporters/teamcity.js +60 -0
  206. package/dist/server/reporters/teamcity.js.map +1 -0
  207. package/dist/server/selenium/internal.d.ts +4 -4
  208. package/dist/server/selenium/internal.js +56 -40
  209. package/dist/server/selenium/internal.js.map +1 -1
  210. package/dist/server/selenium/selenoid.js +12 -6
  211. package/dist/server/selenium/selenoid.js.map +1 -1
  212. package/dist/server/selenium/webdriver.d.ts +3 -3
  213. package/dist/server/selenium/webdriver.js +4 -8
  214. package/dist/server/selenium/webdriver.js.map +1 -1
  215. package/dist/server/shutdown.d.ts +1 -0
  216. package/dist/server/shutdown.js +23 -0
  217. package/dist/server/shutdown.js.map +1 -0
  218. package/dist/server/stories.d.ts +0 -1
  219. package/dist/server/stories.js +0 -12
  220. package/dist/server/stories.js.map +1 -1
  221. package/dist/server/telemetry.js +3 -3
  222. package/dist/server/telemetry.js.map +1 -1
  223. package/dist/server/testsFiles/parser.js +45 -5
  224. package/dist/server/testsFiles/parser.js.map +1 -1
  225. package/dist/server/utils.d.ts +23 -0
  226. package/dist/server/utils.js +114 -15
  227. package/dist/server/utils.js.map +1 -1
  228. package/dist/server/webdriver.d.ts +1 -1
  229. package/dist/server/worker/context.d.ts +3 -0
  230. package/dist/server/worker/context.js +15 -0
  231. package/dist/server/worker/context.js.map +1 -0
  232. package/dist/server/worker/match-image.d.ts +8 -12
  233. package/dist/server/worker/match-image.js +11 -178
  234. package/dist/server/worker/match-image.js.map +1 -1
  235. package/dist/server/worker/start.d.ts +2 -2
  236. package/dist/server/worker/start.js +41 -64
  237. package/dist/server/worker/start.js.map +1 -1
  238. package/dist/shared/index.d.ts +1 -1
  239. package/dist/shared/index.js +9 -7
  240. package/dist/shared/index.js.map +1 -1
  241. package/dist/types.d.ts +84 -43
  242. package/dist/types.js +65 -1
  243. package/dist/types.js.map +1 -1
  244. package/docs/cli.md +80 -0
  245. package/docs/config.md +179 -165
  246. package/docs/examples/playwright-reporer/playwright.config.ts +37 -0
  247. package/docs/migration-0.9-to-0.10.md +144 -0
  248. package/docs/playwright-reporter.md +357 -0
  249. package/docs/storybook.md +60 -0
  250. package/docs/tests.md +50 -45
  251. package/package.json +78 -83
  252. package/playwright.config.mts +46 -0
  253. package/src/client/addon/components/Addon.tsx +1 -1
  254. package/src/client/addon/components/Panel.tsx +4 -4
  255. package/src/client/addon/components/TestSelect.tsx +2 -2
  256. package/src/client/addon/components/Tools.tsx +2 -2
  257. package/src/client/addon/controller.ts +4 -4
  258. package/src/client/addon/makeDecorator.ts +69 -0
  259. package/src/client/addon/manager.ts +38 -37
  260. package/src/client/addon/preset.ts +2 -1
  261. package/src/client/addon/withCreevey.ts +16 -19
  262. package/src/client/shared/components/ImagesView/BlendView.tsx +1 -1
  263. package/src/client/shared/components/ImagesView/ImagesView.tsx +1 -1
  264. package/src/client/shared/components/ImagesView/SideBySideView.tsx +2 -2
  265. package/src/client/shared/components/ImagesView/SlideView.tsx +2 -2
  266. package/src/client/shared/components/ImagesView/SwapView.tsx +20 -2
  267. package/src/client/shared/components/ImagesView/common.ts +1 -1
  268. package/src/client/shared/components/PageFooter/PageFooter.tsx +1 -1
  269. package/src/client/shared/components/PageFooter/Paging.tsx +1 -1
  270. package/src/client/shared/components/PageHeader/ImagePreview.tsx +2 -1
  271. package/src/client/shared/components/PageHeader/PageHeader.tsx +23 -7
  272. package/src/client/shared/components/ResultsPage.tsx +33 -10
  273. package/src/client/shared/creeveyClientApi.ts +19 -1
  274. package/src/client/shared/helpers.ts +4 -24
  275. package/src/client/web/CreeveyApp.tsx +30 -9
  276. package/src/client/web/CreeveyContext.tsx +11 -0
  277. package/src/client/web/CreeveyLoader.tsx +2 -2
  278. package/src/client/web/CreeveyView/SideBar/Checkbox.tsx +3 -3
  279. package/src/client/web/CreeveyView/SideBar/Search.tsx +4 -4
  280. package/src/client/web/CreeveyView/SideBar/SideBar.tsx +11 -6
  281. package/src/client/web/CreeveyView/SideBar/SideBarFooter.tsx +48 -15
  282. package/src/client/web/CreeveyView/SideBar/SideBarHeader.tsx +20 -5
  283. package/src/client/web/CreeveyView/SideBar/SuiteLink.tsx +12 -12
  284. package/src/client/web/CreeveyView/SideBar/TestLink.tsx +10 -10
  285. package/src/client/web/CreeveyView/SideBar/TestStatusIcon.tsx +2 -2
  286. package/src/client/web/CreeveyView/SideBar/TestsStatus.tsx +3 -2
  287. package/src/client/web/CreeveyView/SideBar/Toggle.tsx +1 -1
  288. package/src/client/web/KeyboardEventsContext.tsx +61 -73
  289. package/src/client/web/index.tsx +10 -5
  290. package/src/client/web/themes.ts +24 -0
  291. package/src/creevey.ts +92 -38
  292. package/src/playwright/generator.ts +322 -0
  293. package/src/playwright/helpers.ts +31 -0
  294. package/src/playwright/reporter.ts +381 -0
  295. package/src/playwright/setup.ts +84 -0
  296. package/src/playwright.ts +1 -0
  297. package/src/server/compare.ts +260 -0
  298. package/src/server/config.ts +52 -9
  299. package/src/server/connection.ts +26 -0
  300. package/src/server/docker.ts +62 -34
  301. package/src/server/index.ts +166 -79
  302. package/src/server/master/api.ts +94 -28
  303. package/src/server/master/handlers/capture-handler.ts +20 -0
  304. package/src/server/master/handlers/index.ts +4 -0
  305. package/src/server/master/handlers/ping-handler.ts +6 -0
  306. package/src/server/master/handlers/static-handler.ts +16 -0
  307. package/src/server/master/handlers/stories-handler.ts +20 -0
  308. package/src/server/master/master.ts +10 -27
  309. package/src/server/master/pool.ts +7 -3
  310. package/src/server/master/queue.ts +21 -7
  311. package/src/server/master/runner.ts +123 -134
  312. package/src/server/master/server.ts +214 -101
  313. package/src/server/master/start.ts +19 -41
  314. package/src/server/master/testsManager.ts +316 -0
  315. package/src/server/playwright/docker-file.ts +20 -8
  316. package/src/server/playwright/docker.ts +16 -3
  317. package/src/server/playwright/index-source.mjs +16 -0
  318. package/src/server/playwright/internal.ts +176 -103
  319. package/src/server/playwright/webdriver.ts +4 -10
  320. package/src/server/providers/browser.ts +4 -3
  321. package/src/server/providers/hybrid.ts +2 -3
  322. package/src/server/report.ts +51 -0
  323. package/src/server/reporters/creevey.ts +71 -0
  324. package/src/server/reporters/index.ts +11 -0
  325. package/src/server/reporters/junit.ts +207 -0
  326. package/src/server/reporters/teamcity.ts +74 -0
  327. package/src/server/selenium/internal.ts +70 -53
  328. package/src/server/selenium/selenoid.ts +13 -6
  329. package/src/server/selenium/webdriver.ts +8 -12
  330. package/src/server/shutdown.ts +19 -0
  331. package/src/server/stories.ts +1 -12
  332. package/src/server/telemetry.ts +3 -3
  333. package/src/server/testsFiles/parser.ts +52 -4
  334. package/src/server/utils.ts +124 -16
  335. package/src/server/webdriver.ts +1 -1
  336. package/src/server/worker/context.ts +14 -0
  337. package/src/server/worker/match-image.ts +16 -248
  338. package/src/server/worker/start.ts +49 -79
  339. package/src/shared/index.ts +10 -8
  340. package/src/types.ts +91 -58
  341. package/types/global.d.ts +1 -0
  342. package/dist/client/web/assets/index-DB8lHlJw.js +0 -591
  343. package/dist/server/reporter.d.ts +0 -26
  344. package/dist/server/reporter.js +0 -108
  345. package/dist/server/reporter.js.map +0 -1
  346. package/dist/server/update.d.ts +0 -2
  347. package/dist/server/update.js +0 -53
  348. package/dist/server/update.js.map +0 -1
  349. package/src/server/reporter.ts +0 -139
  350. package/src/server/update.ts +0 -74
@@ -0,0 +1,260 @@
1
+ import path from 'path';
2
+ import { Stats } from 'fs';
3
+ import assert from 'assert';
4
+ import { PNG } from 'pngjs';
5
+ import type { ODiffOptions } from 'odiff-bin';
6
+ import type { PixelmatchOptions } from 'pixelmatch';
7
+ import { mkdir, readdir, readFile, stat, writeFile } from 'fs/promises';
8
+ import { Images } from '../types';
9
+
10
+ export interface ImageContext {
11
+ attachments: string[];
12
+ testFullPath: string[];
13
+ images: Partial<Record<string, Images>>;
14
+ }
15
+
16
+ interface ImagePaths {
17
+ imageName: string;
18
+ actualImageName: string;
19
+ expectImageName: string;
20
+ diffImageName: string;
21
+ expectImageDir: string;
22
+ reportImageDir: string;
23
+ }
24
+
25
+ async function getStat(filePath: string): Promise<Stats | null> {
26
+ try {
27
+ return await stat(filePath);
28
+ } catch (error) {
29
+ if (typeof error == 'object' && error && (error as { code?: unknown }).code === 'ENOENT') {
30
+ return null;
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ async function getLastImageNumber(imageDir: string, imageName: string): Promise<number> {
37
+ const actualImagesRegexp = new RegExp(`${imageName}-actual-(\\d+)\\.png`);
38
+
39
+ try {
40
+ return (
41
+ (await readdir(imageDir))
42
+ .map((filename) => filename.replace(actualImagesRegexp, '$1'))
43
+ .map(Number)
44
+ .filter((x) => !isNaN(x))
45
+ .sort((a, b) => b - a)[0] ?? 0
46
+ );
47
+ } catch (_error) {
48
+ return 0;
49
+ }
50
+ }
51
+
52
+ async function readExpected(expectImageDir: string, imageName: string): Promise<Buffer> {
53
+ const expected = await readFile(path.join(expectImageDir, `${imageName}.png`));
54
+
55
+ return expected;
56
+ }
57
+
58
+ async function saveImages(imageDir: string, images: { name: string; data: Buffer }[]): Promise<string[]> {
59
+ const files: string[] = [];
60
+ await mkdir(imageDir, { recursive: true });
61
+ for (const { name, data } of images) {
62
+ const filePath = path.join(imageDir, name);
63
+ await writeFile(filePath, data);
64
+ files.push(filePath);
65
+ }
66
+ return files;
67
+ }
68
+
69
+ async function getImagePaths(
70
+ config: { screenDir: string; reportDir: string },
71
+ testFullPath: string[],
72
+ assertImageName?: string,
73
+ ): Promise<ImagePaths> {
74
+ const testPath = [...testFullPath];
75
+ const imageName = assertImageName ?? testPath.pop();
76
+
77
+ assert(typeof imageName === 'string', `Can't get image name from empty test scope`);
78
+
79
+ const expectImageDir = path.join(config.screenDir, ...testPath);
80
+ const reportImageDir = path.join(config.reportDir, ...testPath);
81
+ const imageNumber = (await getLastImageNumber(reportImageDir, imageName)) + 1;
82
+ const actualImageName = `${imageName}-actual-${imageNumber}.png`;
83
+ const expectImageName = `${imageName}-expect-${imageNumber}.png`;
84
+ const diffImageName = `${imageName}-diff-${imageNumber}.png`;
85
+
86
+ return { imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir };
87
+ }
88
+
89
+ async function getExpected(
90
+ ctx: ImageContext,
91
+ { imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir }: ImagePaths,
92
+ ): Promise<{
93
+ expected: Buffer | null;
94
+ onCompare: (actual: Buffer, expect?: Buffer, diff?: Buffer) => Promise<void>;
95
+ }> {
96
+ const onCompare = async (actual: Buffer, expect?: Buffer, diff?: Buffer): Promise<void> => {
97
+ const imagesMeta: { name: string; data: Buffer }[] = [];
98
+ const image = (ctx.images[imageName] = ctx.images[imageName] ?? { actual: actualImageName });
99
+
100
+ imagesMeta.push({ name: image.actual, data: actual });
101
+
102
+ if (diff && expect) {
103
+ image.expect = expectImageName;
104
+ image.diff = diffImageName;
105
+ imagesMeta.push({ name: image.expect, data: expect });
106
+ imagesMeta.push({ name: image.diff, data: diff });
107
+ }
108
+ ctx.attachments = await saveImages(reportImageDir, imagesMeta);
109
+ };
110
+
111
+ const expectImageStat = await getStat(path.join(expectImageDir, `${imageName}.png`));
112
+ if (!expectImageStat) return { expected: null, onCompare };
113
+
114
+ const expected = await readExpected(expectImageDir, imageName);
115
+
116
+ return { expected, onCompare };
117
+ }
118
+
119
+ async function getOdiffExpected(
120
+ ctx: ImageContext,
121
+ actual: Buffer,
122
+ { imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir }: ImagePaths,
123
+ ): Promise<{ actual: string; expect: string; diff: string }> {
124
+ const expected = await readExpected(expectImageDir, imageName);
125
+
126
+ const image = (ctx.images[imageName] = ctx.images[imageName] ?? { actual: actualImageName });
127
+ image.expect = expectImageName;
128
+ image.diff = diffImageName;
129
+
130
+ const imagesMeta = [
131
+ { name: image.actual, data: actual },
132
+ { name: expectImageName, data: expected },
133
+ ];
134
+
135
+ ctx.attachments = await saveImages(reportImageDir, imagesMeta);
136
+
137
+ return {
138
+ actual: path.join(reportImageDir, actualImageName),
139
+ expect: path.join(reportImageDir, expectImageName),
140
+ diff: path.join(reportImageDir, diffImageName),
141
+ };
142
+ }
143
+
144
+ function normalizeImageSize(image: PNG, width: number, height: number): Buffer {
145
+ const normalizedImage = Buffer.alloc(4 * width * height);
146
+
147
+ for (let y = 0; y < height; y++) {
148
+ for (let x = 0; x < width; x++) {
149
+ const i = (y * width + x) * 4;
150
+ if (x < image.width && y < image.height) {
151
+ const j = (y * image.width + x) * 4;
152
+ normalizedImage[i + 0] = image.data[j + 0];
153
+ normalizedImage[i + 1] = image.data[j + 1];
154
+ normalizedImage[i + 2] = image.data[j + 2];
155
+ normalizedImage[i + 3] = image.data[j + 3];
156
+ } else {
157
+ normalizedImage[i + 0] = 0;
158
+ normalizedImage[i + 1] = 0;
159
+ normalizedImage[i + 2] = 0;
160
+ normalizedImage[i + 3] = 0;
161
+ }
162
+ }
163
+ }
164
+ return normalizedImage;
165
+ }
166
+
167
+ function hasDiffPixels(diff: Buffer): boolean {
168
+ for (let i = 0; i < diff.length; i += 4) {
169
+ if (diff[i + 0] == 255 && diff[i + 1] == 0 && diff[i + 2] == 0 && diff[i + 3] == 255) return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ function compareImages(
175
+ expect: Buffer,
176
+ actual: Buffer,
177
+ pixelmatch: typeof import('pixelmatch'),
178
+ diffOptions: PixelmatchOptions,
179
+ ): { isEqual: boolean; diff: Buffer } {
180
+ const expectImage = PNG.sync.read(expect);
181
+ const actualImage = PNG.sync.read(actual);
182
+
183
+ const width = Math.max(actualImage.width, expectImage.width);
184
+ const height = Math.max(actualImage.height, expectImage.height);
185
+
186
+ const diffImage = new PNG({ width, height });
187
+
188
+ let actualImageData = actualImage.data;
189
+ if (actualImage.width < width || actualImage.height < height) {
190
+ actualImageData = normalizeImageSize(actualImage, width, height);
191
+ }
192
+
193
+ let expectImageData = expectImage.data;
194
+ if (expectImage.width < width || expectImage.height < height) {
195
+ expectImageData = normalizeImageSize(expectImage, width, height);
196
+ }
197
+
198
+ pixelmatch(expectImageData, actualImageData, diffImage.data, width, height, diffOptions);
199
+
200
+ return {
201
+ isEqual: !hasDiffPixels(diffImage.data),
202
+ diff: PNG.sync.write(diffImage),
203
+ };
204
+ }
205
+
206
+ export function getPixelmatchAssert(
207
+ pixelmatch: typeof import('pixelmatch'),
208
+ ctx: ImageContext,
209
+ config: { screenDir: string; reportDir: string; diffOptions: PixelmatchOptions },
210
+ ) {
211
+ return async function assertImagePixelmatch(actual: Buffer, imageName?: string): Promise<string | undefined> {
212
+ const { expected, onCompare } = await getExpected(ctx, await getImagePaths(config, ctx.testFullPath, imageName));
213
+
214
+ if (expected == null) {
215
+ await onCompare(actual);
216
+ return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
217
+ }
218
+
219
+ if (actual.equals(expected)) {
220
+ await onCompare(actual);
221
+ return;
222
+ }
223
+
224
+ const { isEqual, diff } = compareImages(expected, actual, pixelmatch, config.diffOptions);
225
+
226
+ if (isEqual) {
227
+ await onCompare(actual);
228
+ return;
229
+ }
230
+
231
+ await onCompare(actual, expected, diff);
232
+
233
+ return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
234
+ };
235
+ }
236
+
237
+ export function getOdiffAssert(
238
+ compare: (typeof import('odiff-bin'))['compare'],
239
+ ctx: ImageContext,
240
+ config: { screenDir: string; reportDir: string; odiffOptions?: ODiffOptions },
241
+ ) {
242
+ const diffOptions = {
243
+ ...config.odiffOptions,
244
+ noFailOnFsErrors: true,
245
+ };
246
+ return async function assertImage(image: Buffer, imageName?: string): Promise<string | undefined> {
247
+ const { actual, expect, diff } = await getOdiffExpected(
248
+ ctx,
249
+ image,
250
+ await getImagePaths(config, ctx.testFullPath, imageName),
251
+ );
252
+ const result = await compare(actual, expect, diff, diffOptions);
253
+ if (!result.match) {
254
+ if (result.reason == 'file-not-exists') {
255
+ return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
256
+ }
257
+ return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
258
+ }
259
+ };
260
+ }
@@ -1,34 +1,46 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import cluster from 'cluster';
3
4
  import { pathToFileURL } from 'url';
4
- import { loadStories as browserStoriesProvider } from './providers/browser.js';
5
- import { Config, BrowserConfig, BrowserConfigObject, Options, isDefined } from '../types.js';
5
+ import * as v from 'valibot';
6
+ import { loadStories as hybridStoriesProvider } from './providers/hybrid.js';
7
+ import {
8
+ Config,
9
+ BrowserConfig,
10
+ BrowserConfigObject,
11
+ Options,
12
+ isDefined,
13
+ WorkerOptions,
14
+ OptionsSchema,
15
+ } from '../types.js';
6
16
  import { configExt, loadThroughTSX } from './utils.js';
7
- import { CreeveyReporter, TeamcityReporter } from './reporter.js';
17
+ import { CreeveyReporter } from './reporters/creevey.js';
18
+ import { TeamcityReporter } from './reporters/teamcity.js';
8
19
  import { logger } from './logger.js';
9
20
 
10
21
  export const defaultBrowser = 'chrome';
11
22
 
12
- export const defaultConfig: Omit<Config, 'gridUrl' | 'testsDir' | 'tsConfig' | 'webdriver'> = {
23
+ export const defaultConfig: Omit<Config, 'gridUrl' | 'tsConfig' | 'webdriver'> = {
13
24
  disableTelemetry: false,
14
25
  useWorkerQueue: false,
15
26
  useDocker: true,
16
- dockerImage: 'aerokube/selenoid:latest-release',
27
+ dockerImage: 'aerokube/selenoid:latest', // TODO What about playwright?
17
28
  dockerImagePlatform: '',
18
29
  pullImages: true,
19
30
  failFast: false,
20
31
  storybookUrl: 'http://localhost:6006',
21
32
  screenDir: path.resolve('images'),
22
33
  reportDir: path.resolve('report'),
34
+ testsDir: path.resolve('src'),
23
35
  reporter: process.env.TEAMCITY_VERSION ? TeamcityReporter : CreeveyReporter,
24
- storiesProvider: browserStoriesProvider,
36
+ storiesProvider: hybridStoriesProvider,
25
37
  maxRetries: 0,
26
38
  testTimeout: 30000,
27
39
  diffOptions: { threshold: 0.1, includeAA: false },
28
40
  odiffOptions: { threshold: 0.1, antialiasing: true },
29
41
  browsers: { [defaultBrowser]: true },
30
42
  hooks: {},
31
- testsRegex: /\.creevey\.(t|j)s$/,
43
+ testsRegex: /\.creevey\.(m|c)?(t|j)s$/,
32
44
  };
33
45
 
34
46
  function normalizeBrowserConfig(name: string, config: BrowserConfig): BrowserConfigObject {
@@ -57,7 +69,7 @@ function resolveConfigPath(configPath?: string): string | undefined {
57
69
  return configPath;
58
70
  }
59
71
 
60
- export async function readConfig(options: Options): Promise<Config> {
72
+ export async function readConfig(options: Options | WorkerOptions): Promise<Config> {
61
73
  const configPath = resolveConfigPath(options.config);
62
74
  const userConfig: typeof defaultConfig & Partial<Pick<Config, 'gridUrl' | 'storiesProvider'>> = { ...defaultConfig };
63
75
 
@@ -80,14 +92,45 @@ export async function readConfig(options: Options): Promise<Config> {
80
92
  configData.webdriver = SeleniumWebdriver;
81
93
  }
82
94
 
95
+ for (const key in configData) {
96
+ const configKey = key as keyof typeof configData;
97
+ if (configData[configKey] === undefined) {
98
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
99
+ delete configData[configKey];
100
+ }
101
+ }
102
+
83
103
  Object.assign(userConfig, configData);
84
104
  }
85
105
 
86
- if (options.failFast != undefined) userConfig.failFast = Boolean(options.failFast);
106
+ if (userConfig.resolveStorybookUrl && !options.storybookUrl) {
107
+ userConfig.storybookUrl = await userConfig.resolveStorybookUrl();
108
+ }
109
+
87
110
  if (options.reportDir) userConfig.reportDir = path.resolve(options.reportDir);
88
111
  if (options.screenDir) userConfig.screenDir = path.resolve(options.screenDir);
89
112
  if (options.storybookUrl) userConfig.storybookUrl = options.storybookUrl;
90
113
 
114
+ if (v.is(OptionsSchema, options)) {
115
+ if (options.docker === false) userConfig.useDocker = false;
116
+ if (options.failFast != undefined) userConfig.failFast = Boolean(options.failFast);
117
+ if (cluster.isPrimary) {
118
+ if (options.storybookPort) {
119
+ const url = new URL(userConfig.storybookUrl);
120
+ url.port = `${options.storybookPort}`;
121
+ userConfig.storybookUrl = url.toString();
122
+ }
123
+ if (typeof options.storybookStart === 'string') userConfig.storybookAutorunCmd = options.storybookStart;
124
+ else if (options.storybookStart) {
125
+ const { default: getPort } = await import('get-port');
126
+ const url = new URL(userConfig.storybookUrl);
127
+ const port = await getPort({ port: Number(url.port) });
128
+ url.port = `${port}`;
129
+ userConfig.storybookUrl = url.toString();
130
+ }
131
+ }
132
+ }
133
+
91
134
  // NOTE: Hack to pass typescript checking
92
135
  const config = userConfig as Config;
93
136
 
@@ -0,0 +1,26 @@
1
+ import type { Config, Options } from '../types';
2
+ import { waitOnUrl } from './utils.js';
3
+ import { logger } from './logger.js';
4
+
5
+ const RESPONSE_CHECK_TIMEOUT_MS = 10000;
6
+ const RESPONSE_CHECK_INTERVAL_MS = 200;
7
+
8
+ export function getStorybookUrl({ storybookUrl }: Config, { storybookStart }: Options): [string, string | undefined] {
9
+ if (storybookStart) {
10
+ const url = new URL(storybookUrl);
11
+ url.hostname = 'localhost';
12
+ return [url.toString(), storybookUrl];
13
+ }
14
+ return [storybookUrl, undefined];
15
+ }
16
+
17
+ export async function checkIsStorybookConnected(url: string) {
18
+ try {
19
+ await waitOnUrl(url, RESPONSE_CHECK_TIMEOUT_MS, RESPONSE_CHECK_INTERVAL_MS);
20
+ return true;
21
+ } catch (reason: unknown) {
22
+ const error = reason instanceof Error ? (reason.stack ?? reason.message) : (reason as string);
23
+ logger().error(error);
24
+ return false;
25
+ }
26
+ }
@@ -1,9 +1,10 @@
1
1
  import tar from 'tar-stream';
2
+ import Logger from 'loglevel';
2
3
  import { Writable } from 'stream';
3
4
  import Dockerode, { Container } from 'dockerode';
4
5
  import { DockerAuth } from '../types.js';
5
- import { subscribeOn } from './messages.js';
6
6
  import { logger } from './logger.js';
7
+ import { setWorkerContainer } from './worker/context.js';
7
8
 
8
9
  const docker = new Dockerode();
9
10
 
@@ -50,17 +51,45 @@ export async function pullImages(
50
51
  function onProgress(event: { id: string; status: string; progress?: string }): void {
51
52
  if (!/^[a-z0-9]{12}$/i.test(event.id)) return;
52
53
 
53
- spinner.text = `${image}: [${event.id}] ${event.status} ${event.progress ? event.progress : ''}`;
54
+ spinner.text = `${image}: [${event.id}] ${event.status} ${event.progress ?? ''}`;
54
55
  }
55
56
  });
56
57
  });
57
58
  }
58
59
  }
59
60
 
60
- export async function buildImage(imageName: string, dockerfile: string): Promise<void> {
61
+ export async function buildImage(imageName: string, version: string, dockerfile: string): Promise<void> {
61
62
  const images = await docker.listImages({ filters: { label: [`creevey=${imageName}`] } });
62
63
 
63
- if (images.at(0)) {
64
+ const containers = await docker.listContainers({ all: true, filters: { label: [`creevey=${imageName}`] } });
65
+ if (containers.length > 0) {
66
+ await Promise.all(
67
+ containers.map(async (info) => {
68
+ const container = docker.getContainer(info.Id);
69
+ try {
70
+ await container.remove({ force: true });
71
+ } catch {
72
+ /* noop */
73
+ }
74
+ }),
75
+ );
76
+ }
77
+
78
+ const oldImages = images.filter((info) => info.Labels.version !== version);
79
+ if (oldImages.length > 0) {
80
+ await Promise.all(
81
+ oldImages.map(async (info) => {
82
+ const image = docker.getImage(info.Id);
83
+ try {
84
+ await image.remove({ force: true });
85
+ } catch {
86
+ /* noop */
87
+ }
88
+ }),
89
+ );
90
+ }
91
+
92
+ if (oldImages.length !== images.length) {
64
93
  logger().info(`Image ${imageName} already exists`);
65
94
  return;
66
95
  }
@@ -70,15 +99,19 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
70
99
  pack.finalize();
71
100
 
72
101
  const { default: yoctoSpinner } = await import('yocto-spinner');
73
- const spinner = yoctoSpinner({ text: `${imageName}: Build start` }).start();
102
+ const spinner = yoctoSpinner({ text: `${imageName}: Build start` });
103
+ if (logger().getLevel() > Logger.levels.DEBUG) {
104
+ spinner.start();
105
+ }
106
+ let isFailed = false;
74
107
  await new Promise<void>((resolve, reject) => {
75
- void docker.buildImage(
76
- // @ts-expect-error Type incompatibility AsyncIterator and AsyncIterableIterator
108
+ docker.buildImage(
77
109
  pack,
78
- { t: imageName, labels: { creevey: imageName } },
110
+ // TODO Support buildkit decode grpc (version: '2')
111
+ { t: imageName, labels: { creevey: imageName, version }, version: '1' },
79
112
  (buildError: Error | null, stream) => {
80
113
  if (buildError || !stream) {
81
- spinner.error(buildError?.message);
114
+ // spinner.error(buildError?.message);
82
115
  reject(buildError ?? new Error('Unknown error'));
83
116
  return;
84
117
  }
@@ -86,6 +119,8 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
86
119
  docker.modem.followProgress(stream, onFinished, onProgress);
87
120
 
88
121
  function onFinished(error: Error | null): void {
122
+ if (isFailed) return;
123
+
89
124
  if (error) {
90
125
  spinner.error(error.message);
91
126
  reject(error);
@@ -95,10 +130,23 @@ export async function buildImage(imageName: string, dockerfile: string): Promise
95
130
  resolve();
96
131
  }
97
132
 
98
- function onProgress(event: { id: string; status: string; progress?: string }): void {
99
- if (!/^[a-z0-9]{12}$/i.test(event.id)) return;
100
-
101
- spinner.text = `${imageName}: [${event.id}] ${event.status} ${event.progress ? event.progress : ''}`;
133
+ function onProgress(
134
+ event:
135
+ | { stream: string }
136
+ | { errorDetail: { code: number; message: string }; error: string }
137
+ | { id: string; aux: string }, // NOTE: Only with `version: '2'`
138
+ ): void {
139
+ if ('stream' in event) {
140
+ if (logger().getLevel() <= Logger.levels.DEBUG) {
141
+ logger().debug(event.stream.trim());
142
+ } else {
143
+ spinner.text = `${imageName}: [Build] - ${event.stream}`;
144
+ }
145
+ } else if ('errorDetail' in event) {
146
+ isFailed = true;
147
+ spinner.error(event.error);
148
+ reject(new Error(event.error));
149
+ }
102
150
  }
103
151
  },
104
152
  );
@@ -111,33 +159,13 @@ export async function runImage(
111
159
  options: Record<string, unknown>,
112
160
  debug: boolean,
113
161
  ): Promise<string> {
114
- await Promise.all(
115
- (await docker.listContainers({ all: true, filters: { ancestor: [image] } })).map(async (info) => {
116
- const container = docker.getContainer(info.Id);
117
- try {
118
- await container.stop();
119
- } catch {
120
- /* noop */
121
- }
122
- await container.remove();
123
- }),
124
- );
125
-
126
162
  const hub = docker.run(image, args, debug ? process.stdout : new DevNull(), options, (error) => {
127
163
  if (error) throw error;
128
164
  });
129
165
 
130
166
  return new Promise((resolve) => {
131
167
  hub.once('container', (container: Container) => {
132
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
133
- subscribeOn('shutdown', async () => {
134
- try {
135
- await container.stop();
136
- await container.remove();
137
- } catch {
138
- /* noop */
139
- }
140
- });
168
+ setWorkerContainer(container);
141
169
  });
142
170
  hub.once(
143
171
  'start',