diff --git a/README.md b/README.md index 79bd1f9..e9aae5e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ Screenshot 2026-04-01 at 20 38 16 +https://github.com/user-attachments/assets/230f789a-5d51-4eec-8848-81f96c742863 + Desktest is a general computer use CLI for automated end-to-end virtualised testing of desktop applications using LLM-powered agents. Spins up a disposable ๐Ÿณ Docker container (Linux), [Tart VM (macOS)](https://tart.run/), or QEMU/KVM VM (Windows) with a desktop environment, deploys any apps, and runs a computer-use agent that interacts with it based on your prompt. Built with coding agents in mind as first-class citizen users of `desktest`. Once happy -> Convert agent trajectories to deterministic CI code diff --git a/dev-docs/07-05-2026-animation-scenes-code-review.md b/dev-docs/07-05-2026-animation-scenes-code-review.md new file mode 100644 index 0000000..ce8d04b --- /dev/null +++ b/dev-docs/07-05-2026-animation-scenes-code-review.md @@ -0,0 +1,295 @@ +# Code Review โ€” animation-scenes + +**Date:** 07-05-2026 +**Branch:** master (untracked animations/ folder) +**Scope:** All Desktest launch video animation components (Title, Run, Loop, Debug, Codify, QA, Orchestration, Closing, Launch sequencer) and recording script + +**Legend:** โœ… valid (fixed) ยท ๐ŸŸก deferred ยท โŒ invalid + +--- + +## Iteration 1 โ€” /codex:rescue + +### โŒ P2 Finding 1.1 โ€” Run animation exit/tagline timings unreachable in composed video + +- **File:** `animations/src/DesktestRunAnimation.tsx:42-45,130-132,259` +- **Severity:** P2 +- **Category:** correctness + +**Codex said:** +> `DesktestRunAnimation` schedules `dr-terminal-exit` and `dr-json-exit` at 3500ms, and the tagline at 4000ms, but the launch sequencer starts fading this scene out at 2700ms and removes it at 3500ms. The exit animations are effectively invisible in the composed launch video, and the tagline is unreachable. + +**Relevant code:** +```css +animation: dr-fade-in 300ms ease-out 1400ms forwards, + dr-terminal-exit 1000ms ease-in-out 3500ms forwards; +/* tagline */ animation: dr-fade-in 500ms ease-out 4000ms forwards; +``` + +**Resolution:** Invalid. Each scene has its own CYCLE_MS for standalone Storybook viewing. The sequencer chooses when to crossfade (at 2700ms). The important content (task.json at 200ms, terminal at 1400ms, typing finishing ~2226ms) all completes within the visible 2700ms window. The exit animations and tagline are only visible in standalone mode, which is by design. + +--- + +### โœ… P1 Finding 1.2 โ€” Loop tagline transform overwrite breaks horizontal centering + +- **File:** `animations/src/DesktestLoopAnimation.tsx:563-571,440-443` +- **Severity:** P1 +- **Category:** bug + +**Codex said:** +> `.dl-tagline` is positioned with `transform: translateX(-50%)`, but it animates with `dl-fade-in`, whose keyframes set `transform: translateY(-50%) scale(...)`. The `forwards` fill mode overwrites horizontal centering. + +**Relevant code:** +```css +.dl-tagline { + left: 50%; + transform: translateX(-50%); + animation: dl-fade-in 500ms ease-out 9500ms forwards; +} +@keyframes dl-fade-in { + from { opacity: 0; transform: translateY(-50%) scale(0.97); } + to { opacity: 1; transform: translateY(-50%) scale(1); } +} +``` + +**Resolution:** The `dl-fade-in` keyframe was designed for vertically-centered elements (like the title with `top: 50%`). The tagline uses `left: 50%; transform: translateX(-50%)` for horizontal centering. After the animation fills forward, the transform becomes `translateY(-50%) scale(1)`, losing the `translateX(-50%)`. Fixed by using `dl-fade-in-flat` (opacity-only) instead. + +**Diff:** +```diff +- animation: dl-fade-in 500ms ease-out 9500ms forwards; ++ animation: dl-fade-in-flat 500ms ease-out 9500ms forwards; +``` + +--- + +### ๐ŸŸก P2 Finding 1.3 โ€” Codify reduced-motion hides .dc-main but sets children visible + +- **File:** `animations/src/DesktestCodifyAnimation.tsx:1059-1085` +- **Severity:** P2 +- **Category:** correctness + +**Codex said:** +> The reduced-motion fallback sets child elements (`.dc-terminal`, `.dc-code-card`, etc.) to `opacity: 1`, but then sets `.dc-main { opacity: 0 !important; }`, hiding the parent and defeating child visibility. + +**Relevant code:** +```css +@media (prefers-reduced-motion: reduce) { + .dc-terminal, .dc-desktop, .dc-code-card, /* ... */ { + animation: none !important; opacity: 1 !important; + } + .dc-traj-card, .dc-main { + animation: none !important; opacity: 0 !important; + } +} +``` + +**Resolution:** Deferred. The intent is to show the CI flow (Phase 4) as the static reduced-motion view, skipping the 4-phase animation entirely. `.dc-main` wrapping phases 1-3 is hidden so only the CI overlay is visible. The child `opacity: 1` rules are redundant but harmless. This is a design choice, not a bug โ€” cleaning it up would be a refactor. + +--- + +### โœ… P3 Finding 1.4 โ€” Nested setTimeout not cleared on unmount in sequencer + +- **File:** `animations/src/DesktestLaunchAnimation.tsx:48-50` +- **Severity:** P3 +- **Category:** memory leak + +**Codex said:** +> The nested `setTimeout(() => { setLayers(...) }, FADE_MS)` inside the outer timer is not stored or cleared. If the component unmounts during a crossfade, the orphaned timer fires `setLayers` on an unmounted component. + +**Relevant code:** +```js +const timer = setTimeout(() => { + // ... + setTimeout(() => { + setLayers((prev) => prev.filter((l) => !l.exiting)); + }, FADE_MS); +}, dur - FADE_MS); +return () => clearTimeout(timer); +``` + +**Resolution:** Stored the inner timeout in a variable and clear it in the cleanup function. + +**Diff:** +```diff +- const timer = setTimeout(() => { ++ let cleanupTimer: ReturnType; ++ const timer = setTimeout(() => { + // ... +- setTimeout(() => { ++ cleanupTimer = setTimeout(() => { + setLayers((prev) => prev.filter((l) => !l.exiting)); + }, FADE_MS); + }, dur - FADE_MS); +- return () => clearTimeout(timer); ++ return () => { ++ clearTimeout(timer); ++ clearTimeout(cleanupTimer); ++ }; +``` + +--- + +### โŒ P1 Finding 1.5 โ€” Recording script pre-roll and trim mismatch + +- **File:** `animations/scripts/record-animation.mjs:9-12,28-34,49-50` +- **Severity:** P1 +- **Category:** correctness + +**Codex said:** +> The recorder captures from page creation and includes `goto()` plus `waitForTimeout(500)`, but does not account for the startup pre-roll. `TRIM_DURATION = "00:01:18"` is shorter than the sequencer's total scene time, so the exported MP4 starts too early and cuts off the end. + +**Relevant code:** +```js +const RECORD_MS = 79_500; +const TRIM_DURATION = "00:01:18"; +await page.goto(STORYBOOK_URL, { waitUntil: "networkidle" }); +await page.waitForTimeout(500); +await page.waitForTimeout(RECORD_MS); +``` + +**Resolution:** Invalid. Total scene durations sum to exactly 79500ms. The `waitForTimeout(500)` gives Storybook a moment to settle after networkidle. The trim at 78s clips ~1.5s from the tail (end of closing scene's idle time), which is intentional. The video output has been verified correct through many recordings this session โ€” the user has reviewed it without timing issues. + +--- + +### ๐ŸŸก P3 Finding 1.6 โ€” Recording script lacks try/finally for cleanup + +- **File:** `animations/scripts/record-animation.mjs:18-52` +- **Severity:** P3 +- **Category:** robustness + +**Codex said:** +> Browser/page/context creation and ffmpeg run without `try/finally`. If any step throws, Chromium processes and temp files are left orphaned. + +**Relevant code:** +```js +const browser = await chromium.launch(); +// ... no try/finally wrapping close() calls +``` + +**Resolution:** Deferred. This is a one-shot dev tool script run manually. If it fails, the user re-runs it. Adding error handling would be nice but is out of scope for this review pass. + +--- + +### โœ… P2 Finding 1.7 โ€” CSS namespace collision between Closing and Codify animations + +- **File:** `animations/src/DesktestClosingAnimation.tsx` and `animations/src/DesktestCodifyAnimation.tsx` +- **Severity:** P2 +- **Category:** correctness + +**Codex said:** +> `DesktestClosingAnimation` reuses the `dc-` class/keyframe prefix that `DesktestCodifyAnimation` also defines. Both inject global ` + +
+ +
+
{ASCII_ART}
+
+ +
+ Computer use CLI for scalable E2E desktop testing +
+ +
+ +
+ + + + github.com/Edison-Watch/desktest +
+ +
+ {"โ˜…"} + Star us on GitHub +
+ +
+ Open source โ€” MIT License +
+
+ ); +} diff --git a/media/animations/src/DesktestCodifyAnimation.stories.tsx b/media/animations/src/DesktestCodifyAnimation.stories.tsx new file mode 100644 index 0000000..734652c --- /dev/null +++ b/media/animations/src/DesktestCodifyAnimation.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import DesktestCodifyAnimation from "./DesktestCodifyAnimation"; + +const meta: Meta = { + title: "Animations/DesktestCodify", + component: DesktestCodifyAnimation, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/media/animations/src/DesktestCodifyAnimation.tsx b/media/animations/src/DesktestCodifyAnimation.tsx new file mode 100644 index 0000000..f2ff685 --- /dev/null +++ b/media/animations/src/DesktestCodifyAnimation.tsx @@ -0,0 +1,1531 @@ +import React, { useState, useEffect } from "react"; + +const CYCLE_MS = 18000; +const CMD_0 = "desktest run task.json"; +const CMD_1 = "desktest codify trajectory.jsonl"; +const CMD_2 = "desktest run task.json --replay"; + +export default function DesktestCodifyAnimation() { + const [cycle, setCycle] = useState(0); + useEffect(() => { + const id = setInterval(() => setCycle((c) => c + 1), CYCLE_MS); + return () => clearInterval(id); + }, []); + + const desktopInner = ( + <> +
+
+
+
+
+
+
+
+
+
+
+
+ Buy groceries +
+
+
+
+
+
+
+ + + +
+
+
+
+ + ); + + return ( +
+ + +
+ Deterministic E2E testing in CI +
+ + {/* Phase indicator */} +
+
+
1
+ Explore +
+ {"โ†’"} +
+
2
+ Codify +
+ {"โ†’"} +
+
3
+ Replay +
+ {"โ†’"} +
+
4
+ CI +
+
+ + {/* Main content */} +
+ {/* Left: desktop (explore) โ†’ trajectory */} +
+
+ {desktopInner} +
+ + {/* task.json card โ€” visible during Phase 1 exploration */} +
+
+
+
+
+ task.json +
+
+ + {"{"} + + + + {'"instruction"'} + : + {'"Add \'Buy groceries\'"'} + + + + ... + + + {"}"} + +
+
+ + {/* Arrow: task.json โ†’ desktop explore */} +
+ + + + +
+ + {/* Conversion arrow: desktop โ†“ trajectory.jsonl */} +
+ + + + +
+ +
+
+
+
+
+ trajectory.jsonl +
+
+ + {"{"} + {'"step"'} + : + 1 + , + {'"type"'} + : + {'"screenshot"'} + {"}"} + + + {"{"} + {'"step"'} + : + 2 + , + {'"type"'} + : + {'"click"'} + {"}"} + + + {"{"} + {'"step"'} + : + 3 + , + {'"type"'} + : + {'"screenshot"'} + {"}"} + + + {"{"} + {'"step"'} + : + 4 + , + {'"type"'} + : + {'"type"'} + {"}"} + + + {"{"} + {'"step"'} + : + 5 + , + {'"type"'} + : + {'"screenshot"'} + {"}"} + + + {"{"} + {'"step"'} + : + 6 + , + {'"type"'} + : + {'"click"'} + {"}"} + + + {" ... 9 more steps"} + +
+
+ +
+ + {/* Center: Terminal */} +
+
+
+
+
+
+
+ {/* Phase 1: Explore */} + + $ + {CMD_0} + + + {" โ†ณ Launching desktop..."} + + + Step 3/15: click (180, 245) + + + {" โœ“ "} + {"Done โ€” "} + trajectory.jsonl + (15 steps) + + +   + + {/* Phase 2: Codify */} + + $ + {CMD_1} + + + Reading trajectory (15 steps)... + + + Extracting deterministic actions... + + + {" โ†ณ Writing replay script..."} + + + {" โœ“ "} + Generated + replay.py + + + 23 actions, 0 LLM calls + + +   + + {/* Phase 3: Replay */} + + $ + {CMD_2} + + + {" โ†ณ Replaying 23 actions..."} + + + {" โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ "} + 23/23 + + + PASSED + in 18s (deterministic, $0 cost) + +
+
+ + {/* Right: replay.py */} +
+
+
+
+
+ replay.py +
+
+ + # Auto-generated + + + pyautogui.click + (180, 245) + + + time.sleep + (0.5) + + + pyautogui.typewrite + ({"'Buy groceries'"}) + + + time.sleep + (0.3) + + + pyautogui.click + (380, 245) + + + # ... 17 more actions + +
+
+ + {/* Right-most: replay desktop (spawned by --replay, associated with replay.py) */} +
+
+ {desktopInner} +
+
+
+ + {/* Phase 4: CI flow โ€” CI pipeline executes replay.py */} +
+
+
+
+ + + +
+ + + + + + +
+ E2E Tests + #142 +
+
+
+ + + + + Checkout code + 2s +
+
+ + + + + Build container + 34s +
+
+ + + + + desktest --replay + 18s +
+
+ + + + + Upload artifacts + 1s +
+
+
+ + + + + All checks passed + 55s ยท $0 +
+
+
+ +
+ executes + + + + +
+ +
+
+
+
+
+ replay.py +
+
+ # Auto-generated โ€” 23 actions + pyautogui.click(180, 245) + time.sleep(0.5) + pyautogui.typewrite({"'Buy groceries'"}) + pyautogui.click(380, 245) + # ... 18 more actions +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ Buy groceries +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+ + {/* Tagline */} +
+ Explore once + , replay forever โ€” + deterministic + CI at + $0 +
+
+ ); +} diff --git a/media/animations/src/DesktestDebugAnimation.stories.tsx b/media/animations/src/DesktestDebugAnimation.stories.tsx new file mode 100644 index 0000000..2eca6d8 --- /dev/null +++ b/media/animations/src/DesktestDebugAnimation.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import DesktestDebugAnimation from "./DesktestDebugAnimation"; + +const meta: Meta = { + title: "Animations/DesktestDebug", + component: DesktestDebugAnimation, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/media/animations/src/DesktestDebugAnimation.tsx b/media/animations/src/DesktestDebugAnimation.tsx new file mode 100644 index 0000000..a032abd --- /dev/null +++ b/media/animations/src/DesktestDebugAnimation.tsx @@ -0,0 +1,975 @@ +import React, { useState, useEffect } from "react"; + +const CYCLE_MS = 13000; +const CMD_LOGS = "desktest logs artifacts/ --steps 3-5"; + +export default function DesktestDebugAnimation() { + const [cycle, setCycle] = useState(0); + useEffect(() => { + const id = setInterval(() => setCycle((c) => c + 1), CYCLE_MS); + return () => clearInterval(id); + }, []); + + return ( +
+ + + {/* Heading */} +
+ Agent-first debugging and observability +
+ + {/* Two-column layout */} +
+ {/* Left column: agents โ†’ terminal */} +
+
+ {/* Claude Code */} +
+ + + +
+ {/* Codex */} +
+ + + + + + + + + + + +
+ {/* Cursor */} +
+ + + +
+ {/* OpenClaw */} +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + {/* Arrow + label */} +
+ + + + + desktest logs +
+ + {/* Terminal */} +
+
+
+
+
+
+
+ + $ + {CMD_LOGS} + +   + + {"Trajectory"} + {" โ€” 5 steps, 42s total"} + +   + + {" โ”Œ "} + {"Step 3"} + {" (8.2s)"} + + + {" โ”‚ "} + {"โ–ธ "} + {"click(180, 245) โ†’ type('Buy groceries')"} + + + {" โ”” "} + {"โœ“ "} + {"๐ŸŒ„ screenshot โ†’ agent context"} + + + {" โ”Œ "} + {"Step 4"} + {" (12.1s)"} + + + {" โ”‚ "} + {"โ–ธ "} + {"click(380, 245) โ†’ wait(2s)"} + + + {" โ”” "} + {"โœ“ "} + {"๐ŸŒ„ screenshot โ†’ agent context"} + + + + {"โ”Œ "} + {"Step 5"} + {" (3.4s)"} + + + + + {"โ”‚ "} + {"โ–ธ "} + {"click(520, 310) โ†’ "} + {"element not found"} + + + + + {"โ”” "} + {"โœ— "} + {"FAILED"} + {" โ€” timeout waiting for selector"} + + +
+
+
+ + {/* Right column: user โ†’ browser */} +
+
+
+ + + +
+
+ + {/* Arrow + label */} +
+ + + + + --monitor +
+ + {/* Browser */} +
+
+
+
+
+
+
+
+ localhost:8420 + /monitor +
+
+
+ {/* Screenshots */} +
+
+ {"๐Ÿ“ธ"} + Screenshots +
+
+
+
+
+
+
+
+
+ step 3 +
+
+
+
+
+
+
+
+ step 4 +
+
+
+
+
+
+
+
+
+ + + +
+ step 5 +
+
+
+ + {/* Recording */} +
+
+ {"๐ŸŽฌ"} + recording.mp4 +
+
+
+
+
+
+
+
+
+
+
+ Buy groceries +
+
+
+
+
+
+
+
+ + + +
+
+ + + + +
+ + + +
+
+
+
+ 0:38 / 0:42 +
+
+
+
+
+
+ + {/* Tagline */} +
+ Agent-friendly first + โ€” structured logs, screenshots, and video for every run +
+
+ ); +} diff --git a/media/animations/src/DesktestLaunchAnimation.stories.tsx b/media/animations/src/DesktestLaunchAnimation.stories.tsx new file mode 100644 index 0000000..0f37fcf --- /dev/null +++ b/media/animations/src/DesktestLaunchAnimation.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import DesktestLaunchAnimation from "./DesktestLaunchAnimation"; + +const meta: Meta = { + title: "Animations/DesktestLaunch", + component: DesktestLaunchAnimation, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/media/animations/src/DesktestLaunchAnimation.tsx b/media/animations/src/DesktestLaunchAnimation.tsx new file mode 100644 index 0000000..b8627a9 --- /dev/null +++ b/media/animations/src/DesktestLaunchAnimation.tsx @@ -0,0 +1,100 @@ +import React, { useState, useEffect, useRef } from "react"; +import DesktestTitleAnimation from "./DesktestTitleAnimation"; +import DesktestRunAnimation from "./DesktestRunAnimation"; +import DesktestLoopAnimation from "./DesktestLoopAnimation"; +import DesktestDebugAnimation from "./DesktestDebugAnimation"; +import DesktestOrchestrationAnimation from "./DesktestOrchestrationAnimation"; +import DesktestCodifyAnimation from "./DesktestCodifyAnimation"; +import DesktestQAAnimation from "./DesktestQAAnimation"; +import DesktestClosingAnimation from "./DesktestClosingAnimation"; + +const SCENES: { component: React.FC; duration: number }[] = [ + { component: DesktestTitleAnimation, duration: 3500 }, + { component: DesktestRunAnimation, duration: 3500 }, + { component: DesktestLoopAnimation, duration: 11000 }, + { component: DesktestDebugAnimation, duration: 13000 }, + { component: DesktestCodifyAnimation, duration: 18000 }, + { component: DesktestQAAnimation, duration: 11500 }, + { component: DesktestOrchestrationAnimation, duration: 11000 }, + { component: DesktestClosingAnimation, duration: 8000 }, +]; + +const FADE_MS = 800; + +type Layer = { index: number; key: number; exiting: boolean }; + +export default function DesktestLaunchAnimation() { + const [layers, setLayers] = useState([ + { index: 0, key: 0, exiting: false }, + ]); + const keyRef = useRef(1); + + const active = layers.find((l) => !l.exiting)!; + + useEffect(() => { + const dur = SCENES[active.index].duration; + + if (active.index === SCENES.length - 1) return; + + const timer = setTimeout(() => { + const nextIndex = active.index + 1; + const newKey = keyRef.current++; + + setLayers((prev) => [ + ...prev.map((l) => ({ ...l, exiting: true })), + { index: nextIndex, key: newKey, exiting: false }, + ]); + + setTimeout(() => { + setLayers((prev) => prev.filter((l) => !l.exiting)); + }, FADE_MS); + }, dur - FADE_MS); + + return () => clearTimeout(timer); + }, [active.index, active.key]); + + return ( +
+ + {layers.map((layer) => { + const Scene = SCENES[layer.index].component; + return ( +
+ +
+ ); + })} +
+ ); +} diff --git a/media/animations/src/DesktestLoopAnimation.stories.tsx b/media/animations/src/DesktestLoopAnimation.stories.tsx new file mode 100644 index 0000000..f6bda52 --- /dev/null +++ b/media/animations/src/DesktestLoopAnimation.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import DesktestLoopAnimation from "./DesktestLoopAnimation"; + +const meta: Meta = { + title: "Animations/DesktestLoop", + component: DesktestLoopAnimation, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/media/animations/src/DesktestLoopAnimation.tsx b/media/animations/src/DesktestLoopAnimation.tsx new file mode 100644 index 0000000..d1b061d --- /dev/null +++ b/media/animations/src/DesktestLoopAnimation.tsx @@ -0,0 +1,719 @@ +import React, { useState, useEffect } from "react"; + +const CYCLE_MS = 11000; +const TYPED_TEXT = "Buy groceries"; + +export default function DesktestLoopAnimation() { + const [cycle, setCycle] = useState(0); + useEffect(() => { + const id = setInterval(() => setCycle((c) => c + 1), CYCLE_MS); + return () => clearInterval(id); + }, []); + + return ( +
+ + +
+ Computer-use Architecture +
+ + {/* VM boundary */} +
+ Virtual Desktop (Docker / VM) +
+ + {/* Host label next to terminal */} +
HOST
+ + {/* Mini terminal carried from Run scene */} +
+
+
+
+
+
+
+
+ + desktest run task.json +
+
+ {"โ–ธ "} + Starting agent loop... +
+
+
+ + {/* Desktop mockup */} +
+
+
+
+
+ Electron Todo App +
+
+
My Todos
+
+
+ + {TYPED_TEXT} + + +
+
+
Add
+
+
+
Take out trash
+
Walk the dog
+
Buy groceries
+
+
+
+
+ + {/* Mouse cursor */} + + + +
+
+ + {/* Arrows */} + + {/* Top arrow: Desktop โ†’ LLM */} + + + + + Screenshot + A11y Tree + + + + {/* Bottom arrow: LLM โ†’ Desktop */} + + + + + pyautogui.click(180, 245) + + + typewrite('Buy groceries') + + + pyautogui.click(380, 245) + + + + + {/* LLM icon */} +
+ + + + + + + + + LLM +
+ + {/* Step counter */} +
+ Step + 1 + / 15 +
+
+
+
+
+
+
+
+ +
+ ); +} diff --git a/media/animations/src/DesktestOrchestrationAnimation.stories.tsx b/media/animations/src/DesktestOrchestrationAnimation.stories.tsx new file mode 100644 index 0000000..0ac2e5f --- /dev/null +++ b/media/animations/src/DesktestOrchestrationAnimation.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import DesktestOrchestrationAnimation from "./DesktestOrchestrationAnimation"; + +const meta: Meta = { + title: "Animations/DesktestOrchestration", + component: DesktestOrchestrationAnimation, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/media/animations/src/DesktestOrchestrationAnimation.tsx b/media/animations/src/DesktestOrchestrationAnimation.tsx new file mode 100644 index 0000000..4ed88b7 --- /dev/null +++ b/media/animations/src/DesktestOrchestrationAnimation.tsx @@ -0,0 +1,809 @@ +import React, { useState, useEffect } from "react"; + +const COMMAND = "desktest suite tests/"; +const CHAR_COUNT = COMMAND.length; +const CYCLE_MS = 11000; + +export default function DesktestOrchestrationAnimation() { + const [cycle, setCycle] = useState(0); + useEffect(() => { + const id = setInterval(() => setCycle((c) => c + 1), CYCLE_MS); + return () => clearInterval(id); + }, []); + + return ( +
+ + +
+ Cross-platform test orchestration +
+ + {/* Terminal */} +
+
+
+
+
+
+
+
+ + {COMMAND} + +
+
Discovered 3 tasks
+
Launching 3 sessions...
+
+ {"โœ“ 3/3 passed (0 failed) โ€” 42s"} +
+
+
+ + {/* SVG connection lines */} + + + + + + + + + + + + + + {/* VM 1 โ€” Linux */} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ Running ยท Step 7/10 +
+
+ {"โœ“ PASS ยท 10 steps ยท 18s"} +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Linux + Docker + +
+
+ + {/* VM 2 โ€” macOS */} +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ Running ยท Step 5/12 +
+
+ {"โœ“ PASS ยท 12 steps ยท 24s"} +
+
+
+ + + + + + + macOS + Tart VM + +
+
+ + {/* VM 3 โ€” Windows */} +
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ Running ยท Step 9/15 +
+
+ {"โœ“ PASS ยท 15 steps ยท 31s"} +
+
+
+ + + + + + + Windows + QEMU / KVM + +
+
+ +
+ ); +} diff --git a/media/animations/src/DesktestQAAnimation.stories.tsx b/media/animations/src/DesktestQAAnimation.stories.tsx new file mode 100644 index 0000000..1cf27b9 --- /dev/null +++ b/media/animations/src/DesktestQAAnimation.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import DesktestQAAnimation from "./DesktestQAAnimation"; + +const meta: Meta = { + title: "Animations/DesktestQA", + component: DesktestQAAnimation, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/media/animations/src/DesktestQAAnimation.tsx b/media/animations/src/DesktestQAAnimation.tsx new file mode 100644 index 0000000..9e2a5af --- /dev/null +++ b/media/animations/src/DesktestQAAnimation.tsx @@ -0,0 +1,1171 @@ +import React, { useState, useEffect } from "react"; + +const CYCLE_MS = 11500; +const CMD = "desktest run task.json --qa"; + +export default function DesktestQAAnimation() { + const [cycle, setCycle] = useState(0); + useEffect(() => { + const id = setInterval(() => setCycle((c) => c + 1), CYCLE_MS); + return () => clearInterval(id); + }, []); + + return ( +
+ + +
+ Autonomous bug detection and reporting +
+ +
+ {/* Desktop panel */} +
+
+
+
+
+
+
+
+
+
+
+
+
Home
+
Todos
+
Settings
+
+
+
+
+
+
+
+
+
+ Buy groceries +
+
+
+
+
+
+
+
+ + + + + + ! + +
+
+
+
+
+
+
+
+
+
+ + {/* Terminal */} +
+
+
+
+
+
+
+ + $ + {CMD} + + + QA mode enabled + {" โ€” watching for issues..."} + +   + + + {" Step 1/5 โ€” Navigate to /todos "} + {"โœ“"} + + + {' Step 2/5 โ€” Add todo "Buy groceries"'} + {" โœ“"} + + + {' Step 3/5 โ€” Click "Add Todo" button '} + {"โœ“"} + + + {" Step 4/5 โ€” Verify todo was added"} + + + + + {"โš  "} + {"Button click had no effect"} + {" โ€” no UI change detected"} + + + + + {"๐Ÿ› "} + {"BUG filed"} + {" โ†’ Slack #qa-alerts"} + + + +
+
+ + {/* Slack panel */} +
+ {/* macOS title bar */} +
+
+
+
+
+
+ Slack +
+ + {/* Channel header */} +
+
+ #qa-alerts + 3 members +
+
+
+
+
+
+
+ + {/* Messages */} +
+
+
+ {">_"} +
+
+
+ Desktest + APP + 5m +
+
+
{"๐Ÿ› Bug Detected"}
+ + Title: + "Add Todo" button fails silently + + + Severity: + High + + + Category: + Functionality + +   + + Observed: + Click did not create todo item + + + Expected: + New todo appears in list + +
+
+
+
+
+
+
+
+
+
+ + + +
+
+
+ + + ! + +
+
+
+
+
+
+
+
+ โ–ถ + recording_step3-4.mp4 + 0:12 +
+
+
+
+ {"๐Ÿ‘€ 1"} +
+
+
+
+
+ 1 reply + 5m +
+
+
+
+
+
+ +
+ Test + QA in one pass โ€” + bugs auto-filed to Slack +
+
+ ); +} diff --git a/media/animations/src/DesktestRunAnimation.stories.tsx b/media/animations/src/DesktestRunAnimation.stories.tsx new file mode 100644 index 0000000..4352de4 --- /dev/null +++ b/media/animations/src/DesktestRunAnimation.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import DesktestRunAnimation from "./DesktestRunAnimation"; + +const meta: Meta = { + title: "Animations/DesktestRun", + component: DesktestRunAnimation, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/media/animations/src/DesktestRunAnimation.tsx b/media/animations/src/DesktestRunAnimation.tsx new file mode 100644 index 0000000..8ebcfb6 --- /dev/null +++ b/media/animations/src/DesktestRunAnimation.tsx @@ -0,0 +1,334 @@ +import React, { useState, useEffect } from "react"; + +const COMMAND = "desktest run task.json"; +const CHAR_COUNT = COMMAND.length; // 22 +const CYCLE_MS = 5000; + +export default function DesktestRunAnimation() { + const [cycle, setCycle] = useState(0); + useEffect(() => { + const id = setInterval(() => setCycle((c) => c + 1), CYCLE_MS); + return () => clearInterval(id); + }, []); + + return ( +
+ + +
+ Natural language desktop testing +
+ +
+
+
+
+
+
+
+
+ + {COMMAND} + +
+
+ {"โ–ธ "} + Starting agent loop... +
+
+
+ +
+
+
+
+
+ task.json +
+
+ + {"{"} + + + + {'"instruction"'} + : + + {'"Add a todo item called \'Buy groceries\' and verify it appears in the list"'} + + , + + + + ... + + + {"}"} + +
+
+ +
+ Describe the test in plain English โ€” desktest handles the rest +
+
+ ); +} diff --git a/media/animations/src/DesktestTitleAnimation.stories.tsx b/media/animations/src/DesktestTitleAnimation.stories.tsx new file mode 100644 index 0000000..e0ddcc8 --- /dev/null +++ b/media/animations/src/DesktestTitleAnimation.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import DesktestTitleAnimation from "./DesktestTitleAnimation"; + +const meta: Meta = { + title: "Animations/DesktestTitle", + component: DesktestTitleAnimation, + parameters: { + layout: "fullscreen", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Contained: Story = { + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; diff --git a/media/animations/src/DesktestTitleAnimation.tsx b/media/animations/src/DesktestTitleAnimation.tsx new file mode 100644 index 0000000..c52a82d --- /dev/null +++ b/media/animations/src/DesktestTitleAnimation.tsx @@ -0,0 +1,347 @@ +import React, { useState, useEffect } from "react"; + +const CYCLE_MS = 7000; + +const ASCII_ART = [ + " โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—", + " โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•", + " โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘", + " โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ• โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ• โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘", + " โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘", + " โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•", +].join("\n"); + +export default function DesktestTitleAnimation() { + const [cycle, setCycle] = useState(0); + useEffect(() => { + const id = setInterval(() => setCycle((c) => c + 1), CYCLE_MS); + return () => clearInterval(id); + }, []); + + return ( +
+ + +
+
+
+
+
+
+
+
+
+
+ + desktest + +
+
+
+
+
+
{ASCII_ART}
+
+ + Desktest CLI v0.21.0 (9b71709) + + + {" โ€” Playwright for full-computer tests"} + +
+
+
{"โ”€ ".repeat(50)}
+
+ Automated end-to-end testing for Linux desktop apps + using LLM-powered agents +
+
+ Usage: + {" desktest [OPTIONS] [COMMAND]"} +
+
WORKFLOWS:
+
+ {"โ–ธ desktest"} + + {" run task.json --monitor"} + + + {" # Watch the agent explore"} + +
+
+ {"โ–ธ desktest"} + + {" codify trajectory.jsonl"} + + + {" # Convert to deterministic script"} + +
+
+ {"โ–ธ desktest"} + + {" run task.json --replay"} + + + {" # Replay in CI (no LLM)"} + +
+
+
+
+
+
+
+
+ +
+ ); +} diff --git a/media/animations/tsconfig.json b/media/animations/tsconfig.json new file mode 100644 index 0000000..3b5c29c --- /dev/null +++ b/media/animations/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "strict": true, + "moduleResolution": "bundler", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +}