plum-e2e 2.8.5 → 2.9.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 (235) hide show
  1. package/backend/_scaffold/utils/browser.ts +184 -30
  2. package/backend/_scaffold/utils/hooks.ts +37 -8
  3. package/backend/app.js +0 -5
  4. package/backend/config/scripts/generate-report.js +2 -2
  5. package/backend/constants/socketEvents.js +7 -4
  6. package/backend/lib/reportFilename.js +1 -2
  7. package/backend/lib/{screenshotPoller.js → rrwebPoller.js} +7 -4
  8. package/backend/lib/serverBootstrap.js +14 -0
  9. package/backend/logs/runner-cmtbz5b1l0000mr0110b27w5k.log +8 -0
  10. package/backend/mcp/server.js +3 -47
  11. package/backend/package-lock.json +199 -1
  12. package/backend/package.json +3 -1
  13. package/backend/prisma/migrations/20260828120000_add_recording_and_split_runner_worker_count/migration.sql +39 -0
  14. package/backend/prisma/migrations/20260828140000_add_recording_started_ended_at/migration.sql +5 -0
  15. package/backend/prisma/migrations/20260828150000_strip_screenshot_refs_from_reports/migration.sql +34 -0
  16. package/backend/prisma/migrations/20260828160000_add_backup_include_reports/migration.sql +4 -0
  17. package/backend/prisma/schema.prisma +97 -70
  18. package/backend/routes/backup.routes.js +47 -1
  19. package/backend/routes/reports.routes.js +23 -0
  20. package/backend/routes/test-cases.routes.js +7 -2
  21. package/backend/server.js +1 -1
  22. package/backend/services/backupCronService.js +1 -1
  23. package/backend/services/backupService.js +134 -44
  24. package/backend/services/cronService.js +23 -15
  25. package/backend/services/nodeExecutionService.js +41 -15
  26. package/backend/services/nodeStreamRegistry.js +24 -0
  27. package/backend/services/reportService.js +174 -84
  28. package/backend/services/runnerService.js +19 -7
  29. package/backend/services/settingsService.js +6 -3
  30. package/backend/services/testCaseService.js +3 -2
  31. package/backend/services/triggerService.js +20 -13
  32. package/backend/services/userService.js +1 -3
  33. package/backend/websockets/nodeSocketHandler.js +40 -0
  34. package/backend/websockets/socketHandler.js +9 -7
  35. package/frontend/.svelte-kit/adapter-node/.vite/manifest.json +1 -1
  36. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.C1TbMUws.css +6 -0
  37. package/frontend/.svelte-kit/adapter-node/chunks/internal.js +1 -1
  38. package/frontend/.svelte-kit/adapter-node/entries/pages/_page.svelte.js +22 -22
  39. package/frontend/.svelte-kit/adapter-node/entries/pages/settings/_page.svelte.js +3 -3
  40. package/frontend/.svelte-kit/adapter-node/manifest-full.js +1 -1
  41. package/frontend/.svelte-kit/adapter-node/manifest.js +1 -1
  42. package/frontend/.svelte-kit/adapter-node/nodes/0.js +1 -1
  43. package/frontend/.svelte-kit/adapter-node/nodes/1.js +1 -1
  44. package/frontend/.svelte-kit/adapter-node/nodes/11.js +1 -1
  45. package/frontend/.svelte-kit/adapter-node/nodes/12.js +1 -1
  46. package/frontend/.svelte-kit/adapter-node/nodes/2.js +2 -2
  47. package/frontend/.svelte-kit/adapter-node/nodes/3.js +1 -1
  48. package/frontend/.svelte-kit/adapter-node/nodes/5.js +1 -1
  49. package/frontend/.svelte-kit/adapter-node/nodes/6.js +1 -1
  50. package/frontend/.svelte-kit/adapter-node/nodes/8.js +1 -1
  51. package/frontend/.svelte-kit/adapter-node/nodes/9.js +1 -1
  52. package/frontend/.svelte-kit/ambient.d.ts +4 -0
  53. package/frontend/.svelte-kit/generated/server/internal.js +1 -1
  54. package/frontend/.svelte-kit/output/client/.vite/manifest.json +35 -35
  55. package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.C1TbMUws.css +6 -0
  56. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.C1TbMUws.css +6 -0
  57. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/{DEKAA-Fm.js → DDZw0L0T.js} +1 -1
  58. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/{Dhf4xQKo.js → kkA1mrsH.js} +2 -2
  59. package/frontend/.svelte-kit/output/client/_app/immutable/entry/{app.B0GH5ONo.js → app.KrtMZ3my.js} +2 -2
  60. package/frontend/.svelte-kit/output/client/_app/immutable/entry/{start.Bn25CW9X.js → start.4qxHHAcl.js} +1 -1
  61. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{0.-imXt0f5.js → 0.C8DodwXs.js} +1 -1
  62. package/frontend/{build/client/_app/immutable/nodes/1.Dom3CKWF.js → .svelte-kit/output/client/_app/immutable/nodes/1.Df9sLw8b.js} +1 -1
  63. package/frontend/{build/client/_app/immutable/nodes/11.DMlayLCK.js → .svelte-kit/output/client/_app/immutable/nodes/11.BViaDK2m.js} +1 -1
  64. package/frontend/{build/client/_app/immutable/nodes/12.Bu89A1eX.js → .svelte-kit/output/client/_app/immutable/nodes/12.BMSCSd88.js} +1 -1
  65. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.BCPSg7H4.js +6 -0
  66. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{3.B86rlHXU.js → 3.8BFAJoHT.js} +1 -1
  67. package/frontend/{build/client/_app/immutable/nodes/5.CqTXvadV.js → .svelte-kit/output/client/_app/immutable/nodes/5.DVeH2eQP.js} +1 -1
  68. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/{6.BJZ62MCd.js → 6.DI7-IJhN.js} +1 -1
  69. package/frontend/{build/client/_app/immutable/nodes/8.B4fj2Xyu.js → .svelte-kit/output/client/_app/immutable/nodes/8.93Mb3ofT.js} +4 -4
  70. package/frontend/{build/client/_app/immutable/nodes/9.C57_2xq6.js → .svelte-kit/output/client/_app/immutable/nodes/9.CvfGx3Hu.js} +1 -1
  71. package/frontend/.svelte-kit/output/client/_app/version.json +1 -1
  72. package/frontend/.svelte-kit/output/server/.vite/manifest.json +1 -1
  73. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.C1TbMUws.css +6 -0
  74. package/frontend/.svelte-kit/output/server/chunks/internal.js +1 -1
  75. package/frontend/.svelte-kit/output/server/entries/pages/_page.svelte.js +22 -22
  76. package/frontend/.svelte-kit/output/server/entries/pages/settings/_page.svelte.js +3 -3
  77. package/frontend/.svelte-kit/output/server/manifest-full.js +1 -1
  78. package/frontend/.svelte-kit/output/server/manifest.js +1 -1
  79. package/frontend/.svelte-kit/output/server/nodes/0.js +1 -1
  80. package/frontend/.svelte-kit/output/server/nodes/1.js +1 -1
  81. package/frontend/.svelte-kit/output/server/nodes/11.js +1 -1
  82. package/frontend/.svelte-kit/output/server/nodes/12.js +1 -1
  83. package/frontend/.svelte-kit/output/server/nodes/2.js +2 -2
  84. package/frontend/.svelte-kit/output/server/nodes/3.js +1 -1
  85. package/frontend/.svelte-kit/output/server/nodes/5.js +1 -1
  86. package/frontend/.svelte-kit/output/server/nodes/6.js +1 -1
  87. package/frontend/.svelte-kit/output/server/nodes/8.js +1 -1
  88. package/frontend/.svelte-kit/output/server/nodes/9.js +1 -1
  89. package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css +6 -0
  90. package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css.br +0 -0
  91. package/frontend/build/client/_app/immutable/assets/2.C1TbMUws.css.gz +0 -0
  92. package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css +6 -0
  93. package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css.br +0 -0
  94. package/frontend/build/client/_app/immutable/assets/_page.C1TbMUws.css.gz +0 -0
  95. package/frontend/build/client/_app/immutable/chunks/{DEKAA-Fm.js → DDZw0L0T.js} +1 -1
  96. package/frontend/build/client/_app/immutable/chunks/DDZw0L0T.js.br +0 -0
  97. package/frontend/build/client/_app/immutable/chunks/DDZw0L0T.js.gz +0 -0
  98. package/frontend/build/client/_app/immutable/chunks/{Dhf4xQKo.js → kkA1mrsH.js} +2 -2
  99. package/frontend/build/client/_app/immutable/chunks/kkA1mrsH.js.br +0 -0
  100. package/frontend/build/client/_app/immutable/chunks/kkA1mrsH.js.gz +0 -0
  101. package/frontend/build/client/_app/immutable/entry/{app.B0GH5ONo.js → app.KrtMZ3my.js} +2 -2
  102. package/frontend/build/client/_app/immutable/entry/app.KrtMZ3my.js.br +0 -0
  103. package/frontend/build/client/_app/immutable/entry/app.KrtMZ3my.js.gz +0 -0
  104. package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js → start.4qxHHAcl.js} +1 -1
  105. package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js.br → start.4qxHHAcl.js.br} +0 -0
  106. package/frontend/build/client/_app/immutable/entry/{start.Bn25CW9X.js.gz → start.4qxHHAcl.js.gz} +0 -0
  107. package/frontend/build/client/_app/immutable/nodes/{0.-imXt0f5.js → 0.C8DodwXs.js} +1 -1
  108. package/frontend/build/client/_app/immutable/nodes/0.C8DodwXs.js.br +0 -0
  109. package/frontend/build/client/_app/immutable/nodes/0.C8DodwXs.js.gz +0 -0
  110. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/1.Dom3CKWF.js → build/client/_app/immutable/nodes/1.Df9sLw8b.js} +1 -1
  111. package/frontend/build/client/_app/immutable/nodes/1.Df9sLw8b.js.br +0 -0
  112. package/frontend/build/client/_app/immutable/nodes/1.Df9sLw8b.js.gz +0 -0
  113. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/11.DMlayLCK.js → build/client/_app/immutable/nodes/11.BViaDK2m.js} +1 -1
  114. package/frontend/build/client/_app/immutable/nodes/11.BViaDK2m.js.br +0 -0
  115. package/frontend/build/client/_app/immutable/nodes/11.BViaDK2m.js.gz +0 -0
  116. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/12.Bu89A1eX.js → build/client/_app/immutable/nodes/12.BMSCSd88.js} +1 -1
  117. package/frontend/build/client/_app/immutable/nodes/12.BMSCSd88.js.br +0 -0
  118. package/frontend/build/client/_app/immutable/nodes/{12.Bu89A1eX.js.gz → 12.BMSCSd88.js.gz} +0 -0
  119. package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js +6 -0
  120. package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js.br +0 -0
  121. package/frontend/build/client/_app/immutable/nodes/2.BCPSg7H4.js.gz +0 -0
  122. package/frontend/build/client/_app/immutable/nodes/{3.B86rlHXU.js → 3.8BFAJoHT.js} +1 -1
  123. package/frontend/build/client/_app/immutable/nodes/3.8BFAJoHT.js.br +0 -0
  124. package/frontend/build/client/_app/immutable/nodes/3.8BFAJoHT.js.gz +0 -0
  125. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/5.CqTXvadV.js → build/client/_app/immutable/nodes/5.DVeH2eQP.js} +1 -1
  126. package/frontend/build/client/_app/immutable/nodes/5.DVeH2eQP.js.br +0 -0
  127. package/frontend/build/client/_app/immutable/nodes/5.DVeH2eQP.js.gz +0 -0
  128. package/frontend/build/client/_app/immutable/nodes/{6.BJZ62MCd.js → 6.DI7-IJhN.js} +1 -1
  129. package/frontend/build/client/_app/immutable/nodes/6.DI7-IJhN.js.br +0 -0
  130. package/frontend/build/client/_app/immutable/nodes/6.DI7-IJhN.js.gz +0 -0
  131. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/8.B4fj2Xyu.js → build/client/_app/immutable/nodes/8.93Mb3ofT.js} +4 -4
  132. package/frontend/build/client/_app/immutable/nodes/8.93Mb3ofT.js.br +0 -0
  133. package/frontend/build/client/_app/immutable/nodes/8.93Mb3ofT.js.gz +0 -0
  134. package/frontend/{.svelte-kit/output/client/_app/immutable/nodes/9.C57_2xq6.js → build/client/_app/immutable/nodes/9.CvfGx3Hu.js} +1 -1
  135. package/frontend/build/client/_app/immutable/nodes/9.CvfGx3Hu.js.br +0 -0
  136. package/frontend/build/client/_app/immutable/nodes/9.CvfGx3Hu.js.gz +0 -0
  137. package/frontend/build/client/_app/version.json +1 -1
  138. package/frontend/build/client/_app/version.json.br +0 -0
  139. package/frontend/build/client/_app/version.json.gz +0 -0
  140. package/frontend/build/server/chunks/{0-DG1Uv07a.js → 0-D5YWFygh.js} +2 -2
  141. package/frontend/build/server/chunks/{0-DG1Uv07a.js.map → 0-D5YWFygh.js.map} +1 -1
  142. package/frontend/build/server/chunks/{1-iB0ogJU_.js → 1-Kxn9d30Z.js} +2 -2
  143. package/frontend/build/server/chunks/{1-iB0ogJU_.js.map → 1-Kxn9d30Z.js.map} +1 -1
  144. package/frontend/build/server/chunks/{11-Df9vGZ_v.js → 11-CNLerze_.js} +2 -2
  145. package/frontend/build/server/chunks/{11-Df9vGZ_v.js.map → 11-CNLerze_.js.map} +1 -1
  146. package/frontend/build/server/chunks/{12-DqvjCln4.js → 12-De6Ol6pu.js} +2 -2
  147. package/frontend/build/server/chunks/{12-DqvjCln4.js.map → 12-De6Ol6pu.js.map} +1 -1
  148. package/frontend/build/server/chunks/{2-Cte30Dgy.js → 2-Dc_rKWoK.js} +4 -4
  149. package/frontend/build/server/chunks/{2-Cte30Dgy.js.map → 2-Dc_rKWoK.js.map} +1 -1
  150. package/frontend/build/server/chunks/{3-BYKCZi4U.js → 3-DrRqVrcX.js} +2 -2
  151. package/frontend/build/server/chunks/{3-BYKCZi4U.js.map → 3-DrRqVrcX.js.map} +1 -1
  152. package/frontend/build/server/chunks/{5-B6Jc1foX.js → 5-CFeE5dPZ.js} +2 -2
  153. package/frontend/build/server/chunks/{5-B6Jc1foX.js.map → 5-CFeE5dPZ.js.map} +1 -1
  154. package/frontend/build/server/chunks/{6-dXZKtN86.js → 6-DZk37MZH.js} +2 -2
  155. package/frontend/build/server/chunks/{6-dXZKtN86.js.map → 6-DZk37MZH.js.map} +1 -1
  156. package/frontend/build/server/chunks/{8-D-Bp7HzS.js → 8-CKPVFOqJ.js} +3 -3
  157. package/frontend/build/server/chunks/{8-D-Bp7HzS.js.map → 8-CKPVFOqJ.js.map} +1 -1
  158. package/frontend/build/server/chunks/{9-BynjuGUO.js → 9-DirZG_Lu.js} +2 -2
  159. package/frontend/build/server/chunks/{9-BynjuGUO.js.map → 9-DirZG_Lu.js.map} +1 -1
  160. package/frontend/build/server/chunks/{_page.svelte-CpB-fgJl.js → _page.svelte-04WxjOuH.js} +23 -23
  161. package/frontend/build/server/chunks/_page.svelte-04WxjOuH.js.map +5 -0
  162. package/frontend/build/server/chunks/{_page.svelte-wKyUmA2e.js → _page.svelte-8gELc_B1.js} +4 -4
  163. package/frontend/build/server/chunks/_page.svelte-8gELc_B1.js.map +5 -0
  164. package/frontend/build/server/index.js +1 -1
  165. package/frontend/build/server/index.js.map +1 -1
  166. package/frontend/build/server/manifest.js +11 -11
  167. package/frontend/build/server/manifest.js.map +1 -1
  168. package/frontend/package-lock.json +121 -32
  169. package/frontend/package.json +1 -0
  170. package/frontend/src/lib/api/reports.js +13 -4
  171. package/frontend/src/lib/api/repository.js +2 -2
  172. package/frontend/src/lib/api/settings.js +16 -1
  173. package/frontend/src/lib/components/layout/RunnerPanel.svelte +9 -24
  174. package/frontend/src/lib/components/reports/ElementInspector.svelte +141 -0
  175. package/frontend/src/lib/components/reports/LiveReplayer.svelte +110 -0
  176. package/frontend/src/lib/components/reports/MultiTabTimeline.svelte +115 -0
  177. package/frontend/src/lib/components/reports/RecordingPlayer.svelte +786 -0
  178. package/frontend/src/lib/components/reports/StepsRail.svelte +109 -0
  179. package/frontend/src/lib/components/ui/CodeViewer.svelte +61 -0
  180. package/frontend/src/lib/constants.js +0 -1
  181. package/frontend/src/lib/copy/reports.js +18 -8
  182. package/frontend/src/lib/copy/repository.js +6 -0
  183. package/frontend/src/lib/copy/settings.js +25 -4
  184. package/frontend/src/lib/socketEvents.js +7 -4
  185. package/frontend/src/lib/stores/runner.js +26 -3
  186. package/frontend/src/lib/styles/tokens.css +7 -0
  187. package/frontend/src/lib/utils/format.js +108 -2
  188. package/frontend/src/lib/utils/inspectElement.js +34 -0
  189. package/frontend/src/routes/+page.svelte +6 -1
  190. package/frontend/src/routes/reports/+page.svelte +79 -1
  191. package/frontend/src/routes/reports/[id]/+page.svelte +304 -495
  192. package/frontend/src/routes/reports/live/+page.svelte +236 -260
  193. package/frontend/src/routes/settings/+page.svelte +246 -8
  194. package/frontend/src/routes/test-repository/suites/[id]/+page.svelte +116 -0
  195. package/package.json +1 -1
  196. package/backend/playwright.config.js +0 -85
  197. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
  198. package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.DvMkmYA-.css +0 -6
  199. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
  200. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.BnHalSCi.js +0 -6
  201. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
  202. package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css +0 -6
  203. package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css.br +0 -0
  204. package/frontend/build/client/_app/immutable/assets/2.DvMkmYA-.css.gz +0 -0
  205. package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css +0 -6
  206. package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css.br +0 -0
  207. package/frontend/build/client/_app/immutable/assets/_page.DvMkmYA-.css.gz +0 -0
  208. package/frontend/build/client/_app/immutable/chunks/DEKAA-Fm.js.br +0 -0
  209. package/frontend/build/client/_app/immutable/chunks/DEKAA-Fm.js.gz +0 -0
  210. package/frontend/build/client/_app/immutable/chunks/Dhf4xQKo.js.br +0 -0
  211. package/frontend/build/client/_app/immutable/chunks/Dhf4xQKo.js.gz +0 -0
  212. package/frontend/build/client/_app/immutable/entry/app.B0GH5ONo.js.br +0 -0
  213. package/frontend/build/client/_app/immutable/entry/app.B0GH5ONo.js.gz +0 -0
  214. package/frontend/build/client/_app/immutable/nodes/0.-imXt0f5.js.br +0 -0
  215. package/frontend/build/client/_app/immutable/nodes/0.-imXt0f5.js.gz +0 -0
  216. package/frontend/build/client/_app/immutable/nodes/1.Dom3CKWF.js.br +0 -0
  217. package/frontend/build/client/_app/immutable/nodes/1.Dom3CKWF.js.gz +0 -0
  218. package/frontend/build/client/_app/immutable/nodes/11.DMlayLCK.js.br +0 -0
  219. package/frontend/build/client/_app/immutable/nodes/11.DMlayLCK.js.gz +0 -0
  220. package/frontend/build/client/_app/immutable/nodes/12.Bu89A1eX.js.br +0 -0
  221. package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js +0 -6
  222. package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js.br +0 -0
  223. package/frontend/build/client/_app/immutable/nodes/2.BnHalSCi.js.gz +0 -0
  224. package/frontend/build/client/_app/immutable/nodes/3.B86rlHXU.js.br +0 -0
  225. package/frontend/build/client/_app/immutable/nodes/3.B86rlHXU.js.gz +0 -0
  226. package/frontend/build/client/_app/immutable/nodes/5.CqTXvadV.js.br +0 -0
  227. package/frontend/build/client/_app/immutable/nodes/5.CqTXvadV.js.gz +0 -0
  228. package/frontend/build/client/_app/immutable/nodes/6.BJZ62MCd.js.br +0 -0
  229. package/frontend/build/client/_app/immutable/nodes/6.BJZ62MCd.js.gz +0 -0
  230. package/frontend/build/client/_app/immutable/nodes/8.B4fj2Xyu.js.br +0 -0
  231. package/frontend/build/client/_app/immutable/nodes/8.B4fj2Xyu.js.gz +0 -0
  232. package/frontend/build/client/_app/immutable/nodes/9.C57_2xq6.js.br +0 -0
  233. package/frontend/build/client/_app/immutable/nodes/9.C57_2xq6.js.gz +0 -0
  234. package/frontend/build/server/chunks/_page.svelte-CpB-fgJl.js.map +0 -5
  235. package/frontend/build/server/chunks/_page.svelte-wKyUmA2e.js.map +0 -5
