Demo — Browser Runtime
@absolutejs/demo is the orchestration layer for enterprise-grade AI demos: drive the product, narrate with AI voiceover, record the screen, and draw presenter-style highlights over the UI. It is intentionally adapter-first — Playwright is excellent for web apps, but real demos often need Discord, Google Meet, native apps, screen switching, and OS-level focus control, so every capability sits behind a stable driver interface.
#Quick Start
Install with bun add @absolutejs/demo, and add optional drivers only when needed (bun add -d playwright). A browser demo is three pieces: a Playwright session (page + video + screenshots), a runner holding the drivers, and a declarative script of steps. The report that comes back carries every artifact and a timestamped event log.
import {
createDemoRunner,
goto,
narrate,
signIn,
spotlight,
writeDemoManifest
} from '@absolutejs/demo';
import { createDemoAuthDriver } from '@absolutejs/demo/auth';
import { createPlaywrightDemoSession } from '@absolutejs/demo/playwright';
const session = await createPlaywrightDemoSession({
headless: false,
recordVideoDir: '.demo-video',
screenshotDir: '.demo-shots'
});
const runner = createDemoRunner({
auth: createDemoAuthDriver(),
browser: session.browserDriver,
annotations: session.annotations,
voiceover: {
speak: async ({ text }) => {
console.log('[voiceover]', text);
}
}
});
const report = await runner.run({
profiles: [
{
id: 'ae',
kind: 'absolute',
baseUrl: 'http://localhost:3000',
email: { env: 'DEMO_EMAIL' },
password: { env: 'DEMO_PASSWORD' },
afterLoginUrl: 'http://localhost:3000/pipeline'
}
],
id: 'crm-demo',
title: 'CRM demo',
steps: [
signIn('ae'),
narrate('Here is the live pipeline view.'),
goto('http://localhost:3000/pipeline'),
spotlight({
selector: "[data-demo='pipeline-total']",
label: 'Revenue at risk',
durationMs: 1800
})
]
});
await writeDemoManifest(report, '.demo-artifacts/crm-demo.manifest.json');
console.log(report.status, report.artifacts);
await session.close();runner.run() never throws for a failed demo — it returns a report with status: 'failed' and the error, after stopping the recorder so the partial recording survives. writeDemoManifest() then turns the report into a proof-of-run JSON document.
#Playwright Session
createPlaywrightDemoSession() (from @absolutejs/demo/playwright) launches the browser, opens a context + page, and hands back the two drivers the runner needs: a DemoBrowserDriver and an annotation driver. Playwright itself is loaded dynamically — it stays an optional dependency.
| Option | Type | Description |
|---|---|---|
headless | boolean | Defaults to false — a demo is meant to be seen (and recorded). |
browser | 'chromium' | 'firefox' | 'webkit' | Engine to launch. Defaults to chromium. |
channel | string | Branded browser channel (e.g. "chrome") instead of the bundled build. |
browserInstance | Browser | Reuse an already-launched Playwright browser; close() then leaves it running. |
contextOptions | Record<string, unknown> | Passed straight to browser.newContext() — viewport, locale, permissions. |
recordVideoDir | string | Enables Playwright video for the context; close() returns it as a recording artifact. |
screenshotDir | string | Where screenshot() steps write their PNG files. |
fullPageScreenshots | boolean | Capture the full scroll height (default true) instead of the viewport. |
account | DemoCredentialProfile | A storage-state profile applied when the context is created — start signed in. |
import { createPlaywrightDemoSession } from '@absolutejs/demo/playwright';
const session = await createPlaywrightDemoSession({
browser: 'chromium', // 'chromium' | 'firefox' | 'webkit'
channel: 'chrome', // branded Chrome instead of the bundled build
headless: false,
recordVideoDir: '.demo-video', // Playwright video for this context
screenshotDir: '.demo-shots',
fullPageScreenshots: true,
contextOptions: { viewport: { width: 1600, height: 900 } },
// A storage-state profile applied when the context is created.
account: {
id: 'returning-user',
kind: 'storage-state',
storageState: '.demo-auth/ae.storage-state.json'
}
});
// The session exposes everything the runner needs:
session.browserDriver; // DemoBrowserDriver — goto/click/fill/type/press
session.annotations; // overlay annotation driver for this page
session.page; // the raw Playwright page for anything bespoke
await session.recordingArtifact(); // video as a recording artifact
await session.close(); // stop recording, close context + browser
// Lower-level pieces are exported too:
import {
createPlaywrightAnnotationDriver,
createPlaywrightDemoBrowser,
launchPlaywright
} from '@absolutejs/demo/playwright';
const browser = await launchPlaywright({ headless: true });
const driver = createPlaywrightDemoBrowser({ page, screenshotDir: 'x' });
const annotations = createPlaywrightAnnotationDriver(page);#Runner & Script Steps
createDemoRunner() holds the drivers; demoScript() declares what happens. Every driver is optional — a script that only narrates needs only a voiceover, and a step whose driver is missing fails with an error naming it.
| Option | Type | Description |
|---|---|---|
browser | DemoBrowserDriver | Executes goto/click/fill/type/press/waitFor/screenshot steps. |
auth | DemoAuthDriver | Resolves signIn("<id>") steps against the script’s profiles. |
annotations | DemoAnnotationDriver | Draws spotlight / circle / highlight overlays. |
desktop | DemoDesktopDriver | Executes openApp / focusApp / hotkey / typeText / click steps. |
recorder | DemoRecorder | Started before step one; stopped (and collected as an artifact) after the last step or on failure. |
voiceover | DemoVoiceover | speak() target for narrate steps; returned artifacts join the report. |
annotationFailure | 'throw' | 'continue' | With "continue", a failed overlay becomes a log artifact and the run keeps going. |
onEvent | (event) => void | Promise<void> | Streams run.started, step.started, step.completed, artifact, run.completed, run.failed. |
voiceoverPlayback | 'continue' | 'wait-for-duration' | Block each narration for its rendered audio duration so live playback stays in sync. |
idFactory | () => string | Custom run ids; defaults to a timestamped random id. |
import { createDemoRunner } from '@absolutejs/demo';
const runner = createDemoRunner({
annotations: session.annotations, // overlay callout driver
auth: createDemoAuthDriver(), // resolves signIn('<id>') steps
browser: session.browserDriver, // goto/click/fill/press/waitFor
desktop: createMacDesktopDriver(), // native-app steps
recorder: screenRecorder, // started before step 1, stopped after
voiceover: elevenLabs, // narrate(...) target
// 'throw' (default) or 'continue' — a failed overlay is logged as
// an artifact and the run keeps going.
annotationFailure: 'continue',
// Stream run/step/artifact events as they happen.
onEvent: (event) => sendToDashboard(event),
// Block each narrate() until the rendered audio duration elapses,
// so on-screen action stays in sync with live playback.
voiceoverPlayback: 'wait-for-duration',
idFactory: () => 'run-' + Date.now().toString(36)
});
const report = await runner.run(script);
// report — the DemoRunReport:
// {
// id, scriptId, startedAt, endedAt, durationMs,
// status: 'completed' | 'failed',
// error?: { message, stack? },
// artifacts: DemoArtifact[], // recordings, voiceovers, screenshots
// events: DemoRunnerEvent[] // run.started, step.started,
// } // step.completed, artifact, run.failedSteps are plain JSON-friendly objects; the builders below construct them. Each accepts a trailing { id?, name? } options object that labels the step in the event log and timeline.
import {
click,
demoScript,
fill,
goto,
markRecording,
narrate,
press,
screenshot,
signIn,
spotlight,
wait,
waitFor
} from '@absolutejs/demo';
const script = demoScript({
id: 'crm-demo',
title: 'CRM demo',
metadata: { audience: 'enterprise', owner: 'sales-eng' },
profiles: [
/* credential profiles — see Authentication */
],
steps: [
signIn('ae'), // run a credential profile by id
goto('http://localhost:3000/pipeline'),
waitFor("[data-demo='pipeline-total']"), // selector or ms
click('#quarter-toggle'),
fill('#search', 'Acme'),
press('#search', 'Enter'),
narrate({
text: 'Here is the live pipeline view.',
emotion: 'confident'
}),
spotlight({
selector: "[data-demo='pipeline-total']",
label: 'Revenue at risk',
durationMs: 1800
}),
markRecording('chapter: pipeline'), // recorder chapter marker
screenshot('pipeline'),
wait(1200),
// Escape hatch — custom logic with the full DemoContext.
{
name: 'custom',
run: async (context) => {
await context.narrate('And one more thing.');
await context.annotate({ type: 'clear' });
context.addArtifact({ id: 'note', kind: 'log' });
}
}
]
});
const report = await runner.run(script);#Authentication
Sign-in is profile-based: declare named credential profiles on the script and trigger them with signIn("<id>") steps. Three profile kinds cover the common cases — your own AbsoluteJS app, any third-party login form, and a saved browser session.
| Capability | absolute | form | storage-state |
|---|---|---|---|
| Works on sites you don’t control | |||
| Drives the real login UI Navigates, types into fields, clicks submit | |||
| Posts to the @absolutejs/auth login route /auth/login by default, or routes.login | |||
| Multi-step flows username → Next → password via a steps array | |||
| Human-style typing typeDelayMs types key-by-key; pressEnter submits | |||
| Success confirmation success.selector and/or success.url | afterLoginUrl | n/a | |
| Reuses a saved Playwright session Applied when the browser context is created | |||
| Secrets resolved from env at sign-in | n/a |
{ env: "VAR_NAME" }) — the runner resolves them at sign-in time, so real secrets never enter the script object, the manifest, or the recording. A missing env var throws an error naming the variable, never its value. The sign-in log artifact records only which profile ran and whether it succeeded.import { createDemoRunner, signIn } from '@absolutejs/demo';
import { createDemoAuthDriver } from '@absolutejs/demo/auth';
const runner = createDemoRunner({
// Default driver — handles all three profile kinds. routes.login
// overrides the '/auth/login' default for absolute profiles.
auth: createDemoAuthDriver({ routes: { login: '/api/auth/login' } }),
browser: session.browserDriver
});
await runner.run({
id: 'signin-demo',
profiles: [
// absolute — a site you own that uses @absolutejs/auth. Posts
// email + password to the auth login route.
{
id: 'ae',
kind: 'absolute',
baseUrl: 'http://localhost:3000',
email: { env: 'DEMO_EMAIL' },
password: { env: 'DEMO_PASSWORD' },
afterLoginUrl: 'http://localhost:3000/pipeline'
},
// storage-state — reuse a saved Playwright session; applied
// when the browser context is created.
{
id: 'returning-user',
kind: 'storage-state',
storageState: '.demo-auth/ae.storage-state.json'
}
],
// Trigger a profile from a step by id:
steps: [signIn('ae')]
});form profiles drive the real login UI of any site, including ones you don't control: navigate to loginUrl, fill fields, click submitSelector, and confirm via a success selector and/or URL. For bespoke auth screens, implement your own DemoAuthDriver.
// A third-party site you do NOT control — drives the real login UI.
{
id: 'saucedemo',
kind: 'form',
loginUrl: 'https://www.saucedemo.com/',
fields: [
{ selector: '#user-name', value: { env: 'SAUCE_USERNAME' } },
{
selector: '#password',
value: { env: 'SAUCE_PASSWORD' },
typeDelayMs: 60 // type key-by-key, like a human
}
],
submitSelector: '#login-button',
// Confirm the login worked — an element that appears and/or a URL.
success: { selector: '.inventory_list' }
}
// Multi-step flows (username → Next → password) use a steps array:
{
id: 'workspace',
kind: 'form',
steps: [
{ action: 'goto', url: 'https://accounts.example.com/signin' },
{
action: 'fill',
selector: '#identifier',
value: { env: 'DEMO_EMAIL' }
},
{ action: 'click', selector: '#identifier-next' },
{ action: 'waitFor', target: '#password' },
{
action: 'fill',
selector: '#password',
value: { env: 'DEMO_PASSWORD' },
pressEnter: true // submit with Enter instead of a click
}
],
success: { url: '/dashboard' }
}
// Bespoke auth screens: implement DemoAuthDriver yourself.
const customAuth: DemoAuthDriver = {
signIn: async (profile, context) => {
await context.browser?.goto('https://app.example.com/sso');
await context.browser?.click('#continue-with-okta');
await context.browser?.waitFor('.dashboard');
}
};#Annotations
Annotations are presenter-style callouts drawn as a fixed overlay above the page (max z-index, pointer events off — they never intercept the demo's own clicks). The Playwright annotation driver renders them in-page; any environment can implement DemoAnnotationDriver with a single show() method.
import {
circle,
clearAnnotations,
highlight,
spotlight
} from '@absolutejs/demo';
// Spotlight — dims the rest of the page and glows around the target.
spotlight({
selector: "[data-demo='pipeline-total']",
label: 'Revenue at risk',
durationMs: 1800 // auto-clears; omit to keep it up
});
// Circle — a rounded ring around an element, no page dimming.
circle({ selector: '#submit', color: '#ff3b30' });
// Highlight — border-only emphasis; the page stays fully visible.
// Targets are a selector OR an explicit x/y/width/height rect.
highlight({ x: 120, y: 240, width: 320, height: 48, label: 'Drop zone' });
// Remove whatever is currently drawn.
clearAnnotations();#Desktop Control
Native-app automation goes through createCommandDesktopDriver() (from @absolutejs/demo/desktop) — each capability maps to a CLI command. On macOS, createMacDesktopDriver() ships ready-made: it opens and focuses apps with open -a and sends keystrokes via osascript. Linux and Windows provide equivalent factories with xdotool, wmctrl, PowerShell, or a UIA bridge.
import { createDemoRunner, focusApp, openApp, wait } from '@absolutejs/demo';
import {
createCommandDesktopDriver,
createMacDesktopDriver
} from '@absolutejs/demo/desktop';
// macOS: open/focus apps and send keystrokes via osascript.
const runner = createDemoRunner({ desktop: createMacDesktopDriver() });
await runner.run({
id: 'discord-demo',
steps: [openApp('Discord'), wait(1000), focusApp('Discord')]
});
// Linux / Windows: map each capability to a command — xdotool, wmctrl,
// PowerShell, or a UIA bridge. Unconfigured capabilities throw with a
// message naming the missing capability.
const linuxDesktop = createCommandDesktopDriver({
open: (target) => ['xdg-open', String(target)],
focus: (target) => ['wmctrl', '-a', String(target)],
hotkey: (keys) => ['xdotool', 'key', keys.join('+')],
typeText: (text) => ['xdotool', 'type', text],
click: (x, y) => [
'xdotool', 'mousemove', String(x), String(y), 'click', '1'
]
});