@webship.co/workspace 0.0.0-stage → 1.0.0-alpha1
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/PLUGINS.md +249 -0
- package/README.md +379 -2
- package/WORKSPACES.md +69 -0
- package/actions/ai-items.js +293 -0
- package/actions/audio.js +48 -0
- package/actions/build-order.js +79 -0
- package/actions/checks.js +95 -0
- package/actions/claude-session.js +104 -0
- package/actions/commands-center.js +258 -0
- package/actions/composer.js +267 -0
- package/actions/ddev-addons.js +90 -0
- package/actions/ddev-mail.js +80 -0
- package/actions/ddev.js +237 -0
- package/actions/folders.js +165 -0
- package/actions/graphs.js +107 -0
- package/actions/images-svg.js +33 -0
- package/actions/images.js +145 -0
- package/actions/navigation.js +190 -0
- package/actions/node.js +222 -0
- package/actions/notes.js +55 -0
- package/actions/obsidian.js +59 -0
- package/actions/plugins.js +36 -0
- package/actions/project-make.js +28 -0
- package/actions/rag.js +202 -0
- package/actions/row-actions.js +262 -0
- package/actions/screens.js +270 -0
- package/actions/testing.js +121 -0
- package/actions/video-edit.js +54 -0
- package/ai-inventory.js +182 -0
- package/ai-items.js +202 -0
- package/assistant-cast.js +176 -0
- package/assistant-suggestions.js +91 -0
- package/assistant.js +484 -0
- package/audio-voice.js +528 -0
- package/auth-google.js +260 -0
- package/build-ai.js +52 -0
- package/build-order-page.js +191 -0
- package/build-order.js +313 -0
- package/builder-args.js +117 -0
- package/changelog.js +302 -0
- package/chat-threads.js +134 -0
- package/checks.js +105 -0
- package/claude-args.js +282 -0
- package/claude-sessions.js +129 -0
- package/command-author.js +117 -0
- package/command-block.js +20 -0
- package/commands/defaults.yml +159 -0
- package/commands-page.js +460 -0
- package/commands.js +568 -0
- package/cucumber.js +43 -0
- package/ddev-addons-page.js +124 -0
- package/ddev-addons.js +154 -0
- package/ddev-custom-commands.js +96 -0
- package/ddev-domains.js +84 -0
- package/ddev-import.js +93 -0
- package/ddev-mail-page.js +167 -0
- package/ddev-mail.js +110 -0
- package/ddev-options.js +145 -0
- package/ddev-snapshots.js +126 -0
- package/ddev.js +145 -0
- package/docs/DEVELOPER-GUIDE.md +224 -0
- package/docs/README.md +252 -0
- package/docs/REFERENCE.md +684 -0
- package/docs/USER-GUIDE.md +697 -0
- package/docs/screenshots/assistant/avatar-board.png +0 -0
- package/docs/screenshots/assistant/avatar-coffee.png +0 -0
- package/docs/screenshots/assistant/avatar-colours.png +0 -0
- package/docs/screenshots/assistant/avatar-icons.png +0 -0
- package/docs/screenshots/assistant/avatar-moves.png +0 -0
- package/docs/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- package/docs/screenshots/assistant/avatar-settings-voices.png +0 -0
- package/docs/screenshots/assistant/avatar-status.png +0 -0
- package/docs/screenshots/create-card/create-card.png +0 -0
- package/docs/screenshots/create-card/start-ai.png +0 -0
- package/docs/screenshots/guide-clone-address.png +0 -0
- package/docs/screenshots/guide-clone-worktree-off.png +0 -0
- package/docs/screenshots/guide-clone-worktree-on.png +0 -0
- package/docs/screenshots/guide-ddev-menu.png +0 -0
- package/docs/screenshots/guide-domains.png +0 -0
- package/docs/screenshots/guide-home.png +0 -0
- package/docs/screenshots/guide-ide-menu.png +0 -0
- package/docs/screenshots/guide-remove-refusal.png +0 -0
- package/docs/screenshots/guide-rows-worktree.png +0 -0
- package/docs/screenshots/guide-screens.png +0 -0
- package/docs/screenshots/guide-settings.png +0 -0
- package/docs/screenshots/guide-workspace-page.png +0 -0
- package/docs/screenshots/home/card-settings.png +0 -0
- package/docs/screenshots/home/drag-reorder.png +0 -0
- package/docs/screenshots/home/edit-card.png +0 -0
- package/docs/screenshots/images/claude-code-image.png +0 -0
- package/docs/screenshots/images/edit-with-ai.png +0 -0
- package/docs/screenshots/images/gallery.png +0 -0
- package/docs/screenshots/images/generate-description.png +0 -0
- package/docs/screenshots/images/generate-plan.png +0 -0
- package/docs/screenshots/images/providers.png +0 -0
- package/docs/screenshots/images/svg-form.png +0 -0
- package/docs/screenshots/images/svg-result.png +0 -0
- package/docs/screenshots/images/upload.png +0 -0
- package/docs/screenshots/new-folder/2-form.png +0 -0
- package/docs/screenshots/new-folder/8-ddev-options.png +0 -0
- package/docs/screenshots/new-folder/files-folder.png +0 -0
- package/docs/screenshots/projects/ddev-custom-commands.png +0 -0
- package/docs/screenshots/projects/state-chip-stop.png +0 -0
- package/docs/screenshots/readme/dashboard-dark.png +0 -0
- package/docs/screenshots/readme/dashboard-light.png +0 -0
- package/docs/screenshots/readme/dev-dark.png +0 -0
- package/docs/screenshots/readme/dev-light.png +0 -0
- package/docs/screenshots/readme/docs-dark.png +0 -0
- package/docs/screenshots/readme/docs-light.png +0 -0
- package/docs/screenshots/readme/icons/ai.svg +4 -0
- package/docs/screenshots/readme/icons/code.svg +4 -0
- package/docs/screenshots/readme/icons/docs.svg +4 -0
- package/docs/screenshots/readme/icons/services.svg +4 -0
- package/docs/screenshots/readme/icons/sites.svg +4 -0
- package/docs/screenshots/readme/test-report-dark.png +0 -0
- package/docs/screenshots/readme/test-report-light.png +0 -0
- package/docs/screenshots/readme/test-scenarios.png +0 -0
- package/docs/screenshots/readme/test-steps.png +0 -0
- package/docs/screenshots/readme/videos-dark.png +0 -0
- package/docs/screenshots/readme/videos-light.png +0 -0
- package/docs/screenshots/screens-menu.png +0 -0
- package/docs/screenshots/screens-new.png +0 -0
- package/docs/screenshots/settings/auth-google-lists.png +0 -0
- package/docs/screenshots/settings/delete-1-row.png +0 -0
- package/docs/screenshots/settings/delete-2-confirm.png +0 -0
- package/docs/screenshots/settings/delete-3-type-name.png +0 -0
- package/docs/screenshots/settings/delete-4-type-yes.png +0 -0
- package/event-log.js +59 -0
- package/files-browser.js +247 -0
- package/folders.js +121 -0
- package/format.js +73 -0
- package/git-info.js +81 -0
- package/github-auth.js +62 -0
- package/graphs.js +104 -0
- package/html.js +10 -0
- package/icons.js +83 -0
- package/images-ai.js +447 -0
- package/images-create.js +226 -0
- package/images-page.js +417 -0
- package/images-records.js +39 -0
- package/images-render.js +36 -0
- package/images-svg-core.js +214 -0
- package/images-svg-sanitize.js +188 -0
- package/images-svg.js +241 -0
- package/images.js +270 -0
- package/jobs.js +430 -0
- package/linger.js +21 -0
- package/lists.js +292 -0
- package/media-info.js +136 -0
- package/navigation-page.js +264 -0
- package/navigation.js +394 -0
- package/notes.js +0 -0
- package/obsidian.js +405 -0
- package/package.json +47 -4
- package/palette-index.js +46 -0
- package/playwright.config.js +23 -0
- package/plugin-install.js +239 -0
- package/plugin-loader.js +448 -0
- package/plugin-manifest.js +83 -0
- package/plugin-settings.js +44 -0
- package/plugins/workspace-notes/index.js +59 -0
- package/plugins-page.js +103 -0
- package/project-make.js +241 -0
- package/project-name.js +210 -0
- package/public/assistant-avatar-moves.js +646 -0
- package/public/assistant-avatar-ui.js +1223 -0
- package/public/assistant-avatar.js +530 -0
- package/public/avatar/druplicon.vrm +0 -0
- package/public/css/base.css +260 -0
- package/public/css/components/ai-assistant.css +263 -0
- package/public/css/components/assistant-avatar.css +279 -0
- package/public/css/components/assistant-views.css +198 -0
- package/public/css/components/build-order.css +74 -0
- package/public/css/components/clone-as-a-git-worktree.css +30 -0
- package/public/css/components/command-palette.css +61 -0
- package/public/css/components/commands-center.css +139 -0
- package/public/css/components/confirm-dialog.css +40 -0
- package/public/css/components/create-card.css +8 -0
- package/public/css/components/ddev-menu.css +23 -0
- package/public/css/components/deep-chat.css +17 -0
- package/public/css/components/density.css +11 -0
- package/public/css/components/floating-assistant-workspace-pages.css +55 -0
- package/public/css/components/folders.css +5 -0
- package/public/css/components/full-height-left-assistant-sidebar-all-pages.css +39 -0
- package/public/css/components/full-height-right-actions-rail-pages-that-have-actions.css +267 -0
- package/public/css/components/grid-and-list.css +203 -0
- package/public/css/components/help-page.css +36 -0
- package/public/css/components/hero.css +16 -0
- package/public/css/components/images.css +70 -0
- package/public/css/components/layout-rhythm-and-spacing-polish.css +139 -0
- package/public/css/components/mic-on-any-field-that-wants-typing.css +416 -0
- package/public/css/components/modal-fit.css +113 -0
- package/public/css/components/page-width.css +23 -0
- package/public/css/components/plugins-page.css +9 -0
- package/public/css/components/project-cards.css +256 -0
- package/public/css/components/project-rows.css +390 -0
- package/public/css/components/reordering-the-workspace-cards.css +85 -0
- package/public/css/components/responsive-home-and-the-workspace-pages.css +457 -0
- package/public/css/components/running-commands-as-toasts.css +327 -0
- package/public/css/components/settings-editor-core-config-yml.css +828 -0
- package/public/css/components/short-results-as-a-toast-at-the-top.css +203 -0
- package/public/css/components/sound-player.css +84 -0
- package/public/css/components/suggest-agents-skills-commands.css +112 -0
- package/public/css/components/tab-modal.css +142 -0
- package/public/css/components/terminals.css +244 -0
- package/public/css/components/test-report-media-one-file-per-row-paged.css +144 -0
- package/public/css/components/top-toolbar-logo-breadcrumb.css +107 -0
- package/public/css/components/video-edit.css +6 -0
- package/public/css/components/web-ide-open-with.css +23 -0
- package/public/css/components/working-indicators.css +158 -0
- package/public/css/components/workspace-cards.css +79 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/htmx.min.js +1 -0
- package/public/logo.png +0 -0
- package/public/media-player.js +215 -0
- package/public/mic.js +255 -0
- package/public/narration.js +110 -0
- package/public/palette.js +278 -0
- package/public/sortable.js +251 -0
- package/public/suggest.js +337 -0
- package/public/terminal.js +903 -0
- package/public/themes/default/theme.css +267 -0
- package/public/themes/ember/theme.css +180 -0
- package/public/themes/press/theme.css +290 -0
- package/public/themes/slate/theme.css +182 -0
- package/public/touch-icon.png +0 -0
- package/public/ui.js +2721 -0
- package/public/vendor/ace/LICENSE.txt +24 -0
- package/public/vendor/ace/ace.js +23 -0
- package/public/vendor/ace/ext-language_tools.js +8 -0
- package/public/vendor/ace/ext-searchbox.js +8 -0
- package/public/vendor/ace/mode-css.js +8 -0
- package/public/vendor/ace/mode-html.js +8 -0
- package/public/vendor/ace/mode-javascript.js +8 -0
- package/public/vendor/ace/mode-markdown.js +8 -0
- package/public/vendor/ace/mode-sh.js +8 -0
- package/public/vendor/ace/mode-yaml.js +8 -0
- package/public/vendor/ace/theme-textmate.js +8 -0
- package/public/vendor/ace/theme-tomorrow_night.js +8 -0
- package/public/vendor/ace/worker-yaml.js +1 -0
- package/public/vendor/deep-chat.LICENSE +21 -0
- package/public/vendor/deep-chat.bundle.js +1 -0
- package/public/vendor/fonts/dm-serif-text-LICENSE.txt +93 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-LICENSE.txt +93 -0
- package/public/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- package/public/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- package/public/vendor/fonts/jetbrains-mono-LICENSE.txt +93 -0
- package/public/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/public/vendor/fonts/ubuntu-mono-LICENSE.txt +98 -0
- package/public/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- package/public/vendor/tabler-LICENSE.txt +21 -0
- package/public/vendor/tabler-sprite.svg +1 -0
- package/public/vendor/three/LICENSE +21 -0
- package/public/vendor/three/addons/loaders/GLTFLoader.js +4860 -0
- package/public/vendor/three/addons/utils/BufferGeometryUtils.js +1501 -0
- package/public/vendor/three/addons/utils/SkeletonUtils.js +496 -0
- package/public/vendor/three/three.core.min.js +6 -0
- package/public/vendor/three/three.module.min.js +6 -0
- package/public/vendor/three-vrm/LICENSE +22 -0
- package/public/vendor/three-vrm/three-vrm.module.min.js +983 -0
- package/public/vendor/uikit-icons.min.js +1 -0
- package/public/vendor/uikit.min.css +1 -0
- package/public/vendor/uikit.min.js +1 -0
- package/public/vendor/webship-icons/ui/bookmark-outlined.svg +1 -0
- package/public/vendor/webship-icons/ui/headset.svg +1 -0
- package/public/vendor/webship-icons/ui/menu.svg +1 -0
- package/public/vendor/webship-icons/ui/pause.svg +1 -0
- package/public/vendor/webship-icons/ui/play.svg +1 -0
- package/public/vendor/webship-icons/ui/share.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-off.svg +1 -0
- package/public/vendor/webship-icons/ui/sound-on.svg +1 -0
- package/public/vendor/webship-icons/ui/stop.svg +1 -0
- package/public/vendor/xterm/addon-fit.js +2 -0
- package/public/vendor/xterm/addon-unicode11.js +2 -0
- package/public/vendor/xterm/addon-web-links.js +2 -0
- package/public/vendor/xterm/xterm.css +218 -0
- package/public/vendor/xterm/xterm.js +2 -0
- package/rag.js +169 -0
- package/reader.js +132 -0
- package/repo-blobs.js +66 -0
- package/report-pdf.js +337 -0
- package/row-layout.js +1440 -0
- package/screenrc +37 -0
- package/screens.js +1063 -0
- package/scripts/code-server-start.sh +190 -0
- package/scripts/images-claude-finish.js +107 -0
- package/scripts/screen-host.sh +114 -0
- package/scripts/screen-runner.js +362 -0
- package/scripts/screen-then.js +47 -0
- package/scripts/update.sh +350 -0
- package/server.js +8140 -0
- package/settings-file.js +423 -0
- package/speech.js +297 -0
- package/tab-modal.js +75 -0
- package/test-reports.js +260 -0
- package/themes.js +381 -0
- package/transcripts.js +168 -0
- package/urls.js +13 -0
- package/validate.js +18 -0
- package/version-check.js +94 -0
- package/video-edit.js +588 -0
- package/waiting.js +85 -0
- package/workspace-card-tools.js +41 -0
- package/workspace-delete.js +139 -0
- package/workspace-types.js +85 -0
- package/workspaces.js +557 -0
package/report-pdf.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The automated-testing report, rendered for paper.
|
|
3
|
+
*
|
|
4
|
+
* Built from the run's JSON rather than from cucumber-html-reporter's page. Printing that page
|
|
5
|
+
* produced an unreadable PDF and it was not fixable there: its feature and scenario bodies are
|
|
6
|
+
* Bootstrap 3 collapsibles that open only once jQuery and Bootstrap have loaded from a CDN and
|
|
7
|
+
* somebody has clicked, so a print is a list of closed headers — and offline it is unstyled on top
|
|
8
|
+
* of that. The JSON carries everything the page shows, so this renders it directly.
|
|
9
|
+
*
|
|
10
|
+
* The audience is not the person who ran it. This goes to a product owner, a PMO and a QA lead, so
|
|
11
|
+
* it opens with the verdict and the shape of the run, puts the failures first (that is the part
|
|
12
|
+
* that gets acted on), and only then lists every scenario and step. It is a document, not a dump.
|
|
13
|
+
*
|
|
14
|
+
* Rendered by WeasyPrint, which honours @page, running footers, `break-inside: avoid` and inline
|
|
15
|
+
* SVG. Chromium prints too, but ignores most of that and re-fetches the CDN assets.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const { esc } = require('./html');
|
|
22
|
+
const { humanTime } = require('./format');
|
|
23
|
+
const { CONFIG_DIR } = require('./workspaces');
|
|
24
|
+
|
|
25
|
+
// Status colours are the one place this file names a colour outright: a PDF has no theme to
|
|
26
|
+
// follow, and green-for-passed has to be the same green in every report anybody compares.
|
|
27
|
+
const OK = '#1f8a4c';
|
|
28
|
+
const BAD = '#c0362c';
|
|
29
|
+
const WARN = '#b26a00';
|
|
30
|
+
const MUTED = '#8a8a8a';
|
|
31
|
+
|
|
32
|
+
const ms = (ns) => (ns ? `${(ns / 1e6).toFixed(0)} ms` : '—');
|
|
33
|
+
|
|
34
|
+
// Long runs are reported in the unit a person would say out loud.
|
|
35
|
+
function humanDuration(ns) {
|
|
36
|
+
const s = ns / 1e9;
|
|
37
|
+
if (!s) return '—';
|
|
38
|
+
if (s < 60) return `${s.toFixed(1)} s`;
|
|
39
|
+
const m = Math.floor(s / 60);
|
|
40
|
+
if (m < 60) return `${m} min ${Math.round(s - m * 60)} s`;
|
|
41
|
+
return `${Math.floor(m / 60)} h ${m % 60} min`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function statusOf(steps) {
|
|
45
|
+
const states = steps.map((st) => (st.result || {}).status);
|
|
46
|
+
return states.includes('failed') ? 'failed'
|
|
47
|
+
: states.includes('undefined') || states.includes('ambiguous') ? 'undefined'
|
|
48
|
+
: states.some((x) => x === 'passed') ? 'passed' : 'skipped';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A pass-rate ring, drawn as inline SVG rather than fetched from a chart library: it has to render
|
|
52
|
+
// with no network and inside WeasyPrint, and one arc is not worth a dependency.
|
|
53
|
+
function donut(passed, failed, skipped, total) {
|
|
54
|
+
if (!total) return '';
|
|
55
|
+
const R = 26, C = 2 * Math.PI * R;
|
|
56
|
+
const seg = (n, colour, offset) => (n
|
|
57
|
+
? `<circle cx="34" cy="34" r="${R}" fill="none" stroke="${colour}" stroke-width="12"
|
|
58
|
+
stroke-dasharray="${((n / total) * C).toFixed(2)} ${C.toFixed(2)}"
|
|
59
|
+
stroke-dashoffset="${(-offset / total * C).toFixed(2)}" transform="rotate(-90 34 34)"/>`
|
|
60
|
+
: '');
|
|
61
|
+
const pct = Math.round((passed / total) * 100);
|
|
62
|
+
return `<svg class="donut" width="68" height="68" viewBox="0 0 68 68" aria-hidden="true">
|
|
63
|
+
<circle cx="34" cy="34" r="${R}" fill="none" stroke="#e8e8e8" stroke-width="12"/>
|
|
64
|
+
${seg(passed, OK, 0)}${seg(failed, BAD, passed)}${seg(skipped, MUTED, passed + failed)}
|
|
65
|
+
<text x="34" y="38" text-anchor="middle" font-size="15" font-weight="700">${pct}%</text>
|
|
66
|
+
</svg>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} key the workspace the project is in
|
|
71
|
+
* @param {string} project the project directory name
|
|
72
|
+
* @param {object} report the report record (json, html, mtime, rel, shots, videos)
|
|
73
|
+
* @param {string} dir the workspace directory
|
|
74
|
+
*/
|
|
75
|
+
function testReportPdfHtml(key, project, report, dir) {
|
|
76
|
+
let data = [];
|
|
77
|
+
try { data = JSON.parse(fs.readFileSync(path.join(dir, project, report.json), 'utf8')); } catch (_) { data = []; }
|
|
78
|
+
const brand = (() => {
|
|
79
|
+
try { return JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, 'brand-tokens.json'), 'utf8')).tokens || {}; } catch (_) { return {}; }
|
|
80
|
+
})();
|
|
81
|
+
const c = (name, fallback) => brand[name] || fallback;
|
|
82
|
+
|
|
83
|
+
// A failed step's screenshot, inlined: a PDF that references a file on this machine shows
|
|
84
|
+
// nothing to whoever it is sent to. Matched by the stem convention webship-js writes.
|
|
85
|
+
const shotFor = (scenarioName) => {
|
|
86
|
+
const stem = scenarioName.toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
87
|
+
const hit = (report.shots || []).find((f) => f.name.toLowerCase().replace(/[^a-z0-9]+/g, '').includes(stem.slice(0, 24)));
|
|
88
|
+
if (!hit) return null;
|
|
89
|
+
try {
|
|
90
|
+
const full = path.join(dir, project, hit.file);
|
|
91
|
+
// A report of a bad run can carry a hundred of these. Above 1.5 MB the picture is a full-page
|
|
92
|
+
// screenshot whose detail the PDF cannot show anyway, and ten of them make a file nobody can
|
|
93
|
+
// email — the report says the screenshot exists and where, instead.
|
|
94
|
+
if (fs.statSync(full).size > 1.5 * 1024 * 1024) return { oversize: hit.name };
|
|
95
|
+
const b64 = fs.readFileSync(full).toString('base64');
|
|
96
|
+
const type = /\.jpe?g$/i.test(hit.file) ? 'jpeg' : 'png';
|
|
97
|
+
return { src: `data:image/${type};base64,${b64}`, name: hit.name };
|
|
98
|
+
} catch (_) { return null; }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const shotHtml = (scenarioName) => {
|
|
102
|
+
const s = shotFor(scenarioName);
|
|
103
|
+
if (!s) return '';
|
|
104
|
+
if (s.oversize) {
|
|
105
|
+
return `<p class="shot-note">Screenshot too large to embed: <code>${esc(s.oversize)}</code> — see the run's screenshots directory.</p>`;
|
|
106
|
+
}
|
|
107
|
+
return `<figure class="shot"><img src="${s.src}" alt=""><figcaption>${esc(s.name)}</figcaption></figure>`;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// One pass over the JSON, collecting what both the summary and the detail need.
|
|
111
|
+
let scenarios = 0, passed = 0, failed = 0, skipped = 0, stepCount = 0, totalNs = 0;
|
|
112
|
+
const featureStats = [];
|
|
113
|
+
const failures = [];
|
|
114
|
+
|
|
115
|
+
const features = data.map((feat) => {
|
|
116
|
+
const featName = feat.name || '(unnamed feature)';
|
|
117
|
+
let fScen = 0, fPass = 0, fFail = 0, fSkip = 0, fNs = 0;
|
|
118
|
+
|
|
119
|
+
const rows = (feat.elements || []).filter((sc) => !sc.type || sc.type === 'scenario').map((sc) => {
|
|
120
|
+
const steps = (sc.steps || []).filter((st) => !st.hidden);
|
|
121
|
+
const status = statusOf(steps);
|
|
122
|
+
const scNs = steps.reduce((n, st) => n + ((st.result || {}).duration || 0), 0);
|
|
123
|
+
scenarios += 1; stepCount += steps.length; totalNs += scNs;
|
|
124
|
+
fScen += 1; fNs += scNs;
|
|
125
|
+
if (status === 'passed') { passed += 1; fPass += 1; } else if (status === 'failed') { failed += 1; fFail += 1; } else { skipped += 1; fSkip += 1; }
|
|
126
|
+
|
|
127
|
+
const tags = (sc.tags || []).map((t) => t.name).join(' ');
|
|
128
|
+
const stepRows = steps.map((st) => {
|
|
129
|
+
const r = st.result || {};
|
|
130
|
+
const err = r.error_message
|
|
131
|
+
? `<tr class="err"><td colspan="3"><pre>${esc(String(r.error_message).slice(0, 4000))}</pre></td></tr>` : '';
|
|
132
|
+
return `<tr class="s-${esc(r.status || 'unknown')}">
|
|
133
|
+
<td class="kw">${esc(st.keyword || '')}</td>
|
|
134
|
+
<td>${esc(st.name || '')}</td>
|
|
135
|
+
<td class="num">${esc(ms(r.duration))}</td>
|
|
136
|
+
</tr>${err}`;
|
|
137
|
+
}).join('');
|
|
138
|
+
|
|
139
|
+
if (status === 'failed') {
|
|
140
|
+
const firstErr = steps.map((st) => (st.result || {}).error_message).find(Boolean) || '';
|
|
141
|
+
const failedStep = steps.find((st) => (st.result || {}).status === 'failed');
|
|
142
|
+
failures.push({
|
|
143
|
+
feature: featName,
|
|
144
|
+
name: sc.name || '(unnamed)',
|
|
145
|
+
step: failedStep ? `${failedStep.keyword || ''}${failedStep.name || ''}`.trim() : '',
|
|
146
|
+
error: String(firstErr).slice(0, 1200),
|
|
147
|
+
shot: shotHtml(sc.name || ''),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return `<section class="scenario ${status}">
|
|
152
|
+
<h3><span class="pill ${status}">${status}</span> ${esc(sc.name || '(unnamed)')}</h3>
|
|
153
|
+
${tags ? `<p class="tags">${esc(tags)}</p>` : ''}
|
|
154
|
+
<table class="steps"><tbody>${stepRows}</tbody></table>
|
|
155
|
+
<p class="scenario-meta">${steps.length} steps · ${esc(ms(scNs))}</p>
|
|
156
|
+
</section>`;
|
|
157
|
+
}).join('');
|
|
158
|
+
|
|
159
|
+
if (!rows) return '';
|
|
160
|
+
featureStats.push({ name: featName, uri: feat.uri || '', scenarios: fScen, passed: fPass, failed: fFail, skipped: fSkip, ns: fNs });
|
|
161
|
+
return `<section class="feature">
|
|
162
|
+
<h2>${esc(featName)}</h2>
|
|
163
|
+
<p class="feature-meta">${fScen} scenario${fScen === 1 ? '' : 's'} ·
|
|
164
|
+
<span class="ok">${fPass} passed</span>${fFail ? ` · <span class="bad">${fFail} failed</span>` : ''}${fSkip ? ` · ${fSkip} skipped` : ''} ·
|
|
165
|
+
${esc(humanDuration(fNs))}${feat.uri ? ` · <span class="uri">${esc(feat.uri)}</span>` : ''}</p>
|
|
166
|
+
${rows}</section>`;
|
|
167
|
+
}).join('');
|
|
168
|
+
|
|
169
|
+
const pct = scenarios ? Math.round((passed / scenarios) * 100) : 0;
|
|
170
|
+
const verdict = failed ? 'FAILED' : scenarios ? 'PASSED' : 'NO SCENARIOS';
|
|
171
|
+
|
|
172
|
+
const featureTable = featureStats.length ? `
|
|
173
|
+
<table class="glance">
|
|
174
|
+
<thead><tr><th>Feature</th><th class="n">Scenarios</th><th class="n">Passed</th><th class="n">Failed</th><th class="n">Pass rate</th><th class="n">Time</th></tr></thead>
|
|
175
|
+
<tbody>${featureStats.map((f) => `
|
|
176
|
+
<tr class="${f.failed ? 'has-failures' : ''}">
|
|
177
|
+
<td>${esc(f.name)}</td>
|
|
178
|
+
<td class="n">${f.scenarios}</td>
|
|
179
|
+
<td class="n ok">${f.passed}</td>
|
|
180
|
+
<td class="n ${f.failed ? 'bad' : ''}">${f.failed || '—'}</td>
|
|
181
|
+
<td class="n">${f.scenarios ? Math.round((f.passed / f.scenarios) * 100) : 0}%</td>
|
|
182
|
+
<td class="n">${esc(humanDuration(f.ns))}</td>
|
|
183
|
+
</tr>`).join('')}</tbody>
|
|
184
|
+
</table>` : '';
|
|
185
|
+
|
|
186
|
+
const failuresSection = failures.length ? `
|
|
187
|
+
<section class="failures">
|
|
188
|
+
<h2 class="failures-h">What failed <span class="count">${failures.length}</span></h2>
|
|
189
|
+
<p class="section-lede">Every failing scenario, with the step it stopped on and the screenshot taken at that
|
|
190
|
+
moment. The full run follows.</p>
|
|
191
|
+
${failures.map((f, i) => `
|
|
192
|
+
<section class="failure">
|
|
193
|
+
<h3><span class="idx">${i + 1}</span> ${esc(f.name)}</h3>
|
|
194
|
+
<p class="failure-where">${esc(f.feature)}${f.step ? ` · stopped on <strong>${esc(f.step)}</strong>` : ''}</p>
|
|
195
|
+
${f.error ? `<pre class="failure-err">${esc(f.error)}</pre>` : ''}
|
|
196
|
+
${f.shot}
|
|
197
|
+
</section>`).join('')}
|
|
198
|
+
</section>` : '';
|
|
199
|
+
|
|
200
|
+
return `<!doctype html>
|
|
201
|
+
<html lang="en"><head><meta charset="utf-8"><title>${esc(project)} — automated test report</title>
|
|
202
|
+
<style>
|
|
203
|
+
@page {
|
|
204
|
+
size: A4; margin: 20mm 16mm 18mm 16mm;
|
|
205
|
+
@top-right { content: "${esc(project)}"; font-size: 7.5pt; color: ${c('text-subtle', '#4d4d4d')}; }
|
|
206
|
+
@bottom-left { content: "Automated functional acceptance testing"; font-size: 7.5pt; color: ${c('text-subtle', '#4d4d4d')}; }
|
|
207
|
+
@bottom-right { content: "Page " counter(page) " of " counter(pages); font-size: 7.5pt; color: ${c('text-subtle', '#4d4d4d')}; }
|
|
208
|
+
}
|
|
209
|
+
/* The cover carries the running header nowhere: a title page with a header on it reads as
|
|
210
|
+
page two of something else. */
|
|
211
|
+
@page :first { @top-right { content: ""; } }
|
|
212
|
+
|
|
213
|
+
body { font-family: "DejaVu Sans", "Helvetica Neue", Arial, sans-serif; font-size: 9.5pt;
|
|
214
|
+
color: ${c('text-default', '#000')}; line-height: 1.45; }
|
|
215
|
+
|
|
216
|
+
/* ---- cover ---- */
|
|
217
|
+
.cover { border-bottom: 3px solid ${c('brand-primary', '#0074e0')}; padding-bottom: 5mm; margin-bottom: 6mm; }
|
|
218
|
+
.eyebrow { font-size: 8pt; letter-spacing: .10em; text-transform: uppercase;
|
|
219
|
+
color: ${c('brand-primary', '#0074e0')}; margin: 0 0 1mm; font-weight: 600; }
|
|
220
|
+
h1 { font-size: 22pt; margin: 0 0 2mm; color: ${c('brand-secondary', '#2b447c')}; line-height: 1.15; }
|
|
221
|
+
.lede { color: ${c('text-subtle', '#4d4d4d')}; margin: 0; font-size: 9pt; }
|
|
222
|
+
.verdict { display: inline-block; margin-top: 3mm; padding: 1.2mm 4mm; border-radius: 1mm;
|
|
223
|
+
font-size: 10pt; font-weight: 700; letter-spacing: .06em; color: #fff; background: ${MUTED}; }
|
|
224
|
+
.verdict.PASSED { background: ${OK}; } .verdict.FAILED { background: ${BAD}; }
|
|
225
|
+
|
|
226
|
+
/* ---- summary ---- */
|
|
227
|
+
.summary-row { display: flex; gap: 5mm; align-items: center; margin: 0 0 7mm; }
|
|
228
|
+
.donut-wrap { flex: 0 0 auto; text-align: center; }
|
|
229
|
+
.donut-wrap .cap { font-size: 7pt; text-transform: uppercase; letter-spacing: .04em;
|
|
230
|
+
color: ${c('text-subtle', '#4d4d4d')}; display: block; margin-top: 1mm; }
|
|
231
|
+
.summary { display: flex; gap: 2.5mm; flex: 1; }
|
|
232
|
+
.tile { flex: 1; border: 1px solid ${c('brand-light', '#ebe8e5')}; border-radius: 2mm;
|
|
233
|
+
padding: 2.5mm 1mm; text-align: center; background: ${c('subtle-light', '#f5f3f2')}; }
|
|
234
|
+
.tile .n { display: block; font-size: 15pt; font-weight: 700; line-height: 1.1; }
|
|
235
|
+
.tile .l { font-size: 6.8pt; text-transform: uppercase; letter-spacing: .04em;
|
|
236
|
+
color: ${c('text-subtle', '#4d4d4d')}; }
|
|
237
|
+
.tile.ok .n { color: ${OK}; } .tile.bad .n { color: ${BAD}; }
|
|
238
|
+
.tile.brand .n { color: ${c('brand-primary', '#0074e0')}; }
|
|
239
|
+
|
|
240
|
+
h2 { font-size: 13pt; margin: 8mm 0 2mm; color: ${c('brand-primary', '#0074e0')};
|
|
241
|
+
border-bottom: 2px solid ${c('subtle-brand', '#deedff')}; padding-bottom: 1.5mm;
|
|
242
|
+
break-after: avoid; }
|
|
243
|
+
h3 { font-size: 10.5pt; margin: 4mm 0 1.5mm; font-weight: 600; break-after: avoid; }
|
|
244
|
+
.section-lede { color: ${c('text-subtle', '#4d4d4d')}; font-size: 8.5pt; margin: 0 0 4mm; }
|
|
245
|
+
|
|
246
|
+
/* ---- features at a glance ---- */
|
|
247
|
+
table.glance { width: 100%; border-collapse: collapse; font-size: 8.5pt; margin: 0 0 4mm; }
|
|
248
|
+
table.glance th { text-align: left; font-size: 7.5pt; text-transform: uppercase; letter-spacing: .04em;
|
|
249
|
+
color: ${c('text-subtle', '#4d4d4d')}; border-bottom: 1pt solid ${c('brand-light', '#ebe8e5')};
|
|
250
|
+
padding: 1.5mm 1.5mm; }
|
|
251
|
+
table.glance td { padding: 1.2mm 1.5mm; border-bottom: 0.3pt solid ${c('brand-light', '#ebe8e5')};
|
|
252
|
+
vertical-align: top; }
|
|
253
|
+
table.glance th.n, table.glance td.n { text-align: right; white-space: nowrap; }
|
|
254
|
+
table.glance tr.has-failures td:first-child { border-left: 2px solid ${BAD}; padding-left: 1mm; }
|
|
255
|
+
.ok { color: ${OK}; } .bad { color: ${BAD}; font-weight: 600; }
|
|
256
|
+
|
|
257
|
+
/* ---- what failed ---- */
|
|
258
|
+
.failures-h .count { display: inline-block; background: ${BAD}; color: #fff; font-size: 8pt;
|
|
259
|
+
border-radius: 1mm; padding: 0.3mm 2mm; vertical-align: middle; margin-left: 1.5mm; }
|
|
260
|
+
.failure { break-inside: avoid; margin: 0 0 5mm; padding: 2.5mm 3mm; border-radius: 2mm;
|
|
261
|
+
background: #fdf6f5; border-left: 3px solid ${BAD}; }
|
|
262
|
+
.failure h3 { margin: 0 0 1mm; }
|
|
263
|
+
.failure .idx { display: inline-block; min-width: 5mm; color: ${BAD}; font-weight: 700; }
|
|
264
|
+
.failure-where { font-size: 8pt; color: ${c('text-subtle', '#4d4d4d')}; margin: 0 0 2mm; }
|
|
265
|
+
.failure-err { margin: 0 0 2mm; padding: 2mm; background: #fff; border: 0.3pt solid #e6cdc9;
|
|
266
|
+
font-family: "DejaVu Sans Mono", monospace; font-size: 7.2pt; white-space: pre-wrap;
|
|
267
|
+
max-height: 60mm; overflow: hidden; }
|
|
268
|
+
|
|
269
|
+
/* ---- the full run ---- */
|
|
270
|
+
.feature { break-inside: auto; }
|
|
271
|
+
.feature-meta { font-size: 8pt; color: ${c('text-subtle', '#4d4d4d')}; margin: 0 0 2.5mm; }
|
|
272
|
+
.uri { font-family: "DejaVu Sans Mono", monospace; }
|
|
273
|
+
/* A scenario is the unit you read, so it is the unit that must not be split. */
|
|
274
|
+
.scenario { break-inside: avoid; margin: 0 0 3.5mm; padding-left: 3mm;
|
|
275
|
+
border-left: 2px solid ${c('subtle-brand', '#deedff')}; }
|
|
276
|
+
.scenario.failed { border-left-color: ${BAD}; }
|
|
277
|
+
.tags, .scenario-meta { font-size: 8pt; color: ${c('text-subtle', '#4d4d4d')}; margin: 0 0 1.5mm; }
|
|
278
|
+
.tags { font-family: "DejaVu Sans Mono", monospace; }
|
|
279
|
+
table.steps { width: 100%; border-collapse: collapse; margin: 1mm 0; }
|
|
280
|
+
table.steps td { padding: 0.8mm 1.5mm; vertical-align: top;
|
|
281
|
+
border-bottom: 0.3pt solid ${c('brand-light', '#ebe8e5')}; }
|
|
282
|
+
td.kw { width: 16mm; font-weight: 600; color: ${c('brand-secondary', '#2b447c')}; }
|
|
283
|
+
td.num { width: 18mm; text-align: right; color: ${c('text-subtle', '#4d4d4d')}; font-size: 8pt; }
|
|
284
|
+
tr.s-failed td { background: #fdf1f0; }
|
|
285
|
+
tr.s-skipped td, tr.s-undefined td { color: ${MUTED}; }
|
|
286
|
+
tr.err pre { margin: 1mm 0; padding: 2mm; background: #fdf1f0; border-left: 2px solid ${BAD};
|
|
287
|
+
font-family: "DejaVu Sans Mono", monospace; font-size: 7.2pt; white-space: pre-wrap; }
|
|
288
|
+
.pill { display: inline-block; font-size: 7pt; text-transform: uppercase; letter-spacing: .04em;
|
|
289
|
+
padding: 0.4mm 1.6mm; border-radius: 1mm; color: #fff; background: ${MUTED}; margin-right: 1.5mm; }
|
|
290
|
+
.pill.passed { background: ${OK}; } .pill.failed { background: ${BAD}; }
|
|
291
|
+
.pill.undefined { background: ${WARN}; }
|
|
292
|
+
.shot { margin: 2mm 0; break-inside: avoid; }
|
|
293
|
+
.shot img { max-width: 100%; max-height: 90mm; border: 1px solid ${c('brand-light', '#ebe8e5')}; }
|
|
294
|
+
.shot figcaption, .shot-note { font-size: 7.2pt; color: ${c('text-subtle', '#4d4d4d')}; }
|
|
295
|
+
.legend { font-size: 7.5pt; color: ${c('text-subtle', '#4d4d4d')}; margin: 0 0 5mm; }
|
|
296
|
+
.legend .key { display: inline-block; width: 2.5mm; height: 2.5mm; border-radius: .5mm;
|
|
297
|
+
margin: 0 1mm 0 3mm; vertical-align: middle; }
|
|
298
|
+
.foot { margin-top: 8mm; padding-top: 2mm; font-size: 7.5pt; color: ${c('text-subtle', '#4d4d4d')};
|
|
299
|
+
border-top: 1px solid ${c('brand-light', '#ebe8e5')}; }
|
|
300
|
+
</style></head><body>
|
|
301
|
+
|
|
302
|
+
<header class="cover">
|
|
303
|
+
<p class="eyebrow">Automated functional acceptance testing</p>
|
|
304
|
+
<h1>${esc(project)}</h1>
|
|
305
|
+
<p class="lede">${esc(key)} workspace · run of ${esc(humanTime(report.mtime))} · ${esc(humanDuration(totalNs))} of step time</p>
|
|
306
|
+
<span class="verdict ${verdict.replace(/\s/g, '')}">${verdict}</span>
|
|
307
|
+
</header>
|
|
308
|
+
|
|
309
|
+
<div class="summary-row">
|
|
310
|
+
<div class="donut-wrap">${donut(passed, failed, skipped, scenarios)}<span class="cap">pass rate</span></div>
|
|
311
|
+
<div class="summary">
|
|
312
|
+
<div class="tile brand"><span class="n">${scenarios}</span><span class="l">scenarios</span></div>
|
|
313
|
+
<div class="tile ok"><span class="n">${passed}</span><span class="l">passed</span></div>
|
|
314
|
+
<div class="tile ${failed ? 'bad' : ''}"><span class="n">${failed}</span><span class="l">failed</span></div>
|
|
315
|
+
<div class="tile"><span class="n">${skipped}</span><span class="l">skipped</span></div>
|
|
316
|
+
<div class="tile"><span class="n">${featureStats.length}</span><span class="l">features</span></div>
|
|
317
|
+
<div class="tile"><span class="n">${stepCount}</span><span class="l">steps</span></div>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<p class="legend"><span class="key" style="background:${OK}"></span>passed
|
|
322
|
+
<span class="key" style="background:${BAD}"></span>failed
|
|
323
|
+
<span class="key" style="background:${MUTED}"></span>skipped or undefined</p>
|
|
324
|
+
|
|
325
|
+
${featureTable ? `<h2>Features at a glance</h2>${featureTable}` : ''}
|
|
326
|
+
${failuresSection}
|
|
327
|
+
|
|
328
|
+
<h2>The full run</h2>
|
|
329
|
+
<p class="section-lede">Every scenario in the order it ran, with each step and how long it took.</p>
|
|
330
|
+
${features || '<p>This report contains no scenarios.</p>'}
|
|
331
|
+
|
|
332
|
+
<p class="foot">${esc(report.rel)} · ${scenarios} scenarios · ${stepCount} steps · ${esc(humanDuration(totalNs))} of step time ·
|
|
333
|
+
rendered by the Webship Workspace dashboard. Screenshots of failures are embedded, so this file is complete on its own.</p>
|
|
334
|
+
</body></html>`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = { testReportPdfHtml, humanDuration, statusOf };
|