@@ -18,6 +18,8 @@
18
18
  saveBackupConfig,
19
19
  testBackupS3,
20
20
  runBackupNow,
21
+ fetchS3Backups,
22
+ restoreFromS3,
21
23
  fetchMcpConfig,
22
24
  generateMcpKey as generateMcpKeyApi
23
25
  } from '$lib/api/settings';
@@ -214,8 +216,10 @@
214
216
  IMPORT_BLOCK_TITLE,
215
217
  IMPORT_BLOCK_DESC,
216
218
  CHOOSE_FILE_LABEL,
217
- BACKUP_DISCLAIMER_PREFIX,
218
- BACKUP_DISCLAIMER_SUFFIX,
219
+ INCLUDE_REPORTS_LABEL,
220
+ INCLUDE_REPORTS_HINT,
221
+ includeReportsDisclaimer,
222
+ saveIncludeReportsLabel,
219
223
  S3_STORAGE_CARD_TITLE,
220
224
  S3_STORAGE_DESC_PREFIX,
221
225
  ENDPOINT_URL_LABEL,
@@ -240,6 +244,15 @@
240
244
  S3_CONNECTION_FAILED,
241
245
  BACKUP_CONFIG_SAVED_TOAST,
242
246
  BACKUP_CONFIG_SAVE_FAILED,
