Files
pi-gui/test/quick-pane-ui.test.js
T

411 lines
16 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
test("Quick Pane polls snapshots, renders answers, and promotes runtime without a handoff", async () => {
const source = await readFile(
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
"utf8",
);
assert.match(source, /get_session_runtime_snapshot/);
assert.match(source, /setInterval\(\(\) => void refresh\(\), 800\)/);
assert.match(source, /<ReactMarkdown remarkPlugins=\{\[remarkGfm\]\}>/);
assert.match(source, /className="quick-pane-notice escalation"/);
assert.match(source, /className="quick-pane-assistant-footer"/);
assert.match(source, /Continue in full session/);
const toolbar = source.slice(
source.indexOf('<header className="quick-pane-toolbar">'),
source.indexOf(
"</header>",
source.indexOf('<header className="quick-pane-toolbar">'),
),
);
assert.doesNotMatch(toolbar, /continueFull|Continue/);
assert.match(
source,
/await invoke<\{ runtime: Runtime \}>\("promote_quick_runtime", \{\s+runtimeId: current\.runtimeId,/,
);
assert.doesNotMatch(source, /Quick Pane transcript:/);
assert.doesNotMatch(source, /create_session_runtime/);
assert.doesNotMatch(source, /maximumSeconds|Quick time budget/);
});
test("main Settings uses shared catalog selectors with provider derived from models", async () => {
const [pane, app, catalog, persistence] = await Promise.all([
readFile(
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
"utf8",
),
readFile(new URL("../ui/src/App.tsx", import.meta.url), "utf8"),
readFile(new URL("../ui/src/model-catalog.ts", import.meta.url), "utf8"),
readFile(
new URL("../ui/src/workspace/persistence.ts", import.meta.url),
"utf8",
),
]);
assert.doesNotMatch(catalog, /get_model_catalog/);
assert.match(app, /create_quick_runtime/);
assert.match(app, /get_session_runtime_snapshot/);
assert.match(app, /close_quick_runtime/);
assert.match(app, /label="Quick model"/);
assert.match(app, /label="Follow-up model"/);
assert.match(app, /<ModelSelector/);
assert.doesNotMatch(app, /Quick provider\s*<input/);
assert.doesNotMatch(app, /Follow-up provider\s*<input/);
assert.match(app, /provider: identity\.provider/);
assert.match(app, /followUpProvider: identity\.provider/);
assert.match(app, /Save Quick Pane settings/);
assert.match(pane, /snapshot\.models\?\.data\?\.models/);
assert.match(pane, /preparedRuntimeRef/);
assert.doesNotMatch(pane, /get_model_catalog/);
assert.match(pane, /set_thinking_level/);
assert.match(pane, /Loading models…/);
assert.match(pane, /catalogStatus !== "ready"/);
assert.doesNotMatch(pane, /showSettings/);
assert.doesNotMatch(pane, /Save Quick Pane settings/);
assert.match(pane, /!snapshotIsActive\(snapshot\)/);
assert.match(pane, /if \(current !== submitted\.request\) return current;/);
assert.match(pane, /inputSelectionRef\.current = undefined;/);
assert.match(pane, /input\.setSelectionRange\(/);
assert.match(pane, /onSelect=\{\(event\) => recordInputSelection/);
assert.match(pane, /onInput=\{\(event\) => recordInputChangeSelection/);
assert.match(pane, /<ThinkingSelector/);
assert.match(pane, /quick-pane-catalog-overlay/);
assert.match(pane, /readOnly=\{runActive\}/);
assert.match(pane, /window\.requestAnimationFrame\(focusInput\)/);
assert.doesNotMatch(pane, /\.slice\(0, 5\)/);
assert.doesNotMatch(pane, /aria-label="Answer history"/);
assert.match(
pane,
/transcriptMessages\(snapshot, submittedPromptTurnsRef\.current\)/,
);
assert.match(pane, /const QUICK_PANE_ADVISORY_PREFIX =/);
assert.match(pane, /submittedPromptTurns\[nextSubmittedTurn\]/);
assert.match(
pane,
/text\.startsWith\(QUICK_PANE_ADVISORY_PREFIX\) \? "" : text/,
);
assert.match(pane, /submittedPromptTurnsRef\.current\.push\(submittedTurn\)/);
assert.match(pane, /nextSubmittedTurnIdRef\.current/);
assert.match(pane, /submittedPromptTurnsRef\.current = \[\]/);
assert.doesNotMatch(
pane,
/delimiterIndex|text\.indexOf\(USER_REQUEST_DELIMITER\)/,
);
assert.match(
pane,
/message\.role !== "user" && message\.role !== "assistant"/,
);
assert.match(pane, /lastUserQuestionAtRef\.current = submittedAt/);
assert.match(pane, /lastCompletedAnswerAtRef\.current = Date\.now\(\)/);
assert.doesNotMatch(pane, /localStorage|sessionStorage|save_ui_state/);
assert.match(persistence, /quickPane: current\.quickPane/);
});
test("Quick Pane keeps slash-containing model IDs atomic and renews catalog on focus", async () => {
const [pane, catalog] = await Promise.all([
readFile(
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
"utf8",
),
readFile(new URL("../ui/src/model-catalog.ts", import.meta.url), "utf8"),
]);
assert.match(pane, /useState<ModelIdentity>/);
assert.match(pane, /provider: submittedModel\.provider/);
assert.match(pane, /modelId: submittedModel\.modelId/);
assert.doesNotMatch(pane, /selectedModel\.split/);
assert.match(pane, /onFocusChanged[\s\S]*?void renewChatOnFocus\(\)/);
assert.match(pane, /if \(!event\.payload\) return;/);
assert.match(pane, /const CHAT_RENEWAL_MS = 180_000/);
assert.match(pane, /Date\.now\(\) - lastActivity > CHAT_RENEWAL_MS/);
assert.match(pane, /event\.ctrlKey && event\.key\.toLowerCase\(\) === "n"/);
assert.match(pane, /event\.preventDefault\(\)/);
assert.doesNotMatch(
pane,
/setInterval[^\n]*CHAT_RENEWAL_MS|setTimeout[^\n]*CHAT_RENEWAL_MS/,
);
assert.match(
catalog,
/JSON\.stringify\(\[identity\.provider, identity\.modelId\]\)/,
);
assert.match(catalog, /JSON\.parse\(value\)/);
});
test("Quick Pane dismiss and mutually exclusive window visibility stay native-serialized", async () => {
const [pane, nativeSource] = await Promise.all([
readFile(
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
"utf8",
),
readFile(new URL("../ui/src-tauri/src/lib.rs", import.meta.url), "utf8"),
]);
assert.match(pane, /invoke\("dismiss_quick_pane"\)/);
assert.doesNotMatch(pane, /getCurrentWindow\(\)\.hide\(\)/);
const dismissPane = pane.slice(
pane.indexOf("const dismissPane"),
pane.indexOf("const clearChatState"),
);
assert.doesNotMatch(dismissPane, /closeRuntime|clearRuntime|prepareRuntime/);
assert.match(nativeSource, /fn dismiss_quick_pane\(/);
assert.match(
nativeSource,
/dismiss_quick_pane,[\s\S]*create_session_runtime/,
);
assert.match(
nativeSource,
/struct WindowVisibility\(Mutex<ManagedWindowVisibility>\)/,
);
const mainShow = nativeSource.slice(
nativeSource.indexOf("fn apply_main_window_action"),
nativeSource.indexOf("fn get_or_create_quick_pane"),
);
const mainHideQuick = mainShow.indexOf("quick_pane.hide()");
const mainClearQuick = mainShow.indexOf('visibility.hidden("quick-pane")');
const showMain = mainShow.indexOf("window.show()");
assert.ok(mainShow.indexOf('get_webview_window("quick-pane")') >= 0);
assert.ok(mainHideQuick >= 0 && mainHideQuick < showMain);
assert.ok(mainClearQuick >= 0 && mainClearQuick < showMain);
const quickShow = nativeSource.slice(
nativeSource.indexOf("fn toggle_quick_pane"),
nativeSource.indexOf("fn requested_new_worktree_argument"),
);
const quickHideMain = quickShow.indexOf("main.hide()");
const quickClearMain = quickShow.indexOf('visibility.hidden("main")');
const showQuick = quickShow.indexOf("window.show()");
assert.ok(quickShow.indexOf('get_webview_window("main")') >= 0);
assert.ok(quickHideMain >= 0 && quickHideMain < showQuick);
assert.ok(quickClearMain >= 0 && quickClearMain < showQuick);
});
test("native promotion owns visibility and Main consumes pending runtime after startup refresh", async () => {
const [nativeSource, workspaceSource] = await Promise.all([
readFile(new URL("../ui/src-tauri/src/lib.rs", import.meta.url), "utf8"),
readFile(
new URL("../ui/src/workspace/useWorkspace.ts", import.meta.url),
"utf8",
),
]);
const promotion = nativeSource.slice(
nativeSource.indexOf("fn show_main_for_promoted_runtime"),
nativeSource.indexOf("fn toggle_quick_pane"),
);
assert.match(promotion, /get_or_create_main_with_visibility\(app, false\)/);
assert.ok(
promotion.indexOf("quick_pane\n .hide()") <
promotion.indexOf("window\n .show()"),
);
assert.ok(
promotion.indexOf('visibility.hidden("quick-pane")') <
promotion.indexOf("window\n .show()"),
);
assert.match(promotion, /\*pending = Some\(runtime_id\)/);
assert.match(nativeSource, /fn get_pending_promoted_runtime/);
assert.match(nativeSource, /fn consume_pending_promoted_runtime/);
assert.match(workspaceSource, /await promotedListenerReady/);
assert.match(workspaceSource, /await refresh\(loaded\)/);
assert.match(workspaceSource, /get_pending_promoted_runtime/);
assert.match(workspaceSource, /consume_pending_promoted_runtime/);
});
test("Quick Pane routes extension responses through existing bridge command", async () => {
const source = await readFile(
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
"utf8",
);
assert.match(
source,
/<ExtensionDialog extension=\{extension\} onRespond=\{respond\} \/>/,
);
assert.match(source, /invoke\("respond_to_extension"/);
assert.match(source, /requestId: extension\.id/);
});
test("Quick Pane uses native dimensions and request-first compact layout", async () => {
const [source, stylesheet, configSource, nativeSource, capabilitySource] =
await Promise.all([
readFile(
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../ui/src/quick-pane/quick-pane.css", import.meta.url),
"utf8",
),
readFile(
new URL("../ui/src-tauri/tauri.conf.json", import.meta.url),
"utf8",
),
readFile(new URL("../ui/src-tauri/src/lib.rs", import.meta.url), "utf8"),
readFile(
new URL("../ui/src-tauri/capabilities/default.json", import.meta.url),
"utf8",
),
]);
const config = JSON.parse(configSource);
const capability = JSON.parse(capabilitySource);
const quickPane = config.app.windows.find(
(window) => window.label === "quick-pane",
);
assert.deepEqual(
{
width: quickPane.width,
height: quickPane.height,
minWidth: quickPane.minWidth,
minHeight: quickPane.minHeight,
transparent: quickPane.transparent,
},
{
width: 640,
height: 360,
minWidth: 520,
minHeight: 360,
transparent: true,
},
);
assert.ok(capability.windows.includes("quick-pane"));
assert.match(nativeSource, /WindowEvent::CloseRequested/);
assert.match(nativeSource, /api\.prevent_close\(\)/);
assert.match(
nativeSource,
/WebviewWindowBuilder::from_config\(app, config\)/,
);
assert.match(nativeSource, /get_or_create_quick_pane/);
assert.match(nativeSource, /fn resize_quick_pane/);
assert.match(nativeSource, /height\.clamp\(360\.0, 520\.0\)/);
assert.match(source, /new ResizeObserver\(resizeToContent\)/);
assert.match(source, /invoke\("resize_quick_pane", \{ height \}\)/);
assert.doesNotMatch(source, /onCloseRequested/);
assert.match(source, /className="quick-pane-drag-region"/);
assert.match(source, /startDragging\(\)/);
assert.match(source, /className="quick-pane-request"/);
assert.match(source, /className=\{`quick-pane-exchange/);
assert.match(source, /onOpen=\{handleModelMenuOpen\}/);
assert.match(source, /onClose=\{handleModelMenuClose\}/);
assert.match(source, /requestPaneResize\(QUICK_PANE_MAX_HEIGHT\)/);
assert.match(source, /const QUICK_PANE_MIN_HEIGHT = 360/);
assert.match(source, /const QUICK_PANE_MAX_HEIGHT = 520/);
assert.match(source, /height = clampQuickPaneHeight\(height\)/);
assert.match(source, /if \(modelMenuOpenRef\.current\) return;/);
assert.match(source, /submittedPromptTurnsRef/);
assert.match(source, /function elapsedLabel/);
assert.match(source, /quick-pane-assistant-activity/);
assert.match(source, /transcriptFollowRef/);
assert.match(
source,
/transcript\.scrollHeight\s*-\s*transcript\.clientHeight/,
);
assert.match(source, /className="quick-pane-composer"/);
assert.match(source, /className="quick-pane-composer-actions"/);
assert.match(source, /replaceChat\(false, true\)/);
assert.match(source, /replaceChat\(false, false\)/);
assert.match(source, /className="quick-pane-send"/);
assert.match(source, /ref=\{inputRef\}/);
assert.match(source, /focusInput\(\)/);
assert.doesNotMatch(source, /if \(!event\.payload\)[\s\S]*?hide\(/);
assert.match(
source,
/event\.key === "Enter" &&\s*!event\.shiftKey &&\s*!event\.nativeEvent\.isComposing/,
);
assert.match(source, /event\.preventDefault\(\);\s*void run\(\);/);
assert.match(
stylesheet,
/\.quick-pane-exchange[\s\S]*?flex: 1 1 auto[\s\S]*?overflow: hidden/,
);
assert.match(
stylesheet,
/\.quick-pane-transcript[\s\S]*?flex: 1 1 auto[\s\S]*?overflow-y: auto/,
);
assert.match(
stylesheet,
/\.quick-pane-composer[\s\S]*?height: 120px[\s\S]*?min-height: 110px[\s\S]*?max-height: 130px/,
);
assert.match(stylesheet, /\.quick-pane-drag-region[\s\S]*?position:\s*absolute[\s\S]*?height:\s*10px[\s\S]*?cursor:\s*grab/);
assert.match(
stylesheet,
/\.quick-pane-composer-actions[\s\S]*?position: absolute/,
);
assert.match(stylesheet, /\.quick-pane-send[\s\S]*?position: absolute/);
assert.match(
stylesheet,
/\.quick-pane-assistant-elapsed[\s\S]*?font-variant-numeric/,
);
assert.match(stylesheet, /\.quick-pane-assistant-activity[\s\S]*?color:/);
assert.match(stylesheet, /@media \(max-width: 400px\)/);
assert.match(stylesheet, /html,[\s\S]*?#root\s*\{[\s\S]*?overflow: hidden/);
assert.match(stylesheet, /\.quick-pane\s*\{[\s\S]*?overflow: hidden/);
assert.doesNotMatch(
stylesheet,
/\.quick-pane-primary-controls\s*\{[^}]*overflow:\s*(?:hidden|clip)/,
);
assert.doesNotMatch(
stylesheet,
/\.quick-pane-header-actions\s*\{[^}]*overflow:\s*(?:hidden|clip)/,
);
assert.match(
stylesheet,
/\.quick-pane button\.quick-pane-toolbar-control,[\s\S]*?height:\s*27px[\s\S]*?min-width:\s*48px[\s\S]*?padding:\s*0 8px[\s\S]*?border:[\s\S]*?border-radius:[\s\S]*?background:[\s\S]*?font:/,
);
assert.match(
stylesheet,
/\.quick-pane \.quick-pane-toolbar-control:hover:not\(:disabled\)/,
);
assert.match(
stylesheet,
/\.quick-pane \.quick-pane-toolbar-control:focus-visible/,
);
assert.match(
stylesheet,
/\.quick-pane \.quick-pane-toolbar-control:disabled/,
);
assert.match(
stylesheet,
/\.quick-pane-thinking\s*\{[\s\S]*?flex:\s*0 0 143px[\s\S]*?grid-template-columns:\s*auto minmax\(84px, 1fr\)/,
);
assert.match(
stylesheet,
/\.quick-pane-toolbar\s*\{[\s\S]*?flex-wrap:\s*nowrap/,
);
assert.match(source, /triggerClassName="quick-pane-toolbar-control"/);
assert.match(source, /controlClassName="quick-pane-toolbar-control"/);
assert.match(
source,
/className="quick-pane-toolbar-control quick-pane-action"/,
);
assert.match(
source,
/className="quick-pane-toolbar-control quick-pane-close"/,
);
assert.match(stylesheet, /#f0a347/);
assert.doesNotMatch(stylesheet, /#8aa9d6|#46494f|#23252a/);
const [selectorSource, selectorStylesheet] = await Promise.all([
readFile(
new URL("../ui/src/components/ModelSelector.tsx", import.meta.url),
"utf8",
),
readFile(
new URL("../ui/src/components/ModelSelector.css", import.meta.url),
"utf8",
),
]);
assert.match(selectorSource, /import "\.\/ModelSelector\.css";/);
assert.match(selectorSource, /onOpen\?: \(\) => void;/);
assert.match(selectorSource, /onClose\?: \(\) => void;/);
assert.match(selectorSource, /onCloseRef\.current\?\.\(\);/);
assert.match(
selectorStylesheet,
/\.model-trigger\s*>\s*span\s*\{[\s\S]*?overflow:\s*hidden;[\s\S]*?text-overflow:\s*ellipsis/,
);
assert.match(
selectorStylesheet,
/\.model-menu\s*\{[\s\S]*?grid-template-rows:\s*auto auto minmax\(0, 1fr\);[\s\S]*?overflow:\s*hidden/,
);
assert.match(
selectorStylesheet,
/\.model-results\s*\{[\s\S]*?overflow-y:\s*auto/,
);
});