Files
backnotprop__plannotator/packages/ui/components/AnnotationToolbar.modeShortcutGuard.test.tsx
Michael Ramos 67f47dbac1 fix(annotate): armed-mode interaction fixes from the v0.27.5 QA gate (#1363)
* fix(annotate): pre-release QA fixes for the armed-mode interaction seams

Six confirmed QA findings on the HTML/live annotate surface plus missing
pi-extension resync coverage:

1. Armed pinpoint drifted click (>4px, no selection) was swallowed AND
   leaked to the page: the always-on drag work armed the trailing-click
   suppression on drift alone. The mouseup arming site now requires the
   drag to have actually produced a text selection; drifted clicks pin
   normally and never reach the page. Bridge tests for armed drift,
   armed real drag, and Interact drift.
2. Esc ladder: hover-clear is no longer its own rung; clearing the
   pinpoint outline and posting annotate-exit happen on the same press
   when no draft is open. Draft-close keeps its own press.
3. Compact touch layouts no longer apply a restored toolsHidden:true
   chrome cookie (both header toggles are desktop-only, so applying it
   stranded the user); the cookie value is preserved for desktop.
4. The live-app probe now announces the static-conversion downgrade on
   stderr when a loopback probe fails, naming --app to force live mode.
5. Live-app export: page group headers are now '## Page:' with '### N.'
   entries nested below them; exports without pageUrl stay byte-identical.
6. Shift+1-4 mode shortcuts no longer fire while the annotation
   toolbar's type-to-comment listener owns printable keys, so typing
   ! @ # $ into a starting comment cannot silently switch modes.

Also adds the missing tests for the two resyncPhaseFromSession
executing->idle fallbacks that arm idleNoticePending (verified by
mutation: flipping either arm fails its test).

* fix(annotate): compact arm/disarm affordance, guarded shutdown, restored chrome guards

Follow-up scope from the forensics sweep, same surface:

- Compact touch layouts get Options-menu actions for the HTML/live
  surface: 'Annotate page'/'Interact with page' (the desktop pen and
  Mod+Shift+A were unreachable on touch, so every tap annotated with no
  way out) and 'Show tools'/'Hide tools' (the desktop eye). With the
  menu as the way back, the toolsHidden cookie now applies on compact
  again (desktop parity) instead of being ignored.
- The annotate servers' stop() now guards every disposal step
  individually (Bun: runGuardedShutdown, mirrored inline in Pi): a
  throwing agent-terminal teardown (#1314-class) no longer skips
  liveProxy.stop() and the other disposals after it. Unit-tested with a
  throwing disposer.
- Re-added the two regression guards dropped in the htmlHideTools ->
  htmlChrome test rename: the restore commit never writes stale
  pre-restore chrome values to the cookie, and the sidebar stays
  reachable via Mod+B while tools are hidden.

* fix(annotate): scope the Agent TUI display reset to display settings only

The Display popover's 'Reset terminal display settings' button also called
onSideChange('left'), durably overwriting a user's chosen right/hidden
placement in config.json with no disclosure — the label scopes the reset
to font/appearance. Position is a layout preference with its own explicit
segmented control right below, so the reset no longer touches it: the
button now resets exactly the display settings through the panel's one
sanitized update path, and the popover no longer has any code path from
reset to the side.

AgentTerminalDisplayPopover is now exported with a defaultOpen test seam
(the surrounding panel needs a live WebTUI session to render it); tests
assert reset restores the display defaults without firing onSideChange,
and that the Position control remains the explicit way to change
placement.
2026-08-21 08:55:30 -07:00

136 lines
4.3 KiB
TypeScript

import { afterEach, describe, expect, test } from 'bun:test';
import React, { act } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { AnnotationToolbar } from './AnnotationToolbar';
import { useAnnotationModeShortcuts } from '../shortcuts/plan-review/annotationMode.shortcuts';
/**
* Shift+1..4 vs type-to-comment (DOM-gated, #1244 follow-up).
*
* The annotation-mode shortcuts produce printable characters (! @ # $).
* While the annotation toolbar's type-to-comment listener is active (a
* selection exists and the toolbar is open), those keys belong to the
* comment being started: the failure this guards is Shift+3 ("#") silently
* arming Redline while the user thinks they are typing into the toolbar.
* With no toolbar open, the shortcuts must keep working.
*/
const hasDom = typeof document !== 'undefined';
let root: Root | null = null;
let host: HTMLElement | null = null;
let anchor: HTMLElement | null = null;
afterEach(async () => {
if (root) await act(async () => root?.unmount());
root = null;
host?.remove();
host = null;
anchor?.remove();
anchor = null;
if (hasDom) document.body.replaceChildren();
});
function ModeShortcutHost({ onMode }: { onMode: (mode: string) => void }) {
useAnnotationModeShortcuts({
handlers: {
selectMarkupMode: () => onMode('selection'),
selectCommentMode: () => onMode('comment'),
selectRedlineMode: () => onMode('redline'),
selectQuickLabelMode: () => onMode('quickLabel'),
},
});
return null;
}
function pressShiftDigit(digit: number, key: string): KeyboardEvent {
const event = new KeyboardEvent('keydown', {
key,
code: `Digit${digit}`,
shiftKey: true,
bubbles: true,
cancelable: true,
});
window.dispatchEvent(event);
return event;
}
async function mount(ui: React.ReactElement): Promise<void> {
host = document.createElement('div');
document.body.appendChild(host);
root = createRoot(host);
await act(async () => {
root?.render(ui);
});
}
describe.if(hasDom)('annotation-mode shortcuts vs type-to-comment', () => {
test('with no toolbar open, Shift+3 switches to redline', async () => {
const modes: string[] = [];
await mount(<ModeShortcutHost onMode={(m) => modes.push(m)} />);
await act(async () => {
pressShiftDigit(3, '#');
});
expect(modes).toEqual(['redline']);
});
test('with the toolbar open, typing "!" starts a comment and does NOT switch mode', async () => {
const modes: string[] = [];
const commentChars: Array<string | undefined> = [];
anchor = document.createElement('p');
anchor.textContent = 'annotated paragraph';
document.body.appendChild(anchor);
await mount(
<>
<ModeShortcutHost onMode={(m) => modes.push(m)} />
<AnnotationToolbar
element={anchor}
positionMode="center-above"
onAnnotate={() => {}}
onClose={() => {}}
onRequestComment={(initialChar) => commentChars.push(initialChar)}
/>
</>,
);
expect(document.querySelector('.annotation-toolbar')).not.toBeNull();
await act(async () => {
pressShiftDigit(1, '!');
});
// The character reached the comment path with its identity intact...
expect(commentChars).toEqual(['!']);
// ...and no mode changed underneath the user.
expect(modes).toEqual([]);
});
test('closing the toolbar releases the capture: the shortcut works again', async () => {
const modes: string[] = [];
anchor = document.createElement('p');
anchor.textContent = 'annotated paragraph';
document.body.appendChild(anchor);
const withToolbar = (
<>
<ModeShortcutHost onMode={(m) => modes.push(m)} />
<AnnotationToolbar
element={anchor}
positionMode="center-above"
onAnnotate={() => {}}
onClose={() => {}}
onRequestComment={() => {}}
/>
</>
);
await mount(withToolbar);
await act(async () => {
pressShiftDigit(2, '@');
});
expect(modes).toEqual([]);
// The toolbar unmounts (selection resolved/dismissed): shortcuts return.
await act(async () => {
root?.render(<ModeShortcutHost onMode={(m) => modes.push(m)} />);
});
await act(async () => {
pressShiftDigit(2, '@');
});
expect(modes).toEqual(['comment']);
});
});