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.
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
/**
|
|
* Composer one-click "Looks good" (DOM_TESTS=1)
|
|
*
|
|
* The restored thumbs-up for comment-only surfaces: pinpoint clicks open the
|
|
* composer directly (never the selection toolbar), so the composer carries a
|
|
* footer "Looks good" action when the host passes onQuickLookGood. The button
|
|
* must never discard a draft: once the user has typed, it disables and Save
|
|
* becomes the path. Hosts that pass nothing render no button at all.
|
|
*/
|
|
|
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import React, { act } from 'react';
|
|
import { createRoot, type Root } from 'react-dom/client';
|
|
import { CommentPopover } from './CommentPopover';
|
|
|
|
const hasDom = typeof document !== 'undefined';
|
|
|
|
let root: Root | null = null;
|
|
let host: HTMLElement | null = null;
|
|
|
|
async function mount(props: { onQuickLookGood?: () => void }): Promise<void> {
|
|
const anchor = document.createElement('p');
|
|
anchor.textContent = 'pinpointed element text';
|
|
document.body.appendChild(anchor);
|
|
host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
root = createRoot(host);
|
|
await act(async () =>
|
|
root?.render(
|
|
<CommentPopover
|
|
anchorEl={anchor}
|
|
contextText="pinpointed element text"
|
|
isGlobal={false}
|
|
onSubmit={() => {}}
|
|
onClose={() => {}}
|
|
onQuickLookGood={props.onQuickLookGood}
|
|
/>,
|
|
),
|
|
);
|
|
await act(async () => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
}
|
|
|
|
function looksGoodButton(): HTMLButtonElement | null {
|
|
return (
|
|
Array.from(document.querySelectorAll<HTMLButtonElement>('button')).find((b) =>
|
|
b.textContent?.includes('Looks good'),
|
|
) ?? null
|
|
);
|
|
}
|
|
|
|
function composerTextarea(): HTMLTextAreaElement {
|
|
const el = document.querySelector<HTMLTextAreaElement>('textarea');
|
|
if (!el) throw new Error('composer textarea did not render');
|
|
return el;
|
|
}
|
|
|
|
afterEach(async () => {
|
|
if (root) await act(async () => root?.unmount());
|
|
root = null;
|
|
host?.remove();
|
|
host = null;
|
|
document.body.textContent = '';
|
|
});
|
|
|
|
describe.if(hasDom)('CommentPopover onQuickLookGood', () => {
|
|
test('renders the footer button and a click fires the host callback', async () => {
|
|
let fired = 0;
|
|
await mount({ onQuickLookGood: () => fired++ });
|
|
const btn = looksGoodButton();
|
|
expect(btn).not.toBeNull();
|
|
expect(btn!.disabled).toBe(false);
|
|
await act(async () => btn!.click());
|
|
expect(fired).toBe(1);
|
|
});
|
|
|
|
test('disables once the user has typed, so a click can never discard a draft', async () => {
|
|
let fired = 0;
|
|
await mount({ onQuickLookGood: () => fired++ });
|
|
const textarea = composerTextarea();
|
|
await act(async () => {
|
|
const setter = Object.getOwnPropertyDescriptor(
|
|
HTMLTextAreaElement.prototype,
|
|
'value',
|
|
)!.set!;
|
|
setter.call(textarea, 'actually, one concern');
|
|
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
const btn = looksGoodButton();
|
|
expect(btn).not.toBeNull();
|
|
expect(btn!.disabled).toBe(true);
|
|
await act(async () => btn!.click());
|
|
expect(fired).toBe(0);
|
|
});
|
|
|
|
test('absent callback renders no button (markdown/global composers unchanged)', async () => {
|
|
await mount({});
|
|
expect(looksGoodButton()).toBeNull();
|
|
});
|
|
});
|