mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* feat(web): queue chat sends * feat(web): render code comment directives * feat(web): add preview comments and manual edits * fix(web): polish shared chrome controls * fix(web): align queued send loading state * feat(web): open primary project artifacts * fix(web): keep queued sends and tests aligned * fix(web): restore docked comment tools layout * fix(web): align preview comment toolbar * fix(web): place local cli beside handoff * fix(web): move agent menu beside handoff * fix(web): make project instructions a direct header action * fix(web): compact handoff and toolbar labels * fix(web): clarify handoff menu and annotation label * fix(web): restore compact cursor handoff trigger * fix(web): align agent menu trigger with handoff * fix(web): add draw toolbar close action * fix(web): move inspect editing into edit mode * fix(web): avoid reserving comment sidebar in annotation mode * fix(web): float preview comments panel * fix(web): keep edit canvas full width * fix(web): polish preview annotation tools * fix(web): highlight active preview comments * fix(web): open comments panel after annotation save * fix(web): polish comment handoff controls * fix(web): remove palette preview tool * fix(web): simplify draw annotation toolbar * fix(web): restore queued tasks into composer * fix(web): restore queued send strip styling * fix(web): hide internal comment target ids * fix(web): align manual edit panel header * test(web): cover visual interaction contracts * fix(web): address PR feedback regressions * fix(web): preserve artifact chrome state * fix(daemon): restore project raw file routes --------- Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local> Co-authored-by: mrcfps <mrc@powerformer.com>
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
// @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 { 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 renderPopover(overrides: {
|
|
target: PreviewCommentSnapshot;
|
|
onHoverMember?: (elementId: string | null) => void;
|
|
}) {
|
|
return render(
|
|
<BoardComposerPopover
|
|
target={overrides.target}
|
|
existing={null}
|
|
draft=""
|
|
notes={[]}
|
|
onDraft={() => {}}
|
|
onAddDraft={() => {}}
|
|
onRemoveQueuedNote={() => {}}
|
|
onClose={() => {}}
|
|
onSaveComment={() => {}}
|
|
onSendBatch={() => {}}
|
|
onRemoveMember={() => {}}
|
|
onHoverMember={overrides.onHoverMember}
|
|
sending={false}
|
|
t={((key: string) => String(key)) as never}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
function chipFor(label: string): HTMLElement {
|
|
const chip = screen.getByText(label).closest('.board-pod-chip');
|
|
if (!chip) throw new Error(`chip for ${label} not rendered`);
|
|
return chip as HTMLElement;
|
|
}
|
|
|
|
describe('BoardComposerPopover captured-chip hover', () => {
|
|
it('reports the elementId when the pointer enters a chip and clears on leave', () => {
|
|
const onHoverMember = vi.fn();
|
|
renderPopover({
|
|
target: podTarget([member('alpha', 'Alpha'), member('beta', 'Beta')]),
|
|
onHoverMember,
|
|
});
|
|
|
|
fireEvent.pointerEnter(chipFor('Alpha'));
|
|
expect(onHoverMember).toHaveBeenLastCalledWith('alpha');
|
|
|
|
fireEvent.pointerLeave(chipFor('Alpha'));
|
|
expect(onHoverMember).toHaveBeenLastCalledWith(null);
|
|
});
|
|
|
|
it('reports the elementId from keyboard focus on the chip remove button', () => {
|
|
const onHoverMember = vi.fn();
|
|
renderPopover({
|
|
target: podTarget([member('alpha', 'Alpha'), member('beta', 'Beta')]),
|
|
onHoverMember,
|
|
});
|
|
|
|
const betaRemove = within(chipFor('Beta')).getByRole('button');
|
|
fireEvent.focus(betaRemove);
|
|
expect(onHoverMember).toHaveBeenLastCalledWith('beta');
|
|
|
|
fireEvent.blur(betaRemove);
|
|
expect(onHoverMember).toHaveBeenLastCalledWith(null);
|
|
});
|
|
|
|
it('ignores pointer events from touch and pen so a tap on a chip does not flicker the highlight', () => {
|
|
const onHoverMember = vi.fn();
|
|
renderPopover({
|
|
target: podTarget([member('alpha', 'Alpha')]),
|
|
onHoverMember,
|
|
});
|
|
|
|
fireEvent.pointerEnter(chipFor('Alpha'), { pointerType: 'touch' });
|
|
fireEvent.pointerLeave(chipFor('Alpha'), { pointerType: 'touch' });
|
|
fireEvent.pointerEnter(chipFor('Alpha'), { pointerType: 'pen' });
|
|
fireEvent.pointerLeave(chipFor('Alpha'), { pointerType: 'pen' });
|
|
expect(onHoverMember).not.toHaveBeenCalled();
|
|
|
|
fireEvent.pointerEnter(chipFor('Alpha'), { pointerType: 'mouse' });
|
|
expect(onHoverMember).toHaveBeenLastCalledWith('alpha');
|
|
});
|
|
|
|
it('does not throw when onHoverMember is omitted', () => {
|
|
expect(() =>
|
|
renderPopover({
|
|
target: podTarget([member('alpha', 'Alpha')]),
|
|
}),
|
|
).not.toThrow();
|
|
|
|
expect(() => fireEvent.pointerEnter(chipFor('Alpha'))).not.toThrow();
|
|
expect(() => fireEvent.pointerLeave(chipFor('Alpha'))).not.toThrow();
|
|
});
|
|
});
|