Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion e2e/global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export default async function globalSetup(): Promise<void> {
}

const bin: string = ensureLightpanda();
console.log(`[e2e] lightpanda ${lightpandaVersion(bin)}`);

const port = await freePort();
// --load-resources stylesheet: external CSS is NOT fetched by default, and several
Expand Down
2 changes: 1 addition & 1 deletion e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineConfig({
workers: 1,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
reporter: process.env.CI ? [['github'], ['list']] : [['list']],
reporter: process.env.CI ? [['github'], ['list']] : [['./reporters/quiet.mjs']],
timeout: 60_000,
expect: { timeout: 10_000 },
projects: [
Expand Down
29 changes: 29 additions & 0 deletions e2e/reporters/quiet.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Silent on success: prints only failures and one summary line.
export default class QuietReporter {
#failed = [];
#passed = 0;
#skipped = 0;

printsToStdio() {
return true;
}

onTestEnd(test, result) {
if (result.status === 'skipped') this.#skipped++;
else if (result.status === test.expectedStatus) this.#passed++;
else this.#failed.push({ test, result });
}

onEnd() {
for (const { test, result } of this.#failed) {
console.log(`✘ ${test.titlePath().filter(Boolean).join(' › ')}`);
for (const e of result.errors) console.log(e.stack ?? e.message ?? String(e));
}
const skipped = this.#skipped ? `, ${this.#skipped} skipped` : '';
console.log(
this.#failed.length
? `✘ e2e: ${this.#failed.length} failed, ${this.#passed} passed${skipped}`
: `✔ e2e (${this.#passed} passed${skipped})`,
);
}
}
77 changes: 54 additions & 23 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[settings]
# No "Finished in Ns" line after every task: silent on success.
quiet = true

[tools]
node = "24"
# Default aqua backend, NOT `npm:pnpm`: pnpm's npm tarball ships a placeholder `bin/pnpm`
Expand Down Expand Up @@ -91,27 +95,27 @@ description = "Build Rust system tray binary"
quiet = true
dir = "{{config_root}}/rust/deckbridge-tray"
depends = ["deckbridge-native"]
run = "cargo build --release"
run = "cargo build -q --release"

[tasks.ts-install]
description = "Install TypeScript/esbuild dev dependencies"
quiet = true
dir = "ts"
run = "pnpm install --silent && echo pnpm install OK"
run = "pnpm install --silent"

[tasks.ts]
description = "Bundle TypeScript with esbuild (simple-only UI: excludes the advanced view + its CSS)"
quiet = true
dir = "ts"
depends = ["ts-install", "deckbridge-native"]
run = "node build.mjs"
run = "node ../scripts/quiet.mjs --fail-only -- node build.mjs"

[tasks.ts-mock]
description = "Bundle TypeScript with mock driver and simulation routes"
quiet = true
dir = "ts"
depends = ["ts-install", "deckbridge-native"]
run = "DECKBRIDGE_BUILD_MOCK=1 node build.mjs"
run = "DECKBRIDGE_BUILD_MOCK=1 node ../scripts/quiet.mjs --fail-only -- node build.mjs"

[tasks.build]
description = "Build everything (simple-only UI)"
Expand Down Expand Up @@ -164,9 +168,13 @@ depends = ["compile-advanced"]
run = "SRC_BIN=deckbridge-advanced node scripts/package.mjs ${1:-dev}"

[tasks.lint]
description = "Lint with oxlint"
description = "Lint with oxlint + eslint (silent unless findings)"
quiet = true
depends = ["ts-install"]
run = "./ts/node_modules/.bin/oxlint ts && echo 'oxlint done ✅, waiting for eslint...' && ./ts/node_modules/.bin/eslint ts"
run = """
node scripts/quiet.mjs -- ./ts/node_modules/.bin/oxlint ts
node scripts/quiet.mjs --ok=lint -- ./ts/node_modules/.bin/eslint ts
"""

[tasks.tauri-build]
description = "Build Windows NSIS installer via Tauri (compile + stage sidecars + pinned npm CLI)"
Expand All @@ -177,7 +185,7 @@ run = "node scripts/tauri-build.mjs"
description = "Install pinned prebuilt Tauri CLI"
quiet = true
dir = "src-tauri"
run = "pnpm install --frozen-lockfile --silent && echo Tauri CLI install OK"
run = "pnpm install --frozen-lockfile --silent"

[tasks.tauri-build-macos]
description = "Build macOS .app + .dmg via Tauri (compile + stage sidecars + pinned npm CLI)"
Expand Down Expand Up @@ -224,35 +232,43 @@ run = "node ../scripts/tauri-check-stubs.mjs && cargo check"
# rust/rust-toolchain.toml, so rustup installs it on first use.
[tasks.rust-lint]
description = "Rust fmt-check + clippy + tests"
quiet = true
dir = "rust"
run = """
cargo fmt --check -p deckbridge-native -p deckbridge-tray
cargo clippy --workspace --all-targets -- -D warnings
cargo clippy -p deckbridge-native --no-default-features --features jpeg-fork,usb --all-targets -- -D warnings
cargo test --workspace
cargo clippy -p deckbridge-native --target x86_64-pc-windows-msvc --no-default-features --features jpeg-upstream --all-targets -- -D warnings
Q="node ../scripts/quiet.mjs --fail-only --"
$Q cargo fmt --check -p deckbridge-native -p deckbridge-tray
$Q cargo clippy -q --workspace --all-targets -- -D warnings
$Q cargo clippy -q -p deckbridge-native --no-default-features --features jpeg-fork,usb --all-targets -- -D warnings
$Q cargo test -q --workspace
$Q cargo clippy -q -p deckbridge-native --target x86_64-pc-windows-msvc --no-default-features --features jpeg-upstream --all-targets -- -D warnings
echo '✔ rust-lint'
"""

[tasks.format]
description = "Format with oxfmt (single quotes)"
quiet = true
depends = ["ts-install"]
run = "./ts/node_modules/.bin/oxfmt ts/src ts/test docs-site/src docs-site/scripts e2e/fixtures e2e/helpers e2e/scripts e2e/tests e2e/playwright.config.ts e2e/global-setup.ts e2e/global-teardown.ts"
run = "node scripts/quiet.mjs --ok=format -- ./ts/node_modules/.bin/oxfmt ts/src ts/test docs-site/src docs-site/scripts e2e/fixtures e2e/helpers e2e/scripts e2e/tests e2e/playwright.config.ts e2e/global-setup.ts e2e/global-teardown.ts"

[tasks.check]
description = "Typecheck + lint + format check"
quiet = true
depends = ["types", "lint", "format"]
run = "echo '✔ check'"

[tasks.types]
description = "Type-check TypeScript (no emit)"
quiet = true
dir = "ts"
depends = ["ts-install"]
run = "pnpm exec tsgo --noEmit"
run = "node ../scripts/quiet.mjs --ok=types -- pnpm exec tsgo --noEmit"

[tasks.knip]
description = "Find unused files, exports, types, and dependencies (knip)"
quiet = true
dir = "ts"
depends = ["ts-install"]
run = "pnpm exec knip"
run = "node ../scripts/quiet.mjs --ok=knip -- pnpm exec knip"

[tasks.loc]
description = "Top 10 largest .ts/.tsx files under ts/src by line count (excl. node_modules/dist/test, .d.ts)"
Expand All @@ -261,8 +277,9 @@ run = "node scripts/loc.mjs"

[tasks.check-loc]
description = "Fail (exit 1) if any .ts/.tsx file under ts/src exceeds 500 lines (test/ excluded)"
quiet = true
dir = "ts"
run = "node scripts/check-loc.mjs"
run = "node scripts/check-loc.mjs && echo '✔ check-loc'"


[tasks.docs-devices]
Expand All @@ -273,21 +290,26 @@ run = "node scripts/gen-device-docs.mjs"

[tasks.docs-devices-check]
description = "Fail if the generated device docs are stale vs DEVICE_MODELS"
quiet = true
dir = "ts"
depends = ["ts-install"]
run = "node scripts/gen-device-docs.mjs --check"
run = "node ../scripts/quiet.mjs --fail-only --ok=docs-devices -- node scripts/gen-device-docs.mjs --check"

[tasks.ci-checks]
description = "Lint, typecheck, core/browser tests, knip, check-loc, and generated device docs in parallel"
quiet = true
depends = ["lint", "types", "test", "test-client", "knip", "check-loc", "docs-devices-check"]
run = "echo '✔ ci-checks'"
timeout = "120s"

[tasks.beforeCommit]
description = "Format, lint, types, test, browser e2e, and compile — run before committing"
quiet = true
# e2e-browser is e2e-app + e2e-docs. e2e-docs carries `wait_for = ["docs-check"]`, so in
# this run it starts only once docs-check has produced docs-site/build/.
depends = ["format", "ci-checks", "rust-lint", "docs-check", "e2e-types", "e2e-browser", "compile"]
dir = "ts"
run = "echo '✔ beforeCommit'"
# 300s, not 150s: e2e-docs can't start until docs-check's Docusaurus build finishes.
timeout = "360s"

Expand Down Expand Up @@ -347,6 +369,7 @@ run = """

[tasks.e2e]
description = "Black-box smoke test of a packaged release zip (usage: mise run e2e -- dist/foo.zip)"
quiet = true
run = "./scripts/e2e-smoke.sh ${1:-}"
timeout = "30s"

Expand All @@ -355,40 +378,45 @@ timeout = "30s"
# screenshots, no boundingBox and no `locator.click()` — see e2e/helpers/click.ts.
[tasks.e2e-install]
description = "Install the Playwright e2e suite's dependencies"
quiet = true
dir = "e2e"
# The bundled Chromium is never launched (Lightpanda is the runtime; the E2E_BROWSER=chromium
# triage path uses $CHROME_BIN / a system install), so skip the ~150 MB download.
env = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1" }
run = "pnpm install --silent && echo e2e pnpm install OK"
run = "pnpm install --silent"

[tasks.e2e-app]
description = "Browser e2e: the DeckBridge Web UI in mock mode"
quiet = true
dir = "e2e"
depends = ["build-mock", "e2e-install"]
run = "pnpm run test:app"
run = "pnpm --silent run test:app"
timeout = "180s"

[tasks.e2e-docs]
description = "Browser e2e: the built Docusaurus site (run `mise run docs-build` first)"
quiet = true
dir = "e2e"
depends = ["e2e-install"]
# wait_for, not `depends`: standalone this task must fail in seconds with "run mise run
# docs-build first" instead of silently triggering a multi-minute Docusaurus build. Within
# beforeCommit, docs-check produces docs-site/build/ and wait_for stops the two racing.
wait_for = ["docs-check"]
run = "pnpm run test:docs"
run = "pnpm --silent run test:docs"
timeout = "180s"

[tasks.e2e-browser]
alias = ["e2e-all"]
description = "All browser e2e suites (app + docs)"
quiet = true
depends = ["e2e-app", "e2e-docs"]

[tasks.e2e-types]
description = "Type-check the e2e suite"
quiet = true
dir = "e2e"
depends = ["e2e-install"]
run = "pnpm run types"
run = "node ../scripts/quiet.mjs --ok=e2e-types -- pnpm --silent run types"

[tasks.test]
description = "Run tests (TJS)"
Expand All @@ -400,15 +428,17 @@ timeout = "240s"

[tasks.test-client]
description = "Run Preact browser regression tests"
quiet = true
dir = "ts"
depends = ["ts-install"]
run = "pnpm run test:client"
run = "node ../scripts/quiet.mjs --fail-only --ok=test-client -- pnpm --silent run test:client"
# The script's own browser budget is 90s; keep this clear of it so a browser timeout
# fails with the script's diagnostic (Chrome path + stderr) instead of a bare task kill.
timeout = "150s"

[tasks.docs-install]
description = "Install docs-site pnpm dependencies"
quiet = true
dir = "docs-site"
run = "pnpm install"

Expand All @@ -422,9 +452,10 @@ run = "pnpm exec docusaurus start --port 3001 --host 0.0.0.0"

[tasks.docs-check]
description = "docs-site: typecheck + tests (build/links/content/output)"
quiet = true
dir = "docs-site"
depends = ["docs-install"]
run = "pnpm run beforeCommit"
run = "node ../scripts/quiet.mjs --fail-only --ok=docs-check -- pnpm run beforeCommit"

[tasks.docs-build]
description = "Build Docusaurus static HTML (docs-site/build/)"
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-deckbridge-native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// rust/deckbridge-native (mise task `dir`).
import { execFileSync } from 'node:child_process';

const args = ['build', '--release'];
const args = ['build', '-q', '--release'];
if (process.env.JPEG_FORK === '1') {
args.push('--no-default-features', '--features', 'jpeg-fork,usb');
}
Expand Down
30 changes: 30 additions & 0 deletions scripts/quiet.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env node
// Run a command silently: print its output only on failure or when it carries real
// diagnostics (warnings), plus an optional one-line success message.
// Usage: node scripts/quiet.mjs [--ok=MSG] [--fail-only] -- cmd args...
import { spawnSync } from 'node:child_process';

const argv = process.argv.slice(2);
const sep = argv.indexOf('--');
const flags = argv.slice(0, sep);
const [cmd, ...args] = argv.slice(sep + 1);
const okMsg = flags.find((f) => f.startsWith('--ok='))?.slice('--ok='.length);
// --fail-only: cargo-style tools whose success output is all progress chatter and whose
// warnings are already promoted to errors (-D warnings).
const failOnly = flags.includes('--fail-only');

const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
const out = (r.stdout ?? '') + (r.stderr ?? '');
// Success summaries of oxlint/oxfmt carry no information when clean.
const NOISE = /^(Found 0 warnings and 0 errors\.|Finished in .*|\s*)$/;
const residue = out.split('\n').some((line) => !NOISE.test(line));

if (r.status !== 0 || (!failOnly && residue)) {
process.stdout.write(out);
if (r.status !== 0) {
console.log(`✘ ${cmd} exit ${r.status ?? `signal ${r.signal}`}`);
process.exit(r.status ?? 1);
}
} else if (okMsg) {
console.log(`✔ ${okMsg}`);
}
16 changes: 12 additions & 4 deletions scripts/run-tests.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env node
// Run the txiki.js test suite. For each ts/test/*.test.ts: bundle it, then run it
// under $TJS. Prints one line per test file (relative to project root). On a build
// or run failure (including SIGSEGV), prints the captured output and exit status.
// Continues past failures; exits non-zero if any test failed.
// under $TJS. Silent on success except one summary line (VERBOSE=1 prints a line per
// passing file). On a build or run failure (including SIGSEGV), prints the captured
// output and exit status. Continues past failures; exits non-zero if any test failed.
//
// Env (provided by mise): TJS, DECKBRIDGE_NATIVE_LIB. Invoked by [tasks.test] in mise.toml.
import { spawnSync } from 'node:child_process';
Expand Down Expand Up @@ -30,7 +30,9 @@ const files = readdirSync(join(tsDir, 'test'))
.filter((f) => process.env.CI || !CI_ONLY_TESTS.has(f))
.sort();

const verbose = Boolean(process.env.VERBOSE);
let rc = 0;
let failed = 0;
for (const file of files) {
const startedAt = process.hrtime.bigint();
const name = file.slice(0, -'.test.ts'.length);
Expand All @@ -46,6 +48,7 @@ for (const file of files) {
process.stdout.write((build.stdout ?? '') + (build.stderr ?? ''));
console.log(` build exit ${build.status ?? `signal ${build.signal}`}`);
rc = 1;
failed++;
continue;
}

Expand All @@ -56,10 +59,15 @@ for (const file of files) {
// status is null when the process is killed by a signal (e.g. SIGSEGV).
console.log(` exit ${run.status ?? `signal ${run.signal}`}`);
rc = 1;
failed++;
continue;
}

console.log(`✔ ${label}${duration()}`);
if (verbose) console.log(`✔ ${label}${duration()}`);
}

console.log(
failed ? `✘ tests: ${failed} of ${files.length} files failed` : `✔ tests (${files.length} files)`,
);

process.exit(rc);
1 change: 0 additions & 1 deletion scripts/tjs-common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export function requireTjsEnv() {
}

if (existsSync(TJS)) {
console.log(`tjs already present: ${TJS}`);
process.exit(0);
}

Expand Down
Loading