Files
backnotprop__plannotator/packages/ui/components/AnnotationPanel.props.test.tsx
Michael Ramos 27791a6fba Mobile Phase 2B: Plan shell and navigation (#1303)
* feat(editor): add compact plan navigator

* feat(editor): simplify compact plan chrome

* fix(editor): close compact navigator after file selection

* feat(editor): add compact plan review surfaces

* fix(editor): hold navigator through cold file loads

* fix(editor): preserve desktop diff activation
2026-08-13 09:38:14 -07:00

148 lines
4.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Consumer-surface contract for AnnotationPanel's host props:
* - readOnly hides every mutation affordance (delete/edit on all card kinds)
* - renderCardFooter renders a per-card slot whose interactions do NOT
* select the card
* - embedded presentation leaves stage geometry and chrome to its host
* All default to today's behavior (mutable panel, no footer).
*/
import { afterEach, describe, expect, test } from 'bun:test';
import React from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { act } from 'react';
import { AnnotationPanel } from './AnnotationPanel';
import { AnnotationType, type Annotation } from '../types';
const hasDom = typeof document !== 'undefined';
const annotation: Annotation = {
id: 'a1',
blockId: 'b1',
startOffset: 0,
endOffset: 5,
type: AnnotationType.COMMENT,
text: 'a note',
originalText: 'hello',
createdA: 1,
};
let root: Root | null = null;
let host: HTMLElement | null = null;
async function mount(ui: React.ReactElement): Promise<void> {
host = document.createElement('div');
document.body.appendChild(host);
await act(async () => {
root = createRoot(host!);
root.render(ui);
});
}
afterEach(async () => {
if (root) {
await act(async () => {
root!.unmount();
});
root = null;
}
host?.remove();
host = null;
if (hasDom) document.body.innerHTML = '';
});
const baseProps = {
isOpen: true,
annotations: [annotation],
blocks: [],
onSelect: () => {},
onDelete: () => {},
selectedId: null,
};
describe('AnnotationPanel consumer props', () => {
test.skipIf(!hasDom)('default renders the delete affordance (today’s behavior)', async () => {
await mount(<AnnotationPanel {...baseProps} />);
expect(document.querySelector('button[title="Delete annotation"]')).not.toBeNull();
});
test.skipIf(!hasDom)('readOnly hides delete and edit affordances', async () => {
await mount(
<AnnotationPanel
{...baseProps}
readOnly
onEdit={() => {}}
/>,
);
expect(document.querySelector('button[title="Delete annotation"]')).toBeNull();
expect(document.querySelector('button[title="Edit annotation"]')).toBeNull();
});
test.skipIf(!hasDom)('readOnly hides direct-edit discard but keeps the host footer slot', async () => {
// The footer's contents are host-owned and may be pure read affordances
// (a replies list, a copy link) — view-only panels losing them was the
// Workspaces regression behind the 0.30.0 change. Built-in mutation
// affordances stay hidden; the host gates its own footer contents.
await mount(
<AnnotationPanel
{...baseProps}
readOnly
directEdits={[{
id: 'plan',
added: 1,
removed: 0,
diffText: '@@ -1 +1 @@\n-old\n+new',
onDiscard: () => {},
}]}
renderCardFooter={() => <button type="button">Reply</button>}
/>,
);
expect(document.body.textContent).not.toContain('Discard');
expect(document.querySelector('[data-annotation-card-footer="true"]')).not.toBeNull();
expect(document.body.textContent).toContain('Reply');
});
test.skipIf(!hasDom)('renderCardFooter renders per-card and does not select the card', async () => {
const selected: string[] = [];
let footerClicks = 0;
await mount(
<AnnotationPanel
{...baseProps}
onSelect={(id) => selected.push(id)}
renderCardFooter={(a) => (
<button type="button" data-testid="reply" onClick={() => { footerClicks++; }}>
reply to {a.id}
</button>
)}
/>,
);
const slot = document.querySelector('[data-annotation-card-footer="true"]');
expect(slot).not.toBeNull();
expect(slot!.textContent).toContain('reply to a1');
const replyBtn = document.querySelector('[data-testid="reply"]') as HTMLButtonElement;
await act(async () => {
replyBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(footerClicks).toBe(1);
// The click bubbled only to the slot wrapper, which stops propagation —
// the card's onSelect must not fire.
expect(selected).toHaveLength(0);
});
test.skipIf(!hasDom)('no footer prop → no slot rendered', async () => {
await mount(<AnnotationPanel {...baseProps} />);
expect(document.querySelector('[data-annotation-card-footer="true"]')).toBeNull();
});
test.skipIf(!hasDom)('embedded presentation keeps the timeline and omits panel chrome', async () => {
await mount(<AnnotationPanel {...baseProps} presentation="embedded" />);
const panel = document.querySelector<HTMLElement>('[data-annotation-panel="true"]');
expect(panel?.className).toContain('h-full min-h-0 w-full flex-1');
expect(document.body.textContent).not.toContain('Annotations');
expect(document.querySelector('[data-annotation-id="a1"]')).not.toBeNull();
expect(document.querySelector('.fixed.inset-0')).toBeNull();
});
});