mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
0ae40e73a4
The v0.27.5 comment-only ruling removed every label affordance from HTML and live-app annotate surfaces, leaving no one-click positive feedback: the only path was opening the composer and typing prose. Restore exactly ONE affordance, the hardcoded 'Looks good' thumbs-up, on both input routes: - selection toolbar: commentOnly + a provided onQuickLabel now renders only the thumbs-up (no Delete, no Zap picker, Alt+digit suppressed); HtmlViewer passes a handler that filters by label id as defense in depth - pinpoint: the composer gains an optional one-click 'Looks good' footer action (disabled once anything is typed, so it can never discard a draft), emitting the same isQuickLabel comment shape with the draft's multi-select targets The trust-boundary clamp is untouched: redline/quickLabel modes stay collapsed to selection, so a hostile page still cannot force a DELETION or an arbitrary label. THUMBS_UP_LABEL moves to utils/quickLabels as the canonical definition.
82 lines
3.1 KiB
TypeScript
82 lines
3.1 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';
|
|
|
|
/**
|
|
* The commentOnly seam (DOM-gated): HTML and live-app surfaces present a
|
|
* comment-only selection toolbar (no Delete, no quick labels), while the
|
|
* markdown surface keeps the full toolbar. This guards the seam in BOTH
|
|
* directions — dropping the prop would resurrect Delete on HTML surfaces,
|
|
* and inverting it would strip Delete from markdown.
|
|
*/
|
|
|
|
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();
|
|
});
|
|
|
|
async function mount(props: { commentOnly?: boolean; withQuickLabels?: boolean }) {
|
|
anchor = document.createElement('p');
|
|
anchor.textContent = 'annotated paragraph';
|
|
document.body.appendChild(anchor);
|
|
host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
root = createRoot(host);
|
|
await act(async () => {
|
|
root?.render(
|
|
<AnnotationToolbar
|
|
element={anchor!}
|
|
positionMode="center-above"
|
|
onAnnotate={() => {}}
|
|
onClose={() => {}}
|
|
onRequestComment={() => {}}
|
|
onQuickLabel={props.withQuickLabels ? () => {} : undefined}
|
|
commentOnly={props.commentOnly}
|
|
/>,
|
|
);
|
|
});
|
|
const toolbar = document.querySelector<HTMLElement>('.annotation-toolbar');
|
|
if (!toolbar) throw new Error('annotation toolbar did not render');
|
|
return Array.from(toolbar.querySelectorAll<HTMLButtonElement>('button')).map((b) => b.title);
|
|
}
|
|
|
|
describe.if(hasDom)('AnnotationToolbar commentOnly seam', () => {
|
|
test('commentOnly without a label handler hides Delete and every label affordance', async () => {
|
|
const titles = await mount({ commentOnly: true });
|
|
expect(titles).toContain('Comment');
|
|
expect(titles).not.toContain('Delete');
|
|
expect(titles).not.toContain('Quick label');
|
|
expect(titles).not.toContain('Looks good');
|
|
});
|
|
|
|
test('commentOnly with a label handler restores ONLY the thumbs-up (no Delete, no picker)', async () => {
|
|
// The restricted restore: HTML/live surfaces get exactly one label
|
|
// affordance back. A regression that also renders the Zap picker (or
|
|
// Delete) re-opens the comment-only ruling this seam enforces.
|
|
const titles = await mount({ commentOnly: true, withQuickLabels: true });
|
|
expect(titles).toContain('Comment');
|
|
expect(titles).toContain('Looks good');
|
|
expect(titles).not.toContain('Delete');
|
|
expect(titles).not.toContain('Quick label');
|
|
});
|
|
|
|
test('the default (markdown surface) toolbar keeps Delete and quick labels', async () => {
|
|
const titles = await mount({ withQuickLabels: true });
|
|
expect(titles).toContain('Delete');
|
|
expect(titles).toContain('Comment');
|
|
expect(titles).toContain('Quick label');
|
|
expect(titles).toContain('Looks good');
|
|
});
|
|
});
|