mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Merge pull request #105 from Ark0N/beta/settings-toggles
feat(settings): toggle gesture control + multi-monitor button (off by default)
This commit is contained in:
@@ -160,7 +160,9 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**Z-index layers**: subagent windows (1000), plan agents (1100), log viewers (2000), image popups (3000), local echo overlay (7).
|
||||
|
||||
**Multi-monitor button** (header, top-right; replaces the notification bell, which is hidden by default but still reachable via Settings → Notifications). `app.launchMultiMonitor()` (in `panels-ui.js`) POSTs `/api/system/span-displays`, which spawns `scripts/span-codeman.sh` — a fresh, maximized browser `--app` window sized to the union of all displays (macOS; needs "Displays have separate Spaces" OFF). Supports the gesture layer's in-page floating session panels dragging across the physical monitor seam.
|
||||
**Multi-monitor button** (header, top-right; the notification bell it sits beside stays hidden — notifications live in Settings → Notifications). `app.launchMultiMonitor()` (in `panels-ui.js`) POSTs `/api/system/span-displays`, which spawns `scripts/span-codeman.sh` — a fresh, maximized browser `--app` window sized to the union of all displays (macOS; needs "Displays have separate Spaces" OFF). Supports the gesture layer's in-page floating session panels dragging across the physical monitor seam. **Opt-in:** hidden by default; enable under App Settings → Display → **Header Displays** ("Multi-monitor Button", `showMultiMonitorButton`). The button carries a `btn-multimonitor--hidden` class in the template; `renderIndexHtml` strips that class at render when the setting is on (a unique class token, not a brittle match on the aria-label/style copy), and `applyHeaderVisibilitySettings()` toggles the same class live on save. Solo (detached) windows hide it via `body.solo-mode`.
|
||||
|
||||
**Gesture control** (the camera hand-tracking overlay) is **opt-in, default OFF**, under App Settings → Display → **Input** (`gestureControlEnabled`). `CODEMAN_GESTURE=1` makes the feature *available* on the instance (CSP widening + `/gesture/` assets) and sets `window.__codemanGestureAvailable` (the Input section only shows when set); the overlay bundle is injected by `renderIndexHtml` **only when the setting is enabled**, so that method is `async` and reads `settings.json` via `readSettings(true)` — the `true` forces a **fresh** read (bypassing the 2s `_settingsCache`), because a post-save reload happens within that TTL and the cached value would otherwise render the pre-toggle state. Toggling the setting reloads the page (the bundle is render-injected).
|
||||
|
||||
**Respawn presets**: `solo-work` (3s/60min), `subagent-workflow` (45s/240min), `team-lead` (90s/480min), `ralph-todo` (8s/480min), `overnight-autonomous` (10s/480min).
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-icon-header btn-response-viewer-header" onclick="app.toggleResponseViewer()" title="View last response" aria-label="View last response"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
|
||||
<button class="btn-icon-header btn-multimonitor" onclick="app.launchMultiMonitor()" title="Open Codeman across all displays" aria-label="Open Codeman across all displays"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="13" height="9" rx="1.5"/><rect x="11" y="9" width="11" height="8" rx="1.5"/></svg></button>
|
||||
<button class="btn-icon-header btn-multimonitor btn-multimonitor--hidden" onclick="app.launchMultiMonitor()" title="Open Codeman across all displays" aria-label="Open Codeman across all displays"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="13" height="9" rx="1.5"/><rect x="11" y="9" width="11" height="8" rx="1.5"/></svg></button>
|
||||
<button class="btn-icon-header btn-notifications" onclick="app.toggleNotifications()" title="Notifications" aria-label="Toggle notifications" style="display:none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
<span class="notification-badge" id="notifBadge" style="display:none;">0</span>
|
||||
@@ -971,6 +971,25 @@
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-item" title="Show the multi-monitor button in the header (opens Codeman spanned across all displays)">
|
||||
<span class="settings-item-label">Multi-monitor Button</span>
|
||||
<label class="switch switch-sm">
|
||||
<input type="checkbox" id="appSettingsShowMultiMonitorButton">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Input Section -->
|
||||
<div id="appSettingsInputSection">
|
||||
<div class="settings-section-header">Input</div>
|
||||
<div class="settings-item" title="Enable the camera hand-tracking gesture overlay (applied on reload). The instance must run with CODEMAN_GESTURE=1.">
|
||||
<span class="settings-item-label">Gesture Control</span>
|
||||
<label class="switch switch-sm">
|
||||
<input type="checkbox" id="appSettingsGestureControl">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Bar Section -->
|
||||
<div class="settings-section-header">Tab Bar</div>
|
||||
|
||||
@@ -312,6 +312,13 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.getElementById('appSettingsShowProjectInsights').checked = settings.showProjectInsights ?? defaults.showProjectInsights ?? false;
|
||||
document.getElementById('appSettingsShowFileBrowser').checked = settings.showFileBrowser ?? defaults.showFileBrowser ?? false;
|
||||
document.getElementById('appSettingsShowSubagents').checked = settings.showSubagents ?? defaults.showSubagents ?? false;
|
||||
document.getElementById('appSettingsShowMultiMonitorButton').checked = settings.showMultiMonitorButton ?? defaults.showMultiMonitorButton ?? false;
|
||||
// Input section: gesture control is only available when the instance runs with
|
||||
// CODEMAN_GESTURE=1 (server sets window.__codemanGestureAvailable). Hide the
|
||||
// whole section otherwise so the toggle can't promise something that won't work.
|
||||
const inputSection = document.getElementById('appSettingsInputSection');
|
||||
if (inputSection) inputSection.style.display = window.__codemanGestureAvailable ? '' : 'none';
|
||||
document.getElementById('appSettingsGestureControl').checked = settings.gestureControlEnabled ?? defaults.gestureControlEnabled ?? false;
|
||||
document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? defaults.subagentTrackingEnabled ?? true;
|
||||
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? defaults.subagentActiveTabOnly ?? true;
|
||||
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? defaults.imageWatcherEnabled ?? false;
|
||||
@@ -1107,6 +1114,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
async saveAppSettings() {
|
||||
// Gesture overlay is injected at page render (server-side), so a change to it
|
||||
// only takes effect on reload — remember the prior value to decide below.
|
||||
const _prevGestureEnabled = (this.loadAppSettingsFromStorage().gestureControlEnabled ?? false) === true;
|
||||
const settings = {
|
||||
defaultClaudeMdPath: document.getElementById('appSettingsClaudeMdPath').value.trim(),
|
||||
defaultWorkingDir: document.getElementById('appSettingsDefaultDir').value.trim(),
|
||||
@@ -1121,6 +1131,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
showProjectInsights: document.getElementById('appSettingsShowProjectInsights').checked,
|
||||
showFileBrowser: document.getElementById('appSettingsShowFileBrowser').checked,
|
||||
showSubagents: document.getElementById('appSettingsShowSubagents').checked,
|
||||
showMultiMonitorButton: document.getElementById('appSettingsShowMultiMonitorButton').checked,
|
||||
gestureControlEnabled: document.getElementById('appSettingsGestureControl').checked,
|
||||
subagentTrackingEnabled: document.getElementById('appSettingsSubagentTracking').checked,
|
||||
subagentActiveTabOnly: document.getElementById('appSettingsSubagentActiveTabOnly').checked,
|
||||
imageWatcherEnabled: document.getElementById('appSettingsImageWatcherEnabled').checked,
|
||||
@@ -1273,6 +1285,18 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
|
||||
this.closeAppSettings();
|
||||
|
||||
// The gesture overlay is injected at page render (server reads
|
||||
// gestureControlEnabled from settings.json), so a change only takes effect on
|
||||
// reload. Reload when it actually changed — the server PUT above already
|
||||
// persisted the new value.
|
||||
if (settings.gestureControlEnabled !== _prevGestureEnabled) {
|
||||
this.showToast(
|
||||
settings.gestureControlEnabled ? 'Enabling gesture control — reloading…' : 'Disabling gesture control — reloading…',
|
||||
'info'
|
||||
);
|
||||
setTimeout(() => location.reload(), 400);
|
||||
}
|
||||
},
|
||||
|
||||
// Load model configuration from server for the settings modal
|
||||
@@ -1374,6 +1398,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
showProjectInsights: false,
|
||||
showFileBrowser: false,
|
||||
showSubagents: false,
|
||||
showMultiMonitorButton: false,
|
||||
// Input
|
||||
gestureControlEnabled: false,
|
||||
// Feature toggles - keep tracking on even on mobile
|
||||
subagentTrackingEnabled: true,
|
||||
subagentActiveTabOnly: true, // Only show subagents for active tab
|
||||
@@ -1445,13 +1472,24 @@ Object.assign(CodemanApp.prototype, {
|
||||
lifecycleBtn.style.display = showLifecycleLog ? '' : 'none';
|
||||
}
|
||||
|
||||
// Hide notification bell when notifications are disabled
|
||||
const notifEnabled = this.notificationManager?.preferences?.enabled ?? true;
|
||||
// Multi-monitor button — hidden by default (App Settings → Display → "Header
|
||||
// Displays"). The server renders the correct initial state on every reload;
|
||||
// this handles a live toggle from a settings save (no reload). Toggle the
|
||||
// marker class (matches the server-side reveal) rather than an inline style.
|
||||
const showMultiMonitorButton = settings.showMultiMonitorButton ?? defaults.showMultiMonitorButton ?? false;
|
||||
const multiMonitorBtn = document.querySelector('.btn-multimonitor');
|
||||
if (multiMonitorBtn) {
|
||||
multiMonitorBtn.classList.toggle('btn-multimonitor--hidden', !showMultiMonitorButton);
|
||||
}
|
||||
|
||||
// Notification bell is retired (notifications live in Settings → Notifications
|
||||
// + the drawer); keep it hidden regardless of the notification-enabled state.
|
||||
const notifBtn = document.querySelector('.btn-notifications');
|
||||
if (notifBtn) {
|
||||
notifBtn.style.display = notifEnabled ? '' : 'none';
|
||||
notifBtn.style.display = 'none';
|
||||
}
|
||||
// Close the drawer if notifications got disabled while it's open
|
||||
const notifEnabled = this.notificationManager?.preferences?.enabled ?? true;
|
||||
if (!notifEnabled) {
|
||||
const drawer = document.getElementById('notifDrawer');
|
||||
if (drawer) drawer.classList.remove('open');
|
||||
|
||||
@@ -846,6 +846,13 @@ body {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* Multi-monitor header button: hidden by default (opt-in via App Settings →
|
||||
Display → "Header Displays"). The server strips this class at render when the
|
||||
setting is on; the client toggles it live on save. */
|
||||
.btn-multimonitor--hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.btn-icon-header.btn-settings {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
|
||||
@@ -293,6 +293,9 @@ export const SettingsUpdateSchema = z
|
||||
showProjectInsights: z.boolean().optional(),
|
||||
showFileBrowser: z.boolean().optional(),
|
||||
showSubagents: z.boolean().optional(),
|
||||
showMultiMonitorButton: z.boolean().optional(),
|
||||
// Input
|
||||
gestureControlEnabled: z.boolean().optional(),
|
||||
// Claude CLI settings
|
||||
claudeMode: z.string().max(50).optional(),
|
||||
allowedTools: z.string().max(2000).optional(),
|
||||
|
||||
+46
-11
@@ -558,10 +558,16 @@ export class WebServer extends EventEmitter {
|
||||
// Security headers + CORS
|
||||
registerSecurityHeaders(this.app, this.https);
|
||||
this.app.get('/', async (_req, reply) => {
|
||||
return reply.header('Cache-Control', 'no-cache').type('text/html; charset=utf-8').send(this.renderIndexHtml());
|
||||
return reply
|
||||
.header('Cache-Control', 'no-cache')
|
||||
.type('text/html; charset=utf-8')
|
||||
.send(await this.renderIndexHtml());
|
||||
});
|
||||
this.app.get('/index.html', async (_req, reply) => {
|
||||
return reply.header('Cache-Control', 'no-cache').type('text/html; charset=utf-8').send(this.renderIndexHtml());
|
||||
return reply
|
||||
.header('Cache-Control', 'no-cache')
|
||||
.type('text/html; charset=utf-8')
|
||||
.send(await this.renderIndexHtml());
|
||||
});
|
||||
// Detached single-session window (undock). Serves the same SPA shell but
|
||||
// flags the client into "solo mode" for one session. Auth applies normally
|
||||
@@ -572,7 +578,10 @@ export class WebServer extends EventEmitter {
|
||||
// explicit route wins over the '/' static prefix.
|
||||
this.app.get('/session/:id', async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
return reply.header('Cache-Control', 'no-cache').type('text/html; charset=utf-8').send(this.renderIndexHtml(id));
|
||||
return reply
|
||||
.header('Cache-Control', 'no-cache')
|
||||
.type('text/html; charset=utf-8')
|
||||
.send(await this.renderIndexHtml(id));
|
||||
});
|
||||
// Service worker must never be cached — browsers check for SW updates on navigation
|
||||
this.app.get('/sw.js', async (_req, reply) => {
|
||||
@@ -992,7 +1001,7 @@ export class WebServer extends EventEmitter {
|
||||
this.broadcast(SseEvent.SessionDeleted, { id: sessionId });
|
||||
}
|
||||
|
||||
private renderIndexHtml(soloSessionId?: string): string {
|
||||
private async renderIndexHtml(soloSessionId?: string): Promise<string> {
|
||||
let html = this.indexHtmlTemplate.replace(
|
||||
'<title>Codeman</title>',
|
||||
`<title>${escapeHtmlText(this.windowTitle)}</title>`
|
||||
@@ -1000,6 +1009,20 @@ export class WebServer extends EventEmitter {
|
||||
// Cache-bust same-origin module scripts + stylesheets so a normal reload
|
||||
// always serves the latest (static assets carry a 1-year immutable cache).
|
||||
html = this.cacheBustAssets(html);
|
||||
// Per-user App-Settings flags, read server-side so the page renders in the
|
||||
// right initial state on every normal reload (the client apply* functions
|
||||
// only run on save). Read FRESH (bypass the 2s cache): a setting toggled
|
||||
// moments ago triggers a reload here, and the cached value would render the
|
||||
// pre-toggle state (e.g. the gesture bundle wouldn't inject until a 2nd
|
||||
// reload). Skipped for solo popups (their header differs).
|
||||
const settings: Record<string, unknown> = soloSessionId ? {} : await this.readSettings(true);
|
||||
// Multi-monitor header button: carries the `btn-multimonitor--hidden` class
|
||||
// in the template by default (App Settings → Display → "Header Displays");
|
||||
// reveal by stripping that class when the user enabled it. Matching a unique
|
||||
// class token (not user-facing copy) keeps this robust against template edits.
|
||||
if (settings.showMultiMonitorButton === true) {
|
||||
html = html.replace(' btn-multimonitor--hidden', '');
|
||||
}
|
||||
// Detached single-session ("solo") window: inject the target session id so
|
||||
// the client can enter solo mode even if a (network-first) service worker
|
||||
// later serves a cached shell. The client primarily detects solo mode from
|
||||
@@ -1011,12 +1034,21 @@ export class WebServer extends EventEmitter {
|
||||
html = html.replace('</head>', `<script>window.__CODEMAN_SOLO__=${safeId};</script>\n</head>`);
|
||||
}
|
||||
// Gesture-control overlay (Phase 5): dashboard only (not solo popups, which
|
||||
// have no tab strip), opt-in via CODEMAN_GESTURE=1. The bundle is served
|
||||
// same-origin from /gesture/ so 'self' covers it; CSP is widened to match in
|
||||
// registerSecurityHeaders under the same flag.
|
||||
// have no tab strip). `CODEMAN_GESTURE=1` makes the feature *available* on
|
||||
// this instance (it also widens CSP + serves the assets); the per-user
|
||||
// `gestureControlEnabled` setting (App Settings → Input, default OFF) is the
|
||||
// actual on/off. We expose `__codemanGestureAvailable` so the settings UI can
|
||||
// show the toggle only when the feature is available, and inject the bundle
|
||||
// (served same-origin from /gesture/, so 'self' covers it) only when enabled.
|
||||
if (!soloSessionId && process.env.CODEMAN_GESTURE === '1') {
|
||||
const v = this.gestureBundleVersion();
|
||||
html = html.replace('</head>', `<script type="module" src="/gesture/gesture-codeman.js${v}"></script>\n</head>`);
|
||||
html = html.replace('</head>', `<script>window.__codemanGestureAvailable=true;</script>\n</head>`);
|
||||
if (settings.gestureControlEnabled === true) {
|
||||
const v = this.gestureBundleVersion();
|
||||
html = html.replace(
|
||||
'</head>',
|
||||
`<script type="module" src="/gesture/gesture-codeman.js${v}"></script>\n</head>`
|
||||
);
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
@@ -1184,10 +1216,13 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
// Read ~/.codeman/settings.json once and return the parsed object.
|
||||
// Cached for 2s to avoid redundant reads during session creation bursts.
|
||||
// The settings PUT route writes the file without invalidating this cache, so
|
||||
// callers that must observe a just-saved value (e.g. renderIndexHtml on a
|
||||
// post-save reload) pass forceFresh=true to bypass the cache.
|
||||
private _settingsCache: { data: Record<string, unknown>; ts: number } | null = null;
|
||||
private async readSettings(): Promise<Record<string, unknown>> {
|
||||
private async readSettings(forceFresh = false): Promise<Record<string, unknown>> {
|
||||
const now = Date.now();
|
||||
if (this._settingsCache && now - this._settingsCache.ts < 2000) {
|
||||
if (!forceFresh && this._settingsCache && now - this._settingsCache.ts < 2000) {
|
||||
return this._settingsCache.data;
|
||||
}
|
||||
const settingsPath = dataPath('settings.json');
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* WebServer.renderIndexHtml — server-side gating of the index shell:
|
||||
* - multi-monitor button reveal (stable class-marker, not brittle copy match)
|
||||
* - solo (/session/:id) global injection + escaping, and settings skipped
|
||||
* - gesture overlay availability vs. enablement (CODEMAN_GESTURE + setting)
|
||||
* - settings read FRESH so a post-save reload doesn't render stale state
|
||||
*
|
||||
* WebServer's constructor only assigns fields (no port bind), so we construct it
|
||||
* directly, swap in a tiny indexHtmlTemplate, and stub readSettings to avoid disk.
|
||||
*
|
||||
* Port: N/A (no server start).
|
||||
*/
|
||||
import { describe, it, expect, afterEach, vi } from 'vitest';
|
||||
import { WebServer } from '../src/web/server.js';
|
||||
|
||||
const TEMPLATE = [
|
||||
'<head>',
|
||||
'<title>Codeman</title>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
'<button class="btn-icon-header btn-multimonitor btn-multimonitor--hidden" aria-label="Open Codeman across all displays"></button>',
|
||||
'</body>',
|
||||
].join('\n');
|
||||
|
||||
function makeServer(settings: Record<string, unknown> = {}) {
|
||||
const server = new WebServer(0, false, true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(server as any).indexHtmlTemplate = TEMPLATE;
|
||||
const readSettings = vi.fn(async () => settings);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(server as any).readSettings = readSettings;
|
||||
return { server, readSettings };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const render = (server: WebServer, solo?: string): Promise<string> => (server as any).renderIndexHtml(solo);
|
||||
|
||||
const ORIG_GESTURE = process.env.CODEMAN_GESTURE;
|
||||
afterEach(() => {
|
||||
if (ORIG_GESTURE === undefined) delete process.env.CODEMAN_GESTURE;
|
||||
else process.env.CODEMAN_GESTURE = ORIG_GESTURE;
|
||||
});
|
||||
|
||||
describe('WebServer.renderIndexHtml', () => {
|
||||
it('keeps the multi-monitor button hidden by default and reads settings FRESH', async () => {
|
||||
const { server, readSettings } = makeServer({});
|
||||
const html = await render(server);
|
||||
expect(html).toContain('btn-multimonitor--hidden');
|
||||
// forceFresh=true — fixes the post-save reload race against the 2s cache.
|
||||
expect(readSettings).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('reveals the multi-monitor button when showMultiMonitorButton is set', async () => {
|
||||
const { server } = makeServer({ showMultiMonitorButton: true });
|
||||
const html = await render(server);
|
||||
expect(html).not.toContain('btn-multimonitor--hidden');
|
||||
expect(html).toContain('btn-multimonitor"'); // class list still present, only the marker stripped
|
||||
});
|
||||
|
||||
it('injects the solo global and skips settings for a /session/:id window', async () => {
|
||||
const { server, readSettings } = makeServer({ showMultiMonitorButton: true });
|
||||
const html = await render(server, 'sess-123');
|
||||
expect(html).toContain('window.__CODEMAN_SOLO__="sess-123"');
|
||||
expect(readSettings).not.toHaveBeenCalled();
|
||||
// Solo skips settings, so the button is NOT revealed even though the setting is on.
|
||||
expect(html).toContain('btn-multimonitor--hidden');
|
||||
});
|
||||
|
||||
it('escapes the solo id so it cannot break out of the inline <script>', async () => {
|
||||
const { server } = makeServer({});
|
||||
const html = await render(server, 'a</script><b>');
|
||||
expect(html).not.toContain('</script><b>');
|
||||
expect(html).toContain('\\u003c');
|
||||
});
|
||||
|
||||
it('exposes gesture availability but injects the bundle only when enabled', async () => {
|
||||
process.env.CODEMAN_GESTURE = '1';
|
||||
let { server } = makeServer({ gestureControlEnabled: false });
|
||||
let html = await render(server);
|
||||
expect(html).toContain('window.__codemanGestureAvailable=true');
|
||||
expect(html).not.toContain('gesture-codeman.js');
|
||||
|
||||
({ server } = makeServer({ gestureControlEnabled: true }));
|
||||
html = await render(server);
|
||||
expect(html).toContain('window.__codemanGestureAvailable=true');
|
||||
expect(html).toContain('gesture-codeman.js');
|
||||
});
|
||||
|
||||
it('does not expose gesture at all when CODEMAN_GESTURE is unset', async () => {
|
||||
delete process.env.CODEMAN_GESTURE;
|
||||
const { server } = makeServer({ gestureControlEnabled: true });
|
||||
const html = await render(server);
|
||||
expect(html).not.toContain('__codemanGestureAvailable');
|
||||
expect(html).not.toContain('gesture-codeman.js');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user