247
+ RESTORE_FROM_S3_CARD_TITLE,
248
+ RESTORE_FROM_S3_DESC,
249
+ CONFIGURE_S3_FIRST_RESTORE_MESSAGE,
250
+ NO_S3_BACKUPS_MESSAGE,
251
+ RESTORE_CONFIRM_TITLE,
252
+ restoreConfirmBody,
253
+ RESTORE_SUCCESS_TOAST,
254
+ RESTORE_FAILED_FALLBACK,
255
+ LIST_S3_BACKUPS_FAILED,
243
256
  SCHEDULED_BACKUP_CARD_TITLE,
244
257
  CONFIGURE_S3_FIRST_MESSAGE,
245
258
  ENABLE_SCHEDULED_BACKUP_LABEL,
@@ -264,7 +277,10 @@
264
277
  saveS3ConfigLabel,
265
278
  uploadedToLabel,
266
279
  uploadS3NowLabel,
267
- saveScheduleLabel
280
+ saveScheduleLabel,
281
+ restoreLabel,
282
+ refreshingLabel,
283
+ backupSizeLabel
268
284
  } from '$lib/copy/settings';
269
285
 
270
286
  /** @type {'project' | 'runners' | 'repository' | 'integrations' | 'mcp' | 'account' | 'users' | 'backup'} */
