open-design/apps/web/tests/components/BoardComposerPopover.pod-remove.test.tsx
lefarcen 755d84e64c
feat(web): merge Draw + Screenshot into one Studio mark tool (#3081) (#3277)
Forward-ports chaoxiaoche's Studio toolbar work from #3081 onto current
main. The preview toolbar drops to 4 controls — Comment, Mark (the merged
Draw/Screenshot tool with box-select + pen sub-tools), Edit, Comments —
matching the latest design. The standalone Screenshot button and its
copy-to-clipboard path are removed; capture now flows through the mark
overlay. Also carries #3081's comment select-all/clear-selection panel and
keeps the Draw send guard added in #3270 (Send disabled mid-run, Queue stays).

Reconciled with main work that postdates #3081's base so nothing is lost:
- Preserves #2190's preview iframe keep-alive pool and the AnnotationHoverPopover
  hover card (re-added on top of #3081's BoardComposerPopover, with its own
  anchor helper so it doesn't clash with the composer popover anchoring).
- i18n: keeps every locale key main added; adopts #3081's mark wording.

Behavior change: the comment side-panel Clear now deselects instead of
batch-deleting selected comments (per #3081); per-comment delete and
send-selected remain.

Validation: pnpm --filter @open-design/web typecheck (clean),
full web vitest (2354 passed), pnpm guard.

Co-authored-by: chaoxiaoche <fanzhen910412@gmail.com>
2026-05-29 06:51:38 +00:00

196 lines
5.8 KiB
TypeScript
Raw 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.

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { BoardComposerPopover } from '../../src/components/BoardComposerPopover';
import type { PreviewCommentSnapshot } from '../../src/comments';
import type { PreviewComment, PreviewCommentMember } from '../../src/types';
afterEach(() => {
cleanup();
});
function member(elementId: string, label = elementId): PreviewCommentMember {
return {
elementId,
selector: `#${elementId}`,
label,
text: '',
position: { x: 0, y: 0, width: 10, height: 10 },
htmlHint: '',
};
}
function podTarget(members: PreviewCommentMember[]): PreviewCommentSnapshot {
return {
filePath: 'index.html',
elementId: 'pod-1',
selector: '',
label: 'Pod',
text: '',
position: { x: 0, y: 0, width: 100, height: 60 },
htmlHint: '',
selectionKind: 'pod',
memberCount: members.length,
podMembers: members,
};
}
function elementTarget(): PreviewCommentSnapshot {
return {
...podTarget([]),
selectionKind: 'element',
memberCount: undefined,
podMembers: undefined,
};
}
function existingComment(note: string): PreviewComment {
return {
id: 'comment-1',
projectId: 'project-1',
conversationId: 'conversation-1',
filePath: 'index.html',
elementId: 'pod-1',
selector: '',
label: 'Pod',
text: '',
position: { x: 0, y: 0, width: 100, height: 60 },
htmlHint: '',
note,
status: 'open',
createdAt: 1,
updatedAt: 1,
};
}
function renderPopover(overrides: {
target: PreviewCommentSnapshot;
onRemoveMember: (elementId: string) => void;
}) {
return render(
<BoardComposerPopover
target={overrides.target}
existing={null}
draft=""
notes={[]}
onDraft={() => {}}
onAddDraft={() => {}}
onRemoveQueuedNote={() => {}}
onClose={() => {}}
onSaveComment={() => {}}
onSendBatch={() => {}}
onRemoveMember={overrides.onRemoveMember}
sending={false}
t={((key: string) => String(key)) as never}
/>,
);
}
describe('BoardComposerPopover captured-component removal', () => {
it('calls onRemoveMember with the chip elementId when the chip × is clicked', () => {
const onRemoveMember = vi.fn();
renderPopover({
target: podTarget([member('alpha', 'Alpha'), member('beta', 'Beta')]),
onRemoveMember,
});
const alphaChip = screen.getByText('Alpha').closest('.board-pod-chip');
if (!alphaChip) throw new Error('Alpha chip not rendered');
fireEvent.click(within(alphaChip as HTMLElement).getByRole('button'));
expect(onRemoveMember).toHaveBeenCalledTimes(1);
expect(onRemoveMember).toHaveBeenCalledWith('alpha');
});
it('renders every captured chip so members beyond the sixth stay reachable', () => {
const members = Array.from({ length: 10 }, (_, i) => member(`m${i}`, `Member ${i}`));
renderPopover({ target: podTarget(members), onRemoveMember: () => {} });
expect(document.querySelectorAll('.board-pod-chip')).toHaveLength(10);
expect(screen.queryByText('Member 9')).not.toBeNull();
});
it('keeps the floating composer inside the preview bounds near the right edge', () => {
render(
<BoardComposerPopover
target={{
...podTarget([]),
hoverPoint: { x: 612, y: 120 },
position: { x: 600, y: 110, width: 24, height: 24 },
}}
existing={null}
draft=""
notes={[]}
onDraft={() => {}}
onAddDraft={() => {}}
onRemoveQueuedNote={() => {}}
onClose={() => {}}
onSaveComment={() => {}}
onSendBatch={() => {}}
onRemoveMember={() => {}}
sending={false}
t={((key: string) => String(key)) as never}
bounds={{ width: 640, height: 420 }}
/>,
);
const popover = screen.getByTestId('comment-popover');
expect(Number.parseFloat(popover.style.left)).toBeLessThanOrEqual(306);
expect(Number.parseFloat(popover.style.left)).toBeGreaterThanOrEqual(14);
});
it('disables the comment action for unchanged existing comments', () => {
const onSaveComment = vi.fn();
const { rerender } = render(
<BoardComposerPopover
target={elementTarget()}
existing={existingComment('区域放大')}
draft="区域放大"
notes={[]}
onDraft={() => {}}
onAddDraft={() => {}}
onRemoveQueuedNote={() => {}}
onClose={() => {}}
onSaveComment={onSaveComment}
onSendBatch={() => {}}
onRemoveMember={() => {}}
sending={false}
t={((key: string) => {
if (key === 'chat.comments.comment') return 'Comment';
if (key === 'chat.comments.sendToChat') return 'Send to chat';
if (key === 'common.delete') return 'Delete';
return String(key);
}) as never}
/>,
);
expect(screen.getByTestId('comment-popover-save').hasAttribute('disabled')).toBe(true);
rerender(
<BoardComposerPopover
target={elementTarget()}
existing={existingComment('区域放大')}
draft="区域放大一些"
notes={[]}
onDraft={() => {}}
onAddDraft={() => {}}
onRemoveQueuedNote={() => {}}
onClose={() => {}}
onSaveComment={onSaveComment}
onSendBatch={() => {}}
onRemoveMember={() => {}}
sending={false}
t={((key: string) => {
if (key === 'chat.comments.comment') return 'Comment';
if (key === 'chat.comments.sendToChat') return 'Send to chat';
if (key === 'common.delete') return 'Delete';
return String(key);
}) as never}
/>,
);
expect(screen.getByTestId('comment-popover-save').hasAttribute('disabled')).toBe(false);
});
});