plum-e2e 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -70
- package/backend/Dockerfile +4 -2
- package/backend/_scaffold/utils/browser.ts +1 -0
- package/backend/app.js +4 -2
- package/backend/config/scripts/generate-report.js +38 -30
- package/{.prettierignore → backend/entrypoint.sh} +7 -6
- package/backend/package-lock.json +453 -10
- package/backend/package.json +5 -2
- package/backend/prisma/migrations/20260614000000_init/migration.sql +35 -0
- package/backend/prisma/migrations/20260614000001_add_project/migration.sql +8 -0
- package/backend/prisma/migrations/migration_lock.toml +3 -0
- package/backend/prisma/schema.prisma +53 -0
- package/backend/routes/backup.routes.js +50 -0
- package/backend/routes/cron.routes.js +9 -60
- package/backend/routes/reports.routes.js +39 -6
- package/backend/routes/settings.routes.js +43 -0
- package/backend/server.js +52 -1
- package/backend/services/backupService.js +88 -0
- package/backend/services/cronService.js +68 -78
- package/backend/{routes/schedules.routes.js → services/prisma.js} +3 -13
- package/backend/services/reportService.js +48 -20
- package/backend/services/settingsService.js +36 -0
- package/bin/plum.js +216 -34
- package/docker-compose.yml +24 -0
- package/frontend/.svelte-kit/adapter-node/.vite/manifest.json +190 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/Button.cBruH0aD.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.BVnUajEa.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
- package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
- package/frontend/.svelte-kit/adapter-node/chunks/Badge.js +30 -0
- package/frontend/.svelte-kit/adapter-node/chunks/Button.js +32 -0
- package/frontend/.svelte-kit/adapter-node/chunks/attributes.js +34 -0
- package/frontend/.svelte-kit/adapter-node/chunks/client.js +59 -0
- package/{backend/services/scheduleService.js → frontend/.svelte-kit/adapter-node/chunks/equality.js} +13 -16
- package/frontend/.svelte-kit/adapter-node/chunks/escaping.js +36 -0
- package/frontend/.svelte-kit/adapter-node/chunks/exports.js +247 -0
- package/frontend/.svelte-kit/adapter-node/chunks/index.js +1135 -0
- package/frontend/.svelte-kit/adapter-node/chunks/internal.js +695 -0
- package/frontend/.svelte-kit/adapter-node/chunks/reports.js +57 -0
- package/frontend/.svelte-kit/adapter-node/entries/fallbacks/error.svelte.js +44 -0
- package/frontend/.svelte-kit/adapter-node/entries/pages/_layout.svelte.js +181 -0
- package/frontend/.svelte-kit/adapter-node/entries/pages/_page.svelte.js +164 -0
- package/frontend/.svelte-kit/adapter-node/entries/pages/reports/_page.svelte.js +141 -0
- package/frontend/.svelte-kit/adapter-node/entries/pages/reports/_slug_/_page.svelte.js +42 -0
- package/frontend/.svelte-kit/adapter-node/entries/pages/scheduled-tests/_page.svelte.js +179 -0
- package/frontend/.svelte-kit/adapter-node/index.js +3144 -0
- package/frontend/.svelte-kit/adapter-node/internal.js +30 -0
- package/frontend/.svelte-kit/adapter-node/manifest-full.js +77 -0
- package/frontend/.svelte-kit/adapter-node/manifest.js +81 -0
- package/frontend/.svelte-kit/adapter-node/nodes/0.js +25 -0
- package/frontend/.svelte-kit/adapter-node/nodes/1.js +25 -0
- package/frontend/.svelte-kit/adapter-node/nodes/2.js +25 -0
- package/frontend/.svelte-kit/adapter-node/nodes/3.js +25 -0
- package/frontend/.svelte-kit/adapter-node/nodes/4.js +25 -0
- package/frontend/.svelte-kit/adapter-node/nodes/5.js +25 -0
- package/frontend/.svelte-kit/ambient.d.ts +240 -0
- package/frontend/.svelte-kit/generated/client/app.js +51 -0
- package/frontend/.svelte-kit/generated/client/matchers.js +18 -0
- package/frontend/.svelte-kit/generated/client/nodes/0.js +18 -0
- package/frontend/.svelte-kit/generated/client/nodes/1.js +18 -0
- package/frontend/.svelte-kit/generated/client/nodes/2.js +18 -0
- package/frontend/.svelte-kit/generated/client/nodes/3.js +18 -0
- package/frontend/.svelte-kit/generated/client/nodes/4.js +18 -0
- package/frontend/.svelte-kit/generated/client/nodes/5.js +18 -0
- package/frontend/.svelte-kit/generated/client-optimized/app.js +51 -0
- package/frontend/.svelte-kit/generated/client-optimized/matchers.js +18 -0
- package/frontend/.svelte-kit/generated/client-optimized/nodes/0.js +18 -0
- package/frontend/.svelte-kit/generated/client-optimized/nodes/1.js +18 -0
- package/frontend/.svelte-kit/generated/client-optimized/nodes/2.js +18 -0
- package/frontend/.svelte-kit/generated/client-optimized/nodes/3.js +18 -0
- package/frontend/.svelte-kit/generated/client-optimized/nodes/4.js +18 -0
- package/frontend/.svelte-kit/generated/client-optimized/nodes/5.js +18 -0
- package/frontend/.svelte-kit/generated/root.js +20 -0
- package/frontend/.svelte-kit/generated/root.svelte +83 -0
- package/frontend/.svelte-kit/generated/server/internal.js +65 -0
- package/frontend/.svelte-kit/non-ambient.d.ts +42 -0
- package/frontend/.svelte-kit/output/client/.vite/manifest.json +289 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/0.CnXRuPt4.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.CGnCsn5q.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/3.BVnUajEa.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/4.DBhBrHFz.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/5.D93VAB-w.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/Button.cBruH0aD.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.BVnUajEa.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/B0bn91RF.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/B1kE5jmt.js +23 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/BKz6FZdB.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Be4iUAB0.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/BpaMqo9O.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Bt3-Z7H1.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CHV2KnPr.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CJ8q7g9Y.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Cit9MuSg.js +20 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CwBTqcbj.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DBk3zeq4.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DFJLQFic.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DTUE7kkY.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/IYPwzKs_.js +20 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/oc0we5Us.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/chunks/rVEKg0Ak.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/entry/app.C2MbX06K.js +19 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/entry/start.GiBry-lw.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/0.BsCYK-Tn.js +21 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/1.N4gjrCnX.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.D9yQNzK8.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/3.CBC5WvNb.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/4.BU6UZHSE.js +19 -0
- package/frontend/.svelte-kit/output/client/_app/immutable/nodes/5.Dzzfrntl.js +18 -0
- package/frontend/.svelte-kit/output/client/_app/version.json +1 -0
- package/frontend/.svelte-kit/output/client/favicon.png +0 -0
- package/frontend/.svelte-kit/output/server/.vite/manifest.json +190 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/Button.cBruH0aD.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.BVnUajEa.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
- package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
- package/frontend/.svelte-kit/output/server/chunks/Badge.js +30 -0
- package/frontend/.svelte-kit/output/server/chunks/Button.js +32 -0
- package/frontend/.svelte-kit/output/server/chunks/attributes.js +34 -0
- package/frontend/.svelte-kit/output/server/chunks/client.js +59 -0
- package/frontend/.svelte-kit/output/server/chunks/equality.js +31 -0
- package/frontend/.svelte-kit/output/server/chunks/escaping.js +36 -0
- package/frontend/.svelte-kit/output/server/chunks/exports.js +247 -0
- package/frontend/.svelte-kit/output/server/chunks/index.js +1135 -0
- package/frontend/.svelte-kit/output/server/chunks/internal.js +695 -0
- package/frontend/.svelte-kit/output/server/chunks/reports.js +57 -0
- package/frontend/.svelte-kit/output/server/entries/fallbacks/error.svelte.js +44 -0
- package/frontend/.svelte-kit/output/server/entries/pages/_layout.svelte.js +181 -0
- package/frontend/.svelte-kit/output/server/entries/pages/_page.svelte.js +164 -0
- package/frontend/.svelte-kit/output/server/entries/pages/reports/_page.svelte.js +141 -0
- package/frontend/.svelte-kit/output/server/entries/pages/reports/_slug_/_page.svelte.js +42 -0
- package/frontend/.svelte-kit/output/server/entries/pages/scheduled-tests/_page.svelte.js +179 -0
- package/frontend/.svelte-kit/output/server/index.js +3144 -0
- package/frontend/.svelte-kit/output/server/internal.js +30 -0
- package/frontend/.svelte-kit/output/server/manifest-full.js +77 -0
- package/frontend/.svelte-kit/output/server/manifest.js +77 -0
- package/frontend/.svelte-kit/output/server/nodes/0.js +25 -0
- package/frontend/.svelte-kit/output/server/nodes/1.js +25 -0
- package/frontend/.svelte-kit/output/server/nodes/2.js +25 -0
- package/frontend/.svelte-kit/output/server/nodes/3.js +25 -0
- package/frontend/.svelte-kit/output/server/nodes/4.js +25 -0
- package/frontend/.svelte-kit/output/server/nodes/5.js +25 -0
- package/frontend/.svelte-kit/tsconfig.json +49 -0
- package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css +17 -0
- package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css +17 -0
- package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css +17 -0
- package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css +17 -0
- package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css +17 -0
- package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
- package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css +17 -0
- package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css.gz +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
- package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css.br +0 -0
- package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js +23 -0
- package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js.br +18 -0
- package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js +18 -0
- package/{resources/comments-format.text → frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js.br} +0 -0
- package/frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js +20 -0
- package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js +20 -0
- package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js.gz +0 -0
- package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js +18 -0
- package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js.br +0 -0
- package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js.gz +0 -0
- package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js +19 -0
- package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js.br +0 -0
- package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js.gz +0 -0
- package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js +18 -0
- package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js.br +18 -0
- package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js +21 -0
- package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js +18 -0
- package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js +18 -0
- package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js +18 -0
- package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js +19 -0
- package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js.gz +0 -0
- package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js +18 -0
- package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js.br +0 -0
- package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js.gz +0 -0
- package/frontend/build/client/_app/version.json +1 -0
- package/frontend/build/client/_app/version.json.br +0 -0
- package/frontend/build/client/_app/version.json.gz +0 -0
- package/frontend/build/client/favicon.png +0 -0
- package/frontend/build/env.js +111 -0
- package/frontend/build/handler.js +1511 -0
- package/frontend/build/index.js +362 -0
- package/frontend/build/server/chunks/0-BFfqmlsS.js +26 -0
- package/frontend/build/server/chunks/0-BFfqmlsS.js.map +17 -0
- package/frontend/build/server/chunks/1-hso3H4Ax.js +26 -0
- package/frontend/build/server/chunks/1-hso3H4Ax.js.map +17 -0
- package/frontend/build/server/chunks/2-BSZWXCWp.js +26 -0
- package/frontend/build/server/chunks/2-BSZWXCWp.js.map +17 -0
- package/frontend/build/server/chunks/3-BYumY9dv.js +26 -0
- package/frontend/build/server/chunks/3-BYumY9dv.js.map +17 -0
- package/frontend/build/server/chunks/4-2UUrCVtM.js +26 -0
- package/frontend/build/server/chunks/4-2UUrCVtM.js.map +17 -0
- package/frontend/build/server/chunks/5-C2_y_fwT.js +26 -0
- package/frontend/build/server/chunks/5-C2_y_fwT.js.map +17 -0
- package/frontend/build/server/chunks/Badge-CC4U7C0c.js +30 -0
- package/frontend/build/server/chunks/Badge-CC4U7C0c.js.map +17 -0
- package/frontend/build/server/chunks/Button-BLkPicwS.js +33 -0
- package/frontend/build/server/chunks/Button-BLkPicwS.js.map +17 -0
- package/frontend/build/server/chunks/_layout.svelte-Bfxcj9C6.js +183 -0
- package/frontend/build/server/chunks/_layout.svelte-Bfxcj9C6.js.map +17 -0
- package/frontend/build/server/chunks/_page.svelte-DEK6UvaI.js +45 -0
- package/frontend/build/server/chunks/_page.svelte-DEK6UvaI.js.map +17 -0
- package/frontend/build/server/chunks/_page.svelte-DScVdHOq.js +142 -0
- package/frontend/build/server/chunks/_page.svelte-DScVdHOq.js.map +17 -0
- package/frontend/build/server/chunks/_page.svelte-Ns7w2sZt.js +180 -0
- package/frontend/build/server/chunks/_page.svelte-Ns7w2sZt.js.map +17 -0
- package/frontend/build/server/chunks/_page.svelte-s6ZIOitT.js +165 -0
- package/frontend/build/server/chunks/_page.svelte-s6ZIOitT.js.map +17 -0
- package/frontend/build/server/chunks/attributes-BeaNKpgU.js +34 -0
- package/frontend/build/server/chunks/attributes-BeaNKpgU.js.map +17 -0
- package/frontend/build/server/chunks/client-C9IaEwNo.js +52 -0
- package/frontend/build/server/chunks/client-C9IaEwNo.js.map +17 -0
- package/frontend/build/server/chunks/error.svelte-HKBv5sQT.js +45 -0
- package/frontend/build/server/chunks/error.svelte-HKBv5sQT.js.map +17 -0
- package/frontend/build/server/chunks/escaping-CqgfEcN3.js +36 -0
- package/frontend/build/server/chunks/escaping-CqgfEcN3.js.map +17 -0
- package/frontend/build/server/chunks/exports-OxlCJ8yE.js +192 -0
- package/frontend/build/server/chunks/exports-OxlCJ8yE.js.map +17 -0
- package/frontend/build/server/chunks/index-CjHlq1kt.js +1076 -0
- package/frontend/build/server/chunks/index-CjHlq1kt.js.map +17 -0
- package/frontend/build/server/chunks/reports-D3ZeYSmi.js +56 -0
- package/frontend/build/server/chunks/reports-D3ZeYSmi.js.map +17 -0
- package/frontend/build/server/index.js +5015 -0
- package/frontend/build/server/index.js.map +17 -0
- package/frontend/build/server/manifest.js +84 -0
- package/frontend/build/server/manifest.js.map +17 -0
- package/frontend/build/shims.js +49 -0
- package/frontend/package-lock.json +2 -2
- package/frontend/package.json +1 -1
- package/frontend/src/lib/api/reports.js +38 -27
- package/frontend/src/lib/api/schedules.js +9 -25
- package/frontend/src/lib/api/settings.js +48 -0
- package/frontend/src/lib/components/layout/Nav.svelte +2 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +160 -21
- package/frontend/src/lib/components/ui/Terminal.svelte +2 -2
- package/frontend/src/lib/stores/runner.js +9 -0
- package/frontend/src/routes/+page.svelte +10 -3
- package/frontend/src/routes/reports/+page.svelte +342 -51
- package/frontend/src/routes/reports/[slug]/+page.svelte +2 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +247 -11
- package/frontend/src/routes/settings/+page.svelte +410 -0
- package/package.json +6 -2
- package/.claude/settings.local.json +0 -27
- package/.husky/pre-commit +0 -2
- package/.prettierrc +0 -15
- package/.vscode/settings.json +0 -10
- package/backend/config/scripts/create-settings.js +0 -53
- package/license-config.json +0 -37
|
@@ -15,113 +15,103 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
18
|
const cron = require('node-cron');
|
|
21
19
|
const { spawn } = require('child_process');
|
|
20
|
+
const prisma = require('./prisma');
|
|
22
21
|
|
|
23
|
-
const
|
|
24
|
-
let
|
|
22
|
+
const scheduledJobs = {};
|
|
23
|
+
let _io = null;
|
|
25
24
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const setSocketIO = (io) => {
|
|
26
|
+
_io = io;
|
|
27
|
+
};
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
function scheduleJob(taskName, cronExpression, tags, workers) {
|
|
30
|
+
if (scheduledJobs[taskName]) {
|
|
31
|
+
scheduledJobs[taskName].stop();
|
|
32
|
+
delete scheduledJobs[taskName];
|
|
33
|
+
}
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
scheduledJobs[taskName] = cron.schedule(cronExpression, () => {
|
|
36
|
+
console.log(`Running scheduled task: ${taskName}`);
|
|
37
|
+
if (_io) _io.emit('cron-start', { taskName });
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
task.on('close', (code) => console.log(`Task ${taskName} finished with code ${code}`));
|
|
43
|
-
});
|
|
39
|
+
const env = { ...process.env, TAG: tags, TRIGGER: taskName };
|
|
40
|
+
if (workers && workers > 1) env.PARALLEL = String(workers);
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const task = spawn('npm', ['run', 'test'], { env });
|
|
43
|
+
task.stdout.on('data', (data) => console.log(data.toString()));
|
|
44
|
+
task.stderr.on('data', (data) => console.error(data.toString()));
|
|
45
|
+
task.on('close', (code) => {
|
|
46
|
+
console.log(`Task ${taskName} finished with code ${code}`);
|
|
47
|
+
if (_io) _io.emit('cron-done', { taskName, code });
|
|
47
48
|
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const init = async () => {
|
|
53
|
+
const jobs = await prisma.cronJob.findMany();
|
|
54
|
+
for (const job of jobs) {
|
|
55
|
+
scheduleJob(job.taskName, job.cronExpression, job.tags, job.workers);
|
|
48
56
|
}
|
|
57
|
+
console.log(`⏰ Scheduled ${jobs.length} cron job(s) from database`);
|
|
49
58
|
};
|
|
50
59
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}, {});
|
|
58
|
-
|
|
59
|
-
fs.writeFileSync(CRON_JOBS_FILE, JSON.stringify(cronJobsData, null, 2), 'utf8');
|
|
60
|
+
const reload = async () => {
|
|
61
|
+
for (const name of Object.keys(scheduledJobs)) {
|
|
62
|
+
scheduledJobs[name].stop();
|
|
63
|
+
delete scheduledJobs[name];
|
|
64
|
+
}
|
|
65
|
+
await init();
|
|
60
66
|
};
|
|
61
67
|
|
|
62
|
-
const getAllCronJobs = () =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
cronExpression: cronJobs[taskName].cronExpression,
|
|
66
|
-
tags: cronJobs[taskName].tags
|
|
67
|
-
}));
|
|
68
|
+
const getAllCronJobs = async () => {
|
|
69
|
+
return prisma.cronJob.findMany({ orderBy: { createdAt: 'asc' } });
|
|
70
|
+
};
|
|
68
71
|
|
|
69
|
-
const addCronJob = ({
|
|
72
|
+
const addCronJob = async ({ taskName, cronExpression, tags, workers }) => {
|
|
70
73
|
if (!cronExpression || !taskName || !tags) {
|
|
71
74
|
return { status: 400, message: 'Missing required parameters' };
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const job = await prisma.cronJob.create({
|
|
77
|
+
data: { taskName, cronExpression, tags, workers: workers ?? 1 }
|
|
78
|
+
});
|
|
79
|
+
scheduleJob(job.taskName, job.cronExpression, job.tags, job.workers);
|
|
77
80
|
return { status: 201, message: `Cron job ${taskName} added` };
|
|
78
81
|
};
|
|
79
82
|
|
|
80
|
-
const removeCronJob = (taskName) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
83
|
+
const removeCronJob = async (taskName) => {
|
|
84
|
+
const job = await prisma.cronJob.findUnique({ where: { taskName } });
|
|
85
|
+
if (!job) return { status: 404, message: `Cron job ${taskName} not found` };
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
if (scheduledJobs[taskName]) {
|
|
88
|
+
scheduledJobs[taskName].stop();
|
|
89
|
+
delete scheduledJobs[taskName];
|
|
90
|
+
}
|
|
87
91
|
|
|
88
|
-
delete
|
|
89
|
-
saveCronJobs();
|
|
90
|
-
loadCronJobs(); // Re-load and re-schedule cron jobs without the removed one
|
|
92
|
+
await prisma.cronJob.delete({ where: { taskName } });
|
|
91
93
|
return { status: 200, message: `Cron job ${taskName} deleted` };
|
|
92
94
|
};
|
|
93
95
|
|
|
94
|
-
const updateCronJob = (taskName, { cronExpression, tags }) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Stop the old cron job
|
|
100
|
-
cronJobs[taskName].cronJob.stop();
|
|
101
|
-
|
|
102
|
-
// Update the cron job with new values
|
|
103
|
-
cronJobs[taskName] = { cronExpression, tags };
|
|
104
|
-
|
|
105
|
-
// Reschedule the updated cron job and store the reference
|
|
106
|
-
const scheduledCronJob = cron.schedule(cronExpression, () => {
|
|
107
|
-
console.log(`Running updated task: ${taskName}`);
|
|
108
|
-
|
|
109
|
-
const task = spawn('npm', ['run', 'test'], {
|
|
110
|
-
env: { ...process.env, TAG: tags, TRIGGER: taskName }
|
|
111
|
-
});
|
|
96
|
+
const updateCronJob = async (taskName, { cronExpression, tags, workers }) => {
|
|
97
|
+
const job = await prisma.cronJob.findUnique({ where: { taskName } });
|
|
98
|
+
if (!job) return { status: 404, message: `Cron job ${taskName} not found` };
|
|
112
99
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
100
|
+
const updated = await prisma.cronJob.update({
|
|
101
|
+
where: { taskName },
|
|
102
|
+
data: { cronExpression, tags, workers: workers ?? 1 }
|
|
116
103
|
});
|
|
117
104
|
|
|
118
|
-
|
|
119
|
-
cronJobs[taskName].cronJob = scheduledCronJob;
|
|
120
|
-
|
|
121
|
-
saveCronJobs(); // Save the updated cron jobs to file (excluding cron job references)
|
|
105
|
+
scheduleJob(updated.taskName, updated.cronExpression, updated.tags, updated.workers);
|
|
122
106
|
return { status: 200, message: `Cron job ${taskName} updated` };
|
|
123
107
|
};
|
|
124
108
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
module.exports = {
|
|
110
|
+
init,
|
|
111
|
+
reload,
|
|
112
|
+
getAllCronJobs,
|
|
113
|
+
addCronJob,
|
|
114
|
+
removeCronJob,
|
|
115
|
+
updateCronJob,
|
|
116
|
+
setSocketIO
|
|
117
|
+
};
|
|
@@ -15,18 +15,8 @@
|
|
|
15
15
|
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const router = express.Router();
|
|
20
|
-
const scheduleService = require('../services/scheduleService');
|
|
18
|
+
const { PrismaClient } = require('@prisma/client');
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
* Get Schedules
|
|
24
|
-
* Description:
|
|
25
|
-
* Get all schedules from schedules/
|
|
26
|
-
* ------------------------------------------------------ */
|
|
27
|
-
router.get('/', (req, res) => {
|
|
28
|
-
const schedules = scheduleService.getAllSchedules();
|
|
29
|
-
res.json({ schedules });
|
|
30
|
-
});
|
|
20
|
+
const prisma = new PrismaClient();
|
|
31
21
|
|
|
32
|
-
module.exports =
|
|
22
|
+
module.exports = prisma;
|
|
@@ -17,29 +17,36 @@
|
|
|
17
17
|
|
|
18
18
|
const fs = require('fs');
|
|
19
19
|
const path = require('path');
|
|
20
|
+
const prisma = require('./prisma');
|
|
20
21
|
|
|
21
22
|
const REPORTS_DIR = path.join(__dirname, '../reports');
|
|
22
23
|
|
|
23
|
-
const getAllReports = () => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const bt = fs.statSync(path.join(REPORTS_DIR, b)).mtime;
|
|
31
|
-
return bt - at;
|
|
32
|
-
});
|
|
24
|
+
const getAllReports = async () => {
|
|
25
|
+
return prisma.report.findMany({ orderBy: { createdAt: 'desc' } });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getLatestReport = async () => {
|
|
29
|
+
const report = await prisma.report.findFirst({ orderBy: { createdAt: 'desc' } });
|
|
30
|
+
return report ? report.fileName : null;
|
|
33
31
|
};
|
|
34
32
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const saveReport = async ({ fileName, status, tags, triggerType, runners }) => {
|
|
34
|
+
let cronJobId = null;
|
|
35
|
+
const builtInTriggers = ['manual-trigger', 'command-line-trigger', 'undefined'];
|
|
36
|
+
if (triggerType && !builtInTriggers.includes(triggerType)) {
|
|
37
|
+
const job = await prisma.cronJob.findUnique({ where: { taskName: triggerType } });
|
|
38
|
+
if (job) cronJobId = job.id;
|
|
39
|
+
}
|
|
40
|
+
return prisma.report.upsert({
|
|
41
|
+
where: { fileName },
|
|
42
|
+
create: { fileName, status, tags, triggerType: triggerType || 'undefined', runners, cronJobId },
|
|
43
|
+
update: {}
|
|
44
|
+
});
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
const getReportDetail = (fileName) => {
|
|
41
48
|
const filePath = path.join(REPORTS_DIR, fileName);
|
|
42
|
-
if (!filePath.startsWith(REPORTS_DIR)) return null;
|
|
49
|
+
if (!filePath.startsWith(REPORTS_DIR)) return null;
|
|
43
50
|
if (!fs.existsSync(filePath)) return null;
|
|
44
51
|
|
|
45
52
|
let raw;
|
|
@@ -58,11 +65,11 @@ const getReportDetail = (fileName) => {
|
|
|
58
65
|
const failedStepIndex = visibleSteps.findLastIndex((s) => s.result?.status === 'failed');
|
|
59
66
|
|
|
60
67
|
const steps = visibleSteps.map((step, index) => ({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
keyword: step.keyword.trim(),
|
|
69
|
+
name: step.name ?? '',
|
|
70
|
+
status: step.result?.status ?? 'pending',
|
|
71
|
+
duration: Math.round((step.result?.duration ?? 0) / 1_000_000),
|
|
72
|
+
error: step.result?.error_message ?? null,
|
|
66
73
|
screenshot:
|
|
67
74
|
step.embeddings?.find((e) => e.mime_type === 'image/png')?.data ??
|
|
68
75
|
(index === failedStepIndex ? hookScreenshots[0]?.data : null) ??
|
|
@@ -97,4 +104,25 @@ const getReportDetail = (fileName) => {
|
|
|
97
104
|
return { features };
|
|
98
105
|
};
|
|
99
106
|
|
|
100
|
-
|
|
107
|
+
const deleteReport = async (fileName) => {
|
|
108
|
+
await prisma.report.delete({ where: { fileName } });
|
|
109
|
+
const filePath = path.join(REPORTS_DIR, fileName);
|
|
110
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const deleteReports = async (fileNames) => {
|
|
114
|
+
await prisma.report.deleteMany({ where: { fileName: { in: fileNames } } });
|
|
115
|
+
for (const fileName of fileNames) {
|
|
116
|
+
const filePath = path.join(REPORTS_DIR, fileName);
|
|
117
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
getAllReports,
|
|
123
|
+
getLatestReport,
|
|
124
|
+
saveReport,
|
|
125
|
+
getReportDetail,
|
|
126
|
+
deleteReport,
|
|
127
|
+
deleteReports
|
|
128
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of Plum.
|
|
3
|
+
*
|
|
4
|
+
* Plum is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU General Public License as published by
|
|
6
|
+
* the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* Plum is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU General Public License
|
|
15
|
+
* along with Plum. If not, see https://www.gnu.org/licenses/.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const prisma = require('./prisma');
|
|
19
|
+
|
|
20
|
+
const getProject = async () => {
|
|
21
|
+
let project = await prisma.project.findUnique({ where: { id: 1 } });
|
|
22
|
+
if (!project) {
|
|
23
|
+
project = await prisma.project.create({ data: { id: 1, name: '', logoUrl: '' } });
|
|
24
|
+
}
|
|
25
|
+
return project;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const updateProject = async ({ name, logoUrl }) => {
|
|
29
|
+
return prisma.project.upsert({
|
|
30
|
+
where: { id: 1 },
|
|
31
|
+
create: { id: 1, name: name ?? '', logoUrl: logoUrl ?? '' },
|
|
32
|
+
update: { name: name ?? '', logoUrl: logoUrl ?? '' }
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
module.exports = { getProject, updateProject };
|
package/bin/plum.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { execSync } from 'child_process';
|
|
20
20
|
import fs from 'fs';
|
|
21
|
+
import os from 'os';
|
|
21
22
|
import path from 'path';
|
|
22
23
|
import { fileURLToPath } from 'url';
|
|
23
24
|
import fse from 'fs-extra';
|
|
@@ -55,6 +56,70 @@ IS_HEADLESS=false
|
|
|
55
56
|
console.log('✅ .env file created with default values.\n');
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// Scaffold plum.plugins.json if it doesn't exist yet
|
|
60
|
+
function scaffoldPluginsFile() {
|
|
61
|
+
const pluginsPath = path.join(process.cwd(), 'plum.plugins.json');
|
|
62
|
+
if (fs.existsSync(pluginsPath)) {
|
|
63
|
+
console.log('⚠️ plum.plugins.json already exists. Skipping.\n');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const content = {
|
|
67
|
+
'//': 'Add npm packages your tests depend on. Plum installs them automatically before each run.',
|
|
68
|
+
'// example':
|
|
69
|
+
'To add a package: put its name and version under "dependencies", e.g. "@faker-js/faker": "^9.0.0"',
|
|
70
|
+
dependencies: {}
|
|
71
|
+
};
|
|
72
|
+
fs.writeFileSync(pluginsPath, JSON.stringify(content, null, 2) + '\n', 'utf8');
|
|
73
|
+
console.log('✅ plum.plugins.json created. Add npm packages here to extend your tests.\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Install user plugins listed in plum.plugins.json into the backend
|
|
77
|
+
function installPlugins() {
|
|
78
|
+
const pluginsPath = path.join(process.cwd(), 'plum.plugins.json');
|
|
79
|
+
if (!fs.existsSync(pluginsPath)) return;
|
|
80
|
+
|
|
81
|
+
let plugins;
|
|
82
|
+
try {
|
|
83
|
+
plugins = JSON.parse(fs.readFileSync(pluginsPath, 'utf8'));
|
|
84
|
+
} catch {
|
|
85
|
+
console.log('⚠️ Could not parse plum.plugins.json. Skipping plugin install.\n');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const deps = plugins.dependencies ?? {};
|
|
90
|
+
const packages = Object.entries(deps).map(([name, version]) => `${name}@${version}`);
|
|
91
|
+
if (packages.length === 0) return;
|
|
92
|
+
|
|
93
|
+
console.log(`📦 Installing plugins: ${packages.join(', ')}\n`);
|
|
94
|
+
execSync(`npm install ${packages.join(' ')}`, {
|
|
95
|
+
cwd: path.join(plumRoot, 'backend'),
|
|
96
|
+
stdio: 'inherit'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ensure user's .gitignore contains Plum-generated entries
|
|
101
|
+
function ensureGitignore() {
|
|
102
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
103
|
+
const plumEntries = ['reports/'];
|
|
104
|
+
const plumBlock = `\n# Plum (auto-generated)\n${plumEntries.join('\n')}\n`;
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
107
|
+
fs.writeFileSync(gitignorePath, plumBlock.trimStart(), 'utf8');
|
|
108
|
+
console.log('✅ .gitignore created with Plum entries.\n');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
113
|
+
const missing = plumEntries.filter((e) => !existing.includes(e));
|
|
114
|
+
if (missing.length === 0) {
|
|
115
|
+
console.log('⚠️ .gitignore already contains Plum entries. Skipping.\n');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fs.appendFileSync(gitignorePath, `\n# Plum (auto-generated)\n${missing.join('\n')}\n`);
|
|
120
|
+
console.log('✅ .gitignore updated with Plum entries.\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
58
123
|
// Function to copy .env file from root to backend
|
|
59
124
|
function copyEnvFile() {
|
|
60
125
|
try {
|
|
@@ -91,32 +156,112 @@ switch (command) {
|
|
|
91
156
|
// Create .env file with default values
|
|
92
157
|
createEnvFile();
|
|
93
158
|
|
|
94
|
-
// Create
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
159
|
+
// Create or update .gitignore with Plum-generated paths
|
|
160
|
+
ensureGitignore();
|
|
161
|
+
|
|
162
|
+
// Scaffold plum.plugins.json for user-managed dependencies
|
|
163
|
+
scaffoldPluginsFile();
|
|
164
|
+
|
|
165
|
+
// Create .vscode/settings.json and install Cucumber extension — only if VS Code is available
|
|
166
|
+
{
|
|
167
|
+
let vscodeAvailable = false;
|
|
168
|
+
try {
|
|
169
|
+
execSync('code --version', { stdio: 'ignore' });
|
|
170
|
+
vscodeAvailable = true;
|
|
171
|
+
} catch {}
|
|
172
|
+
|
|
173
|
+
if (vscodeAvailable) {
|
|
174
|
+
const vscodeSettingsPath = path.join(process.cwd(), '.vscode', 'settings.json');
|
|
175
|
+
if (!fs.existsSync(vscodeSettingsPath)) {
|
|
176
|
+
fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
|
|
177
|
+
fs.writeFileSync(
|
|
178
|
+
vscodeSettingsPath,
|
|
179
|
+
JSON.stringify(
|
|
180
|
+
{
|
|
181
|
+
'cucumber.glue': ['tests/step_definitions/**/*.ts'],
|
|
182
|
+
'cucumber.features': ['tests/features/**/*.feature']
|
|
183
|
+
},
|
|
184
|
+
null,
|
|
185
|
+
2
|
|
186
|
+
) + '\n',
|
|
187
|
+
'utf8'
|
|
188
|
+
);
|
|
189
|
+
console.log('✅ .vscode/settings.json created for Cucumber extension.\n');
|
|
190
|
+
} else {
|
|
191
|
+
console.log('⚠️ .vscode/settings.json already exists. Skipping.\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
execSync('code --install-extension cucumberopen.cucumber-official', { stdio: 'inherit' });
|
|
196
|
+
console.log('✅ Cucumber VS Code extension installed.\n');
|
|
197
|
+
} catch {
|
|
198
|
+
console.log(
|
|
199
|
+
'⚠️ Could not install VS Code extension automatically. Install manually: cucumberopen.cucumber-official\n'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
console.log('ℹ️ VS Code not detected — skipping .vscode setup.\n');
|
|
204
|
+
}
|
|
110
205
|
}
|
|
111
206
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
207
|
+
// Create README.md in user's project if one doesn't exist
|
|
208
|
+
{
|
|
209
|
+
const userReadmePath = path.join(process.cwd(), 'README.md');
|
|
210
|
+
if (!fs.existsSync(userReadmePath)) {
|
|
211
|
+
const readmeContent = [
|
|
212
|
+
'# My Tests',
|
|
213
|
+
'',
|
|
214
|
+
'Powered by [Plum](https://github.com/silverlunah/plum) — Playwright + Cucumber.',
|
|
215
|
+
'',
|
|
216
|
+
'## Commands',
|
|
217
|
+
'',
|
|
218
|
+
'| Command | Description |',
|
|
219
|
+
'|---|---|',
|
|
220
|
+
'| `plum dev` | Run all tests locally |',
|
|
221
|
+
'| `plum dev @tag` | Run tests matching a tag |',
|
|
222
|
+
'| `plum dev --parallel N` | Run tests across N parallel workers |',
|
|
223
|
+
'| `plum start` | Start the full UI via Docker (`http://localhost:5173`) |',
|
|
224
|
+
'| `plum create-step` | Interactively generate a new step definition |',
|
|
225
|
+
'',
|
|
226
|
+
'## Configuration',
|
|
227
|
+
'',
|
|
228
|
+
'| File | Purpose |',
|
|
229
|
+
'|---|---|',
|
|
230
|
+
'| `.env` | Set `BASE_URL` and `IS_HEADLESS` |',
|
|
231
|
+
'| `plum.plugins.json` | Add extra npm packages for your tests |',
|
|
232
|
+
'',
|
|
233
|
+
'## Test Structure',
|
|
234
|
+
'',
|
|
235
|
+
'```',
|
|
236
|
+
'tests/',
|
|
237
|
+
' features/ — Gherkin .feature files',
|
|
238
|
+
' step_definitions/ — TypeScript step implementations',
|
|
239
|
+
' pages/ — Page Object Models',
|
|
240
|
+
' utils/ — Browser setup, hooks, helpers',
|
|
241
|
+
'```',
|
|
242
|
+
'',
|
|
243
|
+
'Tags are used to filter which tests to run:',
|
|
244
|
+
'',
|
|
245
|
+
'```gherkin',
|
|
246
|
+
'@suite-login',
|
|
247
|
+
'Feature: Login',
|
|
248
|
+
'',
|
|
249
|
+
' @test-login-1',
|
|
250
|
+
' Scenario: User can log in',
|
|
251
|
+
' Given I am on the login page',
|
|
252
|
+
' ...',
|
|
253
|
+
'```',
|
|
254
|
+
'',
|
|
255
|
+
'```bash',
|
|
256
|
+
'plum dev @test-login-1 # single scenario',
|
|
257
|
+
'plum dev @suite-login # whole suite',
|
|
258
|
+
'```'
|
|
259
|
+
].join('\n');
|
|
260
|
+
fs.writeFileSync(userReadmePath, readmeContent + '\n', 'utf8');
|
|
261
|
+
console.log('✅ README.md created with command reference.\n');
|
|
262
|
+
} else {
|
|
263
|
+
console.log('⚠️ README.md already exists. Skipping.\n');
|
|
264
|
+
}
|
|
120
265
|
}
|
|
121
266
|
|
|
122
267
|
// Initialize project
|
|
@@ -128,7 +273,7 @@ switch (command) {
|
|
|
128
273
|
});
|
|
129
274
|
|
|
130
275
|
console.log(
|
|
131
|
-
'🟣 Plum is now ready!\n\n Scaffold test cases are
|
|
276
|
+
'🟣 Plum is now ready!\n\n Scaffold test cases are in `tests/`.\n Add extra npm packages to `plum.plugins.json`.\n\n - Run tests locally:\n `plum dev` or `plum dev @tag`\n\n - Start the full UI (requires Docker):\n `plum start`\n\n - Generate a step:\n `plum create-step`'
|
|
132
277
|
);
|
|
133
278
|
console.log('--------------------------------------\n');
|
|
134
279
|
break;
|
|
@@ -141,13 +286,32 @@ switch (command) {
|
|
|
141
286
|
// Copy .env file from root to backend
|
|
142
287
|
copyEnvFile();
|
|
143
288
|
|
|
144
|
-
//
|
|
145
|
-
|
|
289
|
+
// Merge user plugins into backend/package.json before Docker build
|
|
290
|
+
{
|
|
291
|
+
const userPluginsPath = path.join(process.cwd(), 'plum.plugins.json');
|
|
292
|
+
if (fs.existsSync(userPluginsPath)) {
|
|
293
|
+
try {
|
|
294
|
+
const userPlugins = JSON.parse(fs.readFileSync(userPluginsPath, 'utf8'));
|
|
295
|
+
const backendPkgPath = path.join(plumRoot, 'backend', 'package.json');
|
|
296
|
+
const backendPkg = JSON.parse(fs.readFileSync(backendPkgPath, 'utf8'));
|
|
297
|
+
const pluginDeps = userPlugins.dependencies ?? {};
|
|
298
|
+
if (Object.keys(pluginDeps).length > 0) {
|
|
299
|
+
backendPkg.dependencies = { ...backendPkg.dependencies, ...pluginDeps };
|
|
300
|
+
fs.writeFileSync(backendPkgPath, JSON.stringify(backendPkg, null, '\t') + '\n', 'utf8');
|
|
301
|
+
console.log(`📦 Merged plugins into backend: ${Object.keys(pluginDeps).join(', ')}\n`);
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
console.log('⚠️ Could not read plum.plugins.json. Skipping plugin merge.\n');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Copy config to ~/.plum/config so Docker can mount it without polluting the project
|
|
310
|
+
const userConfigPath = path.join(os.homedir(), '.plum', 'config');
|
|
146
311
|
fse.copySync(path.join(plumRoot, 'backend', 'config'), userConfigPath);
|
|
147
312
|
|
|
148
313
|
// Convert Windows paths to safe format
|
|
149
314
|
const userTestsAbs = path.resolve(process.cwd(), 'tests').replace(/\\/g, '/');
|
|
150
|
-
const userModulesAbs = path.resolve(process.cwd(), 'node_modules').replace(/\\/g, '/');
|
|
151
315
|
const userReportsAbs = path.resolve(process.cwd(), 'reports').replace(/\\/g, '/');
|
|
152
316
|
const userConfigAbs = userConfigPath.replace(/\\/g, '/');
|
|
153
317
|
|
|
@@ -158,15 +322,14 @@ switch (command) {
|
|
|
158
322
|
' volumes:',
|
|
159
323
|
` - "${userReportsAbs}:/app/reports"`,
|
|
160
324
|
` - "${userConfigAbs}:/app/config"`,
|
|
161
|
-
` - "${userTestsAbs}:/app/tests"
|
|
162
|
-
` - "${userModulesAbs}:/app/tests/node_modules"`
|
|
325
|
+
` - "${userTestsAbs}:/app/tests"`
|
|
163
326
|
].join('\n');
|
|
164
327
|
|
|
165
328
|
fs.writeFileSync(overrideFilePath, overrideYAML + '\n', 'utf8');
|
|
166
329
|
console.log('✅ docker-compose.override.yml written');
|
|
167
330
|
|
|
168
|
-
// Run docker compose
|
|
169
|
-
execSync('docker compose up', {
|
|
331
|
+
// Run docker compose (--build picks up any plugin or config changes)
|
|
332
|
+
execSync('docker compose up --build', {
|
|
170
333
|
cwd: plumRoot,
|
|
171
334
|
stdio: 'inherit'
|
|
172
335
|
});
|
|
@@ -204,6 +367,9 @@ switch (command) {
|
|
|
204
367
|
stdio: 'inherit'
|
|
205
368
|
});
|
|
206
369
|
|
|
370
|
+
// Install user-defined plugins from plum.plugins.json
|
|
371
|
+
installPlugins();
|
|
372
|
+
|
|
207
373
|
console.log('Running `npx playwright install`...');
|
|
208
374
|
|
|
209
375
|
execSync('npx playwright install', {
|
|
@@ -232,6 +398,17 @@ switch (command) {
|
|
|
232
398
|
break;
|
|
233
399
|
}
|
|
234
400
|
|
|
401
|
+
case 'stop':
|
|
402
|
+
console.log('--------------------------------------\n');
|
|
403
|
+
console.log('🛑 Stopping Plum...');
|
|
404
|
+
execSync('docker compose down', {
|
|
405
|
+
cwd: plumRoot,
|
|
406
|
+
stdio: 'inherit'
|
|
407
|
+
});
|
|
408
|
+
console.log('✅ Plum stopped. Your data is preserved in the database volume.\n');
|
|
409
|
+
console.log('--------------------------------------\n');
|
|
410
|
+
break;
|
|
411
|
+
|
|
235
412
|
case 'create-step': {
|
|
236
413
|
const createStepScript = path.join(plumRoot, 'backend', 'config', 'scripts', 'create-step.mjs');
|
|
237
414
|
execSync(`node ${createStepScript}`, {
|
|
@@ -247,6 +424,11 @@ switch (command) {
|
|
|
247
424
|
|
|
248
425
|
default:
|
|
249
426
|
console.log('--------------------------------------\n');
|
|
250
|
-
console.log('Usage: plum <
|
|
251
|
-
console.log('
|
|
427
|
+
console.log('Usage: plum <command>\n');
|
|
428
|
+
console.log(' init Set up a new Plum project');
|
|
429
|
+
console.log(' start Start the full UI stack via Docker');
|
|
430
|
+
console.log(' stop Stop Docker containers (data is preserved)');
|
|
431
|
+
console.log(' dev Run tests locally without Docker');
|
|
432
|
+
console.log(' create-step Interactively scaffold a new step definition');
|
|
433
|
+
console.log('\n--------------------------------------\n');
|
|
252
434
|
}
|