@@ -335,9 +351,11 @@
335
351
  backupS3Bucket: '',
336
352
  backupS3AccessKey: '',
337
353
  backupS3SecretKey: '',
338
- backupS3Prefix: ''
354
+ backupS3Prefix: '',
355
+ backupIncludeReports: false
339
356
  };
340
357
  let backupConfigSaving = false;
358
+ let includeReportsSaving = false;
341
359
  let backupS3SecretKeySet = false;
342
360
  let backupTestingS3 = false;
343
361
  let backupRunningNow = false;
@@ -346,6 +364,13 @@
346
364
  let backupLastRunAt = null;
347
365
  let backupLastStatus = '';
348
366
 
367
+ let s3Backups = [];
368
+ let s3BackupsLoaded = false;
369
+ let loadingS3Backups = false;
370
+ let restoringKey = null;
371
+ let restoreConfirmOpen = false;
372
+ let restoreTarget = null;
373
+
349
374
  let runners = [];
350
375
  let runnerForm = { name: '', url: '', token: '', browser: 'chromium' };
351
376
  let runnerFormError = '';
@@ -406,7 +431,8 @@
406
431
  backupS3Bucket: bc.backupS3Bucket,
407
432
  backupS3AccessKey: bc.backupS3AccessKey,
408
433
  backupS3SecretKey: '',
409
- backupS3Prefix: bc.backupS3Prefix
434
+ backupS3Prefix: bc.backupS3Prefix,
435
+ backupIncludeReports: bc.backupIncludeReports
410
436
  };
411
437
  } catch {}
412
438
  if ($auth.user) {
@@ -802,6 +828,23 @@
802
828
  }
803
829
  }
804
830
 
831
+ async function handleSaveIncludeReports() {
832
+ includeReportsSaving = true;
833
+ try {
834
+ const payload = { ...backupConfig };
835
+ if (!payload.backupS3SecretKey) delete payload.backupS3SecretKey;
836
+ const result = await saveBackupConfig(payload);
837
+ if (result.error) throw new Error(result.error);
838
+ if (backupConfig.backupS3SecretKey) backupS3SecretKeySet = true;
839
+ backupConfig = { ...backupConfig, backupS3SecretKey: '' };
840
+ showToast('success', BACKUP_CONFIG_SAVED_TOAST);
841
+ } catch (e) {
842
+ showToast('error', e.message || BACKUP_CONFIG_SAVE_FAILED);
843
+ } finally {
844
+ includeReportsSaving = false;
845
+ }
846
+ }
847
+
805
848
  async function handleTestS3() {
806
849
  backupTestingS3 = true;
807
850
  backupS3TestResult = null;
@@ -845,6 +888,48 @@
845
888
  backupS3SecretKeySet
846
889
  );
847
890
 
891
+ // Fetch the list once, the first time S3 looks configured — the Refresh
892
+ // button covers reloading after a new upload or a bucket/prefix change.
893
+ $: if (s3Configured && !s3BackupsLoaded) {
894
+ s3BackupsLoaded = true;
895
+ loadS3Backups();
896
+ }
897
+
898
+ async function loadS3Backups() {
899
+ loadingS3Backups = true;
900
+ try {
901
+ const result = await fetchS3Backups();
902
+ if (result.error) throw new Error(result.error);
903
+ s3Backups = result.backups ?? [];
904
+ } catch (e) {
905
+ showToast('error', e.message || LIST_S3_BACKUPS_FAILED);
906
+ } finally {
907
+ loadingS3Backups = false;
908
+ }
909
+ }
910
+
911
+ function openRestoreConfirm(backup) {
912
+ restoreTarget = backup;
913
+ restoreConfirmOpen = true;
914
+ }
915
+
916
+ async function handleRestoreFromS3() {
917
+ if (!restoreTarget) return;
918
+ restoringKey = restoreTarget.key;
919
+ try {
920
+ const result = await restoreFromS3(restoreTarget.key);
921
+ if (result.error) throw new Error(result.error);
922
+ showToast('success', RESTORE_SUCCESS_TOAST);
923
+ project = await fetchProject();
924
+ restoreConfirmOpen = false;
925
+ restoreTarget = null;
926
+ } catch (e) {
927
+ showToast('error', e.message || RESTORE_FAILED_FALLBACK);
928
+ } finally {
929
+ restoringKey = null;
930
+ }
931
+ }
932
+
848
933
  $: mcpConfigSnippet = JSON.stringify(
849
934
  {
850
935
  mcpServers: {
@@ -1822,10 +1907,36 @@
1822
1907
  </div>
1823
1908
  </div>
1824
1909
  </div>
1910
+
1911
+ <div class="include-reports-row">
1912
+ <label class="field-label backup-toggle-label" for="include-reports">
1913
+ <span>
1914
+ {INCLUDE_REPORTS_LABEL}
1915
+ <span class="field-hint">{INCLUDE_REPORTS_HINT}</span>
1916
+ </span>
1917
+ <button
1918
+ id="include-reports"
1919
+ class="toggle-btn"
1920
+ class:active={backupConfig.backupIncludeReports}
1921
+ on:click={() =>
1922
+ (backupConfig.backupIncludeReports = !backupConfig.backupIncludeReports)}
1923
+ role="switch"
1924
+ aria-checked={backupConfig.backupIncludeReports}
1925
+ >
1926
+ <span class="toggle-thumb"></span>
1927
+ </button>
1928
+ </label>
1929
+ <Button
1930
+ variant="ghost"
1931
+ on:click={handleSaveIncludeReports}
1932
+ disabled={includeReportsSaving}
1933
+ >
1934
+ {saveIncludeReportsLabel(includeReportsSaving)}
1935
+ </Button>
1936
+ </div>
1937
+
1825
1938
  <p class="backup-disclaimer">
1826
- {BACKUP_DISCLAIMER_PREFIX}
1827
- <code>pg_dump</code>
1828
- {BACKUP_DISCLAIMER_SUFFIX}
1939
+ {includeReportsDisclaimer(backupConfig.backupIncludeReports)}
1829
1940
  </p>
1830
1941
  </div>
1831
1942
 
@@ -1947,6 +2058,64 @@
1947
2058
  {/if}
1948
2059
  </div>
1949
2060
 
2061
+ <!-- Restore from S3 -->
2062
+ <ConfirmModal
2063
+ bind:open={restoreConfirmOpen}
2064
+ title={RESTORE_CONFIRM_TITLE}
2065
+ confirmLabel={restoreLabel(false)}
2066
+ loading={!!restoringKey}
2067
+ on:confirm={handleRestoreFromS3}
2068
+ >
2069
+ {#if restoreTarget}
2070
+ {restoreConfirmBody(restoreTarget.key)}
2071
+ {/if}
2072
+ </ConfirmModal>
2073
+
2074
+ <div class="card settings-card" class:card-disabled={!s3Configured}>
2075
+ <div class="card-title-row">
2076
+ <p class="card-title">{RESTORE_FROM_S3_CARD_TITLE}</p>
2077
+ {#if s3Configured}
2078
+ <Button variant="ghost" on:click={loadS3Backups} disabled={loadingS3Backups}>
2079
+ {refreshingLabel(loadingS3Backups)}
2080
+ </Button>
2081
+ {/if}
2082
+ </div>
2083
+
2084
+ {#if !s3Configured}
2085
+ <p class="backup-block-desc">{CONFIGURE_S3_FIRST_RESTORE_MESSAGE}</p>
2086
+ {:else}
2087
+ <p class="backup-block-desc" style="margin-bottom: 1rem;">{RESTORE_FROM_S3_DESC}</p>
2088
+
2089
+ {#if s3Backups.length === 0}
2090
+ <p class="backup-block-desc">
2091
+ {loadingS3Backups ? refreshingLabel(true) : NO_S3_BACKUPS_MESSAGE}
2092
+ </p>
2093
+ {:else}
2094
+ <ul class="s3-backup-list">
2095
+ {#each s3Backups as backup (backup.key)}
2096
+ <li class="s3-backup-row">
2097
+ <div class="s3-backup-info">
2098
+ <span class="s3-backup-key">{backup.key}</span>
2099
+ <span class="s3-backup-meta">
2100
+ {new Date(backup.lastModified).toLocaleString()} · {backupSizeLabel(
2101
+ backup.size
2102
+ )}
2103
+ </span>
2104
+ </div>
2105
+ <Button
2106
+ variant="ghost"
2107
+ on:click={() => openRestoreConfirm(backup)}
2108
+ disabled={restoringKey === backup.key}
2109
+ >
2110
+ {restoreLabel(restoringKey === backup.key)}
2111
+ </Button>
2112
+ </li>
2113
+ {/each}
2114
+ </ul>
2115
+ {/if}
2116
+ {/if}
2117
+ </div>
2118
+
1950
2119
  <!-- Scheduled backup -->
1951
2120
  <div class="card settings-card" class:card-disabled={!s3Configured}>
1952
2121
  <p class="card-title">{SCHEDULED_BACKUP_CARD_TITLE}</p>
@@ -2390,6 +2559,23 @@
2390
2559
  border-radius: 3px;
2391
2560
  }
2392
2561
 
2562
+ .include-reports-row {
2563
+ display: flex;
2564
+ align-items: center;
2565
+ gap: 1.25rem;
2566
+ margin-top: 1.25rem;
2567
+ padding-top: 1.25rem;
2568
+ border-top: 1px solid var(--border);
2569
+ }
2570
+ .include-reports-row .backup-toggle-label {
2571
+ flex: 1;
2572
+ }
2573
+ .include-reports-row .field-label > span {
2574
+ display: flex;
2575
+ flex-direction: column;
2576
+ gap: 0.15rem;
2577
+ }
2578
+
2393
2579
  .backup-disclaimer {
2394
2580
  margin-top: 1rem;
2395
2581
  padding: 0.625rem 0.875rem;
@@ -2422,6 +2608,58 @@
2422
2608
  color: var(--fail);
2423
2609
  }
2424
2610
 
2611
+ .card-title-row {
2612
+ display: flex;
2613
+ align-items: center;
2614
+ justify-content: space-between;
2615
+ gap: 1rem;
2616
+ margin-bottom: 0.75rem;
2617
+ }
2618
+ .card-title-row .card-title {
2619
+ margin-bottom: 0;
2620
+ }
2621
+
2622
+ .s3-backup-list {
2623
+ display: flex;
2624
+ flex-direction: column;
2625
+ gap: 0.5rem;
2626
+ list-style: none;
2627
+ margin: 0;
2628
+ padding: 0;
2629
+ }
2630
+
2631
+ .s3-backup-row {
2632
+ display: flex;
2633
+ align-items: center;
2634
+ justify-content: space-between;
2635
+ gap: 1rem;
2636
+ padding: 0.625rem 0.875rem;
2637
+ background: var(--bg-subtle);
2638
+ border: 1px solid var(--border);
2639
+ border-radius: var(--radius-sm);
2640
+ }
2641
+
2642
+ .s3-backup-info {
2643
+ display: flex;
2644
+ flex-direction: column;
2645
+ gap: 0.2rem;
2646
+ min-width: 0;
2647
+ }
2648
+
2649
+ .s3-backup-key {
2650
+ font-family: 'JetBrains Mono', monospace;
2651
+ font-size: 0.8125rem;
2652
+ color: var(--text);
2653
+ overflow: hidden;
2654
+ text-overflow: ellipsis;
2655
+ white-space: nowrap;
2656
+ }
2657
+
2658
+ .s3-backup-meta {
2659
+ font-size: 0.75rem;
2660
+ color: var(--text-muted);
2661
+ }
2662
+
2425
2663
  .backup-last-run {
2426
2664
  font-size: 0.8125rem;
2427
2665
  color: var(--text-muted);
@@ -9,6 +9,7 @@
9
9
  import { fly, fade } from 'svelte/transition';
10
10
  import {
11
11
  fetchSuite,
12
+ fetchAllSuitesWithCases,
12
13
  updateSuite,
13
14
  createTestCase,
14
15
  fetchTestCase,
@@ -80,13 +81,20 @@
80
81
  TEST_CASE_UPDATED_TOAST,
81
82
  STEPS_SAVED_TOAST,
82
83
  SUITE_UPDATED_TOAST,
84
+ MOVE_CASE_TITLE,
85
+ MOVE_TEST_CASE_MODAL_TITLE,
86
+ MOVE_TO_SUITE_LABEL,
87
+ NO_SUITES_AVAILABLE,
88
+ FAILED_TO_MOVE_CASE,
83
89
  suiteDetailTitle,
84
90
  createdByCapitalized,
85
91
  caseCreatedToast,
86
92
  caseDeletedToast,
93
+ caseMovedToast,
87
94
  caseCount,
88
95
  stepCount,
89
96
  saveStepsLabel,
97
+ moveCaseLabel,
90
98
  runLinkLabel
91
99
  } from '$lib/copy/repository';
92
100
 
@@ -118,6 +126,13 @@
118
126
  let confirmDeleteCase = null;
119
127
  let confirmDeleteCaseOpen = false;
120
128
 
129
+ let allSuites = [];
130
+ let moveCaseOpen = false;
131
+ let moveCaseTarget = null;
132
+ let moveCaseSuiteId = '';
133
+ let moveCaseSaving = false;
134
+ $: otherSuites = allSuites.filter((s) => s.id !== suiteId);
135
+
121
136
  let caseSearch = '';
122
137
  let casesPage = 1;
123
138
  let casesSort = (() => {
@@ -181,6 +196,9 @@
181
196
  } finally {
182
197
  loading = false;
183
198
  }
199
+ try {
200
+ allSuites = await fetchAllSuitesWithCases();
201
+ } catch {}
184
202
  });
185
203
 
186
204
  async function handleCreateCase() {
@@ -287,6 +305,34 @@
287
305
  confirmDeleteCaseOpen = false;
288
306
  }
289
307
 
308
+ function openMoveCase(tc) {
309
+ moveCaseTarget = tc;
310
+ moveCaseSuiteId = otherSuites[0]?.id ?? '';
311
+ moveCaseOpen = true;
312
+ }
313
+
314
+ async function handleMoveCase() {
315
+ if (!moveCaseTarget || !moveCaseSuiteId) return;
316
+ moveCaseSaving = true;
317
+ try {
318
+ await updateTestCase(moveCaseTarget.id, { suiteId: moveCaseSuiteId });
319
+ const targetSuite = allSuites.find((s) => s.id === moveCaseSuiteId);
320
+ suite = {
321
+ ...suite,
322
+ cases: suite.cases.filter((c) => c.id !== moveCaseTarget.id),
323
+ _count: { cases: suite._count.cases - 1 }
324
+ };
325
+ if (selectedCase?.id === moveCaseTarget.id) selectedCase = null;
326
+ showToast('success', caseMovedToast(moveCaseTarget.displayId, targetSuite?.name ?? ''));
327
+ moveCaseOpen = false;
328
+ moveCaseTarget = null;
329
+ } catch (e) {
330
+ showToast('error', e.message ?? FAILED_TO_MOVE_CASE);
331
+ } finally {
332
+ moveCaseSaving = false;
333
+ }
334
+ }
335
+
290
336
  async function handleUpdateSuite() {
291
337
  editSuiteSaving = true;
292
338
  try {
@@ -409,6 +455,32 @@
409
455
  </div>
410
456
  </Modal>
411
457
 
458
+ <Modal bind:open={moveCaseOpen} title={MOVE_TEST_CASE_MODAL_TITLE}>
459
+ <div class="form-fields">
460
+ {#if moveCaseTarget}
461
+ <p class="move-case-subtitle">{moveCaseTarget.displayId} — {moveCaseTarget.title}</p>
462
+ {/if}
463
+ {#if otherSuites.length === 0}
464
+ <p class="form-error">{NO_SUITES_AVAILABLE}</p>
465
+ {:else}
466
+ <div class="field">
467
+ <label class="field-label" for="move-suite">{MOVE_TO_SUITE_LABEL}</label>
468
+ <select id="move-suite" class="field-input" bind:value={moveCaseSuiteId}>
469
+ {#each otherSuites as s}
470
+ <option value={s.id}>{s.displayId} — {s.name}</option>
471
+ {/each}
472
+ </select>
473
+ </div>
474
+ {/if}
475
+ <div class="modal-actions">
476
+ <Button on:click={handleMoveCase} disabled={moveCaseSaving || otherSuites.length === 0}>
477
+ {moveCaseLabel(moveCaseSaving)}
478
+ </Button>
479
+ <Button variant="ghost" on:click={() => (moveCaseOpen = false)}>{CANCEL_LABEL}</Button>
480
+ </div>
481
+ </div>
482
+ </Modal>
483
+
412
484
  {#if loading}
413
485
  <div class="loading-state">{LOADING_LABEL}</div>
414
486
  {:else if suite}
@@ -548,6 +620,26 @@
548
620
  <span class="auto-badge">{AUTOMATED_LABEL}</span>
549
621
  {/if}
550
622
  <div class="case-row-actions" on:click|stopPropagation>
623
+ <button
624
+ class="icon-btn small"
625
+ title={MOVE_CASE_TITLE}
626
+ on:click={() => openMoveCase(tc)}
627
+ >
628
+ <svg
629
+ width="11"
630
+ height="11"
631
+ viewBox="0 0 24 24"
632
+ fill="none"
633
+ stroke="currentColor"
634
+ stroke-width="2"
635
+ stroke-linecap="round"
636
+ stroke-linejoin="round"
637
+ >
638
+ <path
639
+ d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"
640
+ />
641
+ </svg>
642
+ </button>
551
643
  <button
552
644
  class="icon-btn danger small"
553
645
  on:click={() => {
@@ -606,6 +698,26 @@
606
698
  {/if}
607
699
  <div class="detail-header-actions">
608
700
  {#if !editingCase}
701
+ <button
702
+ class="icon-btn"
703
+ title={MOVE_CASE_TITLE}
704
+ on:click={() => openMoveCase(selectedCase)}
705
+ >
706
+ <svg
707
+ width="13"
708
+ height="13"
709
+ viewBox="0 0 24 24"
710
+ fill="none"
711
+ stroke="currentColor"
712
+ stroke-width="2"
713
+ stroke-linecap="round"
714
+ stroke-linejoin="round"
715
+ >
716
+ <path
717
+ d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"
718
+ />
719
+ </svg>
720
+ </button>
609
721
  <button class="icon-btn" title={EDIT_CASE_TITLE} on:click={startEditCase}>
610
722
  <svg
611
723
  width="13"
@@ -1577,6 +1689,10 @@
1577
1689
  font-size: 0.8125rem;
1578
1690
  color: var(--fail);
1579
1691
  }
1692
+ .move-case-subtitle {
1693
+ font-size: 0.8125rem;
1694
+ color: var(--text-muted);
1695
+ }
1580
1696
  .modal-actions {
1581
1697
  display: flex;
1582
1698
  gap: 0.5rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plum-e2e",
3
- "version": "2.8.5",
3
+ "version": "2.9.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/silverlunah/plum.git"
@@ -1,85 +0,0 @@
1
- /*
2
- * This file is part of Plum.
3
- * Licensed under the MIT License. See LICENSE file in the project root for details.
4
- */
5
-
6
- // @ts-check
7
- import { defineConfig, devices } from '@playwright/test';
8
-
9
- /**
10
- * Read environment variables from file.
11
- * https://github.com/motdotla/dotenv
12
- */
13
- import dotenv from 'dotenv';
14
- import path from 'path';
15
- dotenv.config({ path: path.resolve(__dirname, '.env') });
16
-
17
- /**
18
- * @see https://playwright.dev/docs/test-configuration
19
- */
20
- export default defineConfig({
21
- testDir: './tests',
22
- /* Run tests in files in parallel */
23
- fullyParallel: true,
24
- /* Fail the build on CI if you accidentally left test.only in the source code. */
25
- forbidOnly: !!process.env.CI,
26
- /* Retry on CI only */
27
- retries: process.env.CI ? 2 : 0,
28
- /* Opt out of parallel tests on CI. */
29
- workers: process.env.CI ? 1 : undefined,
30
- /* Reporter to use. See https://playwright.dev/docs/test-reporters */
31
- reporter: 'html',
32
- /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
33
- use: {
34
- /* Base URL to use in actions like `await page.goto('/')`. */
35
- // baseURL: 'http://127.0.0.1:3000',
36
-
37
- /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
38
- trace: 'on-first-retry'
39
- },
40
-
41
- /* Configure projects for major browsers */
42
- projects: [
43
- {
44
- name: 'chromium',
45
- use: { ...devices['Desktop Chrome'] }
46
- },
47
-
48
- {
49
- name: 'firefox',
50
- use: { ...devices['Desktop Firefox'] }
51
- },
52
-
53
- {
54
- name: 'webkit',
55
- use: { ...devices['Desktop Safari'] }
56
- }
57
-
58
- /* Test against mobile viewports. */
59
- // {
60
- // name: 'Mobile Chrome',
61
- // use: { ...devices['Pixel 5'] },
62
- // },
63
- // {
64
- // name: 'Mobile Safari',
65
- // use: { ...devices['iPhone 12'] },
66
- // },
67
-
68
- /* Test against branded browsers. */
69
- // {
70
- // name: 'Microsoft Edge',
71
- // use: { ...devices['Desktop Edge'], channel: 'msedge' },
72
- // },
73
- // {
74
- // name: 'Google Chrome',
75
- // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
76
- // },
77
- ]
78
-
79
- /* Run your local dev server before starting the tests */
80
- // webServer: {
81
- // command: 'npm run start',
82
- // url: 'http://127.0.0.1:3000',
83
- // reuseExistingServer: !process.env.CI,
84
- // },
85
- });
@@ -1,6 +0,0 @@
1
- /*
2
- * This file is part of Plum.
3
- * Licensed under the MIT License. See LICENSE file in the project root for details.
4
- */
5
-
6
- .page-header.svelte-ll28r2{margin-bottom:1.75rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border)}.header-top.svelte-ll28r2{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;margin-bottom:1.25rem}h1.svelte-ll28r2{font-size:2.5rem;margin-bottom:.25rem}.subtitle.svelte-ll28r2{color:var(--text-muted);font-size:.875rem}.search-row.svelte-ll28r2{display:flex;gap:.75rem}.search-wrap.svelte-ll28r2{position:relative;flex:1;max-width:480px}.search-icon.svelte-ll28r2{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--text-muted);pointer-events:none}.search-input.svelte-ll28r2{width:100%;padding:.55rem .875rem .55rem 2.25rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-subtle);color:var(--text);font-family:var(--font-body);font-size:.875rem;font-weight:300;outline:none;transition:border-color var(--duration-fast),box-shadow var(--duration-fast)}.search-input.svelte-ll28r2:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);background:var(--bg-elevated)}.sort-bar.svelte-ll28r2{display:flex;align-items:center;gap:.375rem;margin:1.25rem 0;flex-wrap:wrap}.sort-label.svelte-ll28r2{font-size:.75rem;color:var(--text-muted);margin-right:.25rem}.sort-chip.svelte-ll28r2{display:flex;align-items:center;gap:.25rem;height:28px;padding:0 .625rem;font-family:var(--font-body);font-size:.8125rem;color:var(--text-muted);background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-pill);cursor:pointer;transition:color var(--duration-fast),border-color var(--duration-fast),background var(--duration-fast)}.sort-chip.svelte-ll28r2:hover{color:var(--text);border-color:var(--accent)}.sort-chip.active.svelte-ll28r2{color:var(--accent);border-color:var(--accent);background:var(--accent-soft)}.sort-dir.svelte-ll28r2{font-size:.7rem;opacity:.8}.search-input.svelte-ll28r2::placeholder{color:var(--text-muted)}.search-clear.svelte-ll28r2{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;border:none;background:var(--border);color:var(--text-muted);cursor:pointer;transition:background var(--duration-fast),color var(--duration-fast)}.search-clear.svelte-ll28r2:hover{background:var(--text-muted);color:var(--bg)}.suites.svelte-ll28r2{display:flex;flex-direction:column;gap:1rem}.suite.svelte-ll28r2{border:1px solid var(--border);border-radius:var(--radius-lg);overflow:visible;background:var(--bg-elevated);animation:fadeUp .35s var(--ease-out) both}.suite-header.svelte-ll28r2{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.875rem 1.25rem;background:var(--bg-subtle);border-bottom:1px solid var(--border);border-radius:var(--radius-lg) var(--radius-lg) 0 0}.suite-toggle.svelte-ll28r2{display:flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;border-radius:var(--radius-sm);transition:background var(--duration-fast),color var(--duration-fast)}.suite-toggle.svelte-ll28r2:hover{background:var(--bg-elevated);color:var(--text)}.suite-toggle.svelte-ll28r2 svg:where(.svelte-ll28r2){transition:transform var(--duration-fast) var(--ease-out)}.suite-toggle.svelte-ll28r2 svg.rotated:where(.svelte-ll28r2){transform:rotate(90deg)}.suite-meta.svelte-ll28r2{display:flex;align-items:center;gap:.625rem;flex:1;min-width:0}.suite-badges.svelte-ll28r2{display:flex;gap:.25rem;flex-shrink:0}.suite-name.svelte-ll28r2{font-size:.9rem;font-weight:400;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.suite-count.svelte-ll28r2{font-size:.75rem;color:var(--text-muted);flex-shrink:0}.run-btn.svelte-ll28r2{display:inline-flex;align-items:center;gap:.35rem;font-family:var(--font-body);font-size:.75rem;font-weight:400;padding:.3rem .75rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated);color:var(--text-muted);cursor:pointer;white-space:nowrap;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast)}.run-btn.svelte-ll28r2:hover{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)}.tests.svelte-ll28r2{display:flex;flex-direction:column}.test-row.svelte-ll28r2{border-bottom:1px solid var(--border);animation:fadeUp .3s var(--ease-out) both}.test-row.svelte-ll28r2:last-child{border-bottom:none}.test-main.svelte-ll28r2{display:flex;align-items:center;gap:.625rem;padding:.75rem 1.25rem}.run-icon-btn.svelte-ll28r2{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;border:1px solid var(--border);background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast)}.run-icon-btn.svelte-ll28r2:hover{background:var(--accent);color:var(--white);border-color:var(--accent)}.test-ids.svelte-ll28r2{display:flex;gap:.25rem;flex-shrink:0}.id-pill.svelte-ll28r2{position:relative;font-size:.68rem;font-weight:500;letter-spacing:.04em;color:var(--accent);background:var(--accent-soft);border:none;border-radius:var(--radius-pill);padding:.15rem .55rem;cursor:pointer;font-family:JetBrains Mono,monospace;transition:background var(--duration-fast),color var(--duration-fast),filter var(--duration-fast)}.id-pill.svelte-ll28r2:hover{filter:brightness(.92)}.id-pill.copied.svelte-ll28r2{color:var(--pass);background:var(--pass-soft)}.id-pill.copied.svelte-ll28r2:after{content:"Copied";position:absolute;left:50%;bottom:calc(100% + 6px);z-index:50;transform:translate(-50%);padding:.16rem .42rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated);color:var(--pass);box-shadow:0 4px 14px #00000014;font-family:var(--font-body);font-size:.65rem;font-weight:500;letter-spacing:0;pointer-events:none;animation:svelte-ll28r2-copiedPop 1.4s var(--ease-out) both}.test-name.svelte-ll28r2{flex:1;font-size:.875rem;color:var(--text);min-width:0}.outline-badge.svelte-ll28r2{display:inline-block;font-size:.62rem;font-weight:500;letter-spacing:.06em;text-transform:uppercase;background:var(--warn-soft);color:var(--warn);padding:.1rem .4rem;border-radius:var(--radius-pill);margin-left:.4rem;vertical-align:middle}.steps-toggle.svelte-ll28r2{display:inline-flex;align-items:center;gap:.2rem;font-family:var(--font-body);font-size:.72rem;font-weight:400;color:var(--text-muted);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);padding:.2rem .5rem;cursor:pointer;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast)}.steps-toggle.svelte-ll28r2:hover{background:var(--bg-subtle);color:var(--text)}.steps-toggle.svelte-ll28r2 svg:where(.svelte-ll28r2){transition:transform var(--duration-fast) var(--ease-out)}.steps-toggle.svelte-ll28r2 svg.rotated:where(.svelte-ll28r2){transform:rotate(90deg)}.steps.svelte-ll28r2{padding:.75rem 1.25rem .875rem 3.75rem;background:var(--bg-subtle);border-top:1px solid var(--border)}.step-list.svelte-ll28r2{list-style:none;display:flex;flex-direction:column;gap:.3rem}.step.svelte-ll28r2{font-size:.8125rem;color:var(--text-muted);font-family:JetBrains Mono,monospace;line-height:1.5;padding-left:1rem;position:relative}.step.svelte-ll28r2:before{content:"›";position:absolute;left:0;color:var(--accent);font-family:var(--font-body)}.examples.svelte-ll28r2{margin-top:.75rem;padding-top:.75rem;border-top:1px dashed var(--border)}.examples-label.svelte-ll28r2{display:block;font-size:.68rem;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--text-muted);margin-bottom:.5rem}.examples-table-wrap.svelte-ll28r2{overflow-x:auto}.examples-table.svelte-ll28r2{border-collapse:collapse;font-size:.78rem;font-family:JetBrains Mono,monospace;width:auto}.examples-table.svelte-ll28r2 th:where(.svelte-ll28r2){text-align:left;padding:.3rem .75rem;font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);border-bottom:1px solid var(--border);font-family:var(--font-body);font-weight:500}.examples-table.svelte-ll28r2 td:where(.svelte-ll28r2){padding:.3rem .75rem;color:var(--text-muted);border-bottom:1px solid var(--border)}.examples-table.svelte-ll28r2 tbody:where(.svelte-ll28r2) tr:where(.svelte-ll28r2):last-child td:where(.svelte-ll28r2){border-bottom:none}@keyframes svelte-ll28r2-copiedPop{0%{opacity:0;transform:translate(-50%,4px) scale(.96)}12%,78%{opacity:1;transform:translate(-50%) scale(1)}to{opacity:0;transform:translate(-50%,-2px) scale(.98)}}
@@ -1,6 +0,0 @@
1
- /*
2
- * This file is part of Plum.
3
- * Licensed under the MIT License. See LICENSE file in the project root for details.
4
- */
5
-
6
- .page-header.svelte-ll28r2{margin-bottom:1.75rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border)}.header-top.svelte-ll28r2{display:flex;align-items:flex-end;justify-content:space-between;gap:1rem;margin-bottom:1.25rem}h1.svelte-ll28r2{font-size:2.5rem;margin-bottom:.25rem}.subtitle.svelte-ll28r2{color:var(--text-muted);font-size:.875rem}.search-row.svelte-ll28r2{display:flex;gap:.75rem}.search-wrap.svelte-ll28r2{position:relative;flex:1;max-width:480px}.search-icon.svelte-ll28r2{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--text-muted);pointer-events:none}.search-input.svelte-ll28r2{width:100%;padding:.55rem .875rem .55rem 2.25rem;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-subtle);color:var(--text);font-family:var(--font-body);font-size:.875rem;font-weight:300;outline:none;transition:border-color var(--duration-fast),box-shadow var(--duration-fast)}.search-input.svelte-ll28r2:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);background:var(--bg-elevated)}.sort-bar.svelte-ll28r2{display:flex;align-items:center;gap:.375rem;margin:1.25rem 0;flex-wrap:wrap}.sort-label.svelte-ll28r2{font-size:.75rem;color:var(--text-muted);margin-right:.25rem}.sort-chip.svelte-ll28r2{display:flex;align-items:center;gap:.25rem;height:28px;padding:0 .625rem;font-family:var(--font-body);font-size:.8125rem;color:var(--text-muted);background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-pill);cursor:pointer;transition:color var(--duration-fast),border-color var(--duration-fast),background var(--duration-fast)}.sort-chip.svelte-ll28r2:hover{color:var(--text);border-color:var(--accent)}.sort-chip.active.svelte-ll28r2{color:var(--accent);border-color:var(--accent);background:var(--accent-soft)}.sort-dir.svelte-ll28r2{font-size:.7rem;opacity:.8}.search-input.svelte-ll28r2::placeholder{color:var(--text-muted)}.search-clear.svelte-ll28r2{position:absolute;right:.625rem;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;border:none;background:var(--border);color:var(--text-muted);cursor:pointer;transition:background var(--duration-fast),color var(--duration-fast)}.search-clear.svelte-ll28r2:hover{background:var(--text-muted);color:var(--bg)}.suites.svelte-ll28r2{display:flex;flex-direction:column;gap:1rem}.suite.svelte-ll28r2{border:1px solid var(--border);border-radius:var(--radius-lg);overflow:visible;background:var(--bg-elevated);animation:fadeUp .35s var(--ease-out) both}.suite-header.svelte-ll28r2{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.875rem 1.25rem;background:var(--bg-subtle);border-bottom:1px solid var(--border);border-radius:var(--radius-lg) var(--radius-lg) 0 0}.suite-toggle.svelte-ll28r2{display:flex;align-items:center;justify-content:center;width:22px;height:22px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;border-radius:var(--radius-sm);transition:background var(--duration-fast),color var(--duration-fast)}.suite-toggle.svelte-ll28r2:hover{background:var(--bg-elevated);color:var(--text)}.suite-toggle.svelte-ll28r2 svg:where(.svelte-ll28r2){transition:transform var(--duration-fast) var(--ease-out)}.suite-toggle.svelte-ll28r2 svg.rotated:where(.svelte-ll28r2){transform:rotate(90deg)}.suite-meta.svelte-ll28r2{display:flex;align-items:center;gap:.625rem;flex:1;min-width:0}.suite-badges.svelte-ll28r2{display:flex;gap:.25rem;flex-shrink:0}.suite-name.svelte-ll28r2{font-size:.9rem;font-weight:400;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.suite-count.svelte-ll28r2{font-size:.75rem;color:var(--text-muted);flex-shrink:0}.run-btn.svelte-ll28r2{display:inline-flex;align-items:center;gap:.35rem;font-family:var(--font-body);font-size:.75rem;font-weight:400;padding:.3rem .75rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated);color:var(--text-muted);cursor:pointer;white-space:nowrap;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast)}.run-btn.svelte-ll28r2:hover{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)}.tests.svelte-ll28r2{display:flex;flex-direction:column}.test-row.svelte-ll28r2{border-bottom:1px solid var(--border);animation:fadeUp .3s var(--ease-out) both}.test-row.svelte-ll28r2:last-child{border-bottom:none}.test-main.svelte-ll28r2{display:flex;align-items:center;gap:.625rem;padding:.75rem 1.25rem}.run-icon-btn.svelte-ll28r2{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;border:1px solid var(--border);background:transparent;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast)}.run-icon-btn.svelte-ll28r2:hover{background:var(--accent);color:var(--white);border-color:var(--accent)}.test-ids.svelte-ll28r2{display:flex;gap:.25rem;flex-shrink:0}.id-pill.svelte-ll28r2{position:relative;font-size:.68rem;font-weight:500;letter-spacing:.04em;color:var(--accent);background:var(--accent-soft);border:none;border-radius:var(--radius-pill);padding:.15rem .55rem;cursor:pointer;font-family:JetBrains Mono,monospace;transition:background var(--duration-fast),color var(--duration-fast),filter var(--duration-fast)}.id-pill.svelte-ll28r2:hover{filter:brightness(.92)}.id-pill.copied.svelte-ll28r2{color:var(--pass);background:var(--pass-soft)}.id-pill.copied.svelte-ll28r2:after{content:"Copied";position:absolute;left:50%;bottom:calc(100% + 6px);z-index:50;transform:translate(-50%);padding:.16rem .42rem;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--bg-elevated);color:var(--pass);box-shadow:0 4px 14px #00000014;font-family:var(--font-body);font-size:.65rem;font-weight:500;letter-spacing:0;pointer-events:none;animation:svelte-ll28r2-copiedPop 1.4s var(--ease-out) both}.test-name.svelte-ll28r2{flex:1;font-size:.875rem;color:var(--text);min-width:0}.outline-badge.svelte-ll28r2{display:inline-block;font-size:.62rem;font-weight:500;letter-spacing:.06em;text-transform:uppercase;background:var(--warn-soft);color:var(--warn);padding:.1rem .4rem;border-radius:var(--radius-pill);margin-left:.4rem;vertical-align:middle}.steps-toggle.svelte-ll28r2{display:inline-flex;align-items:center;gap:.2rem;font-family:var(--font-body);font-size:.72rem;font-weight:400;color:var(--text-muted);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);padding:.2rem .5rem;cursor:pointer;flex-shrink:0;transition:background var(--duration-fast),color var(--duration-fast)}.steps-toggle.svelte-ll28r2:hover{background:var(--bg-subtle);color:var(--text)}.steps-toggle.svelte-ll28r2 svg:where(.svelte-ll28r2){transition:transform var(--duration-fast) var(--ease-out)}.steps-toggle.svelte-ll28r2 svg.rotated:where(.svelte-ll28r2){transform:rotate(90deg)}.steps.svelte-ll28r2{padding:.75rem 1.25rem .875rem 3.75rem;background:var(--bg-subtle);border-top:1px solid var(--border)}.step-list.svelte-ll28r2{list-style:none;display:flex;flex-direction:column;gap:.3rem}.step.svelte-ll28r2{font-size:.8125rem;color:var(--text-muted);font-family:JetBrains Mono,monospace;line-height:1.5;padding-left:1rem;position:relative}.step.svelte-ll28r2:before{content:"›";position:absolute;left:0;color:var(--accent);font-family:var(--font-body)}.examples.svelte-ll28r2{margin-top:.75rem;padding-top:.75rem;border-top:1px dashed var(--border)}.examples-label.svelte-ll28r2{display:block;font-size:.68rem;font-weight:500;letter-spacing:.07em;text-transform:uppercase;color:var(--text-muted);margin-bottom:.5rem}.examples-table-wrap.svelte-ll28r2{overflow-x:auto}.examples-table.svelte-ll28r2{border-collapse:collapse;font-size:.78rem;font-family:JetBrains Mono,monospace;width:auto}.examples-table.svelte-ll28r2 th:where(.svelte-ll28r2){text-align:left;padding:.3rem .75rem;font-size:.7rem;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);border-bottom:1px solid var(--border);font-family:var(--font-body);font-weight:500}.examples-table.svelte-ll28r2 td:where(.svelte-ll28r2){padding:.3rem .75rem;color:var(--text-muted);border-bottom:1px solid var(--border)}.examples-table.svelte-ll28r2 tbody:where(.svelte-ll28r2) tr:where(.svelte-ll28r2):last-child td:where(.svelte-ll28r2){border-bottom:none}@keyframes svelte-ll28r2-copiedPop{0%{opacity:0;transform:translate(-50%,4px) scale(.96)}12%,78%{opacity:1;transform:translate(-50%) scale(1)}to{opacity:0;transform:translate(-50%,-2px) scale(.98)}}