feat(chat): show playful teacher introductions on hover

This commit is contained in:
鲨鱼辣椒
2026-09-28 12:12:10 +08:00
parent b26e25c9ed
commit 772da4f8e4
16 changed files with 433 additions and 62 deletions

View File

@@ -6,19 +6,22 @@ import { usePreviewClearance } from '@/pages/Chat/use-preview-clearance';
let resize: () => void;
let bubbleRect: DOMRect;
let pageRect: DOMRect;
let introductionRect: DOMRect;
const bubble = document.createElement('div');
const introduction = document.createElement('div');
const rect = (x: number, y: number, width: number, height: number) => new DOMRect(x, y, width, height);
function Harness({ visible = true, open = true }: { visible?: boolean; open?: boolean }) {
function Harness({ visible = true, open = true, intro = false }: { visible?: boolean; open?: boolean; intro?: boolean }) {
const panel = useRef<HTMLDivElement>(null);
const viewport = useRef<HTMLDivElement>(null);
usePreviewClearance(panel, viewport, visible ? bubble : null, open);
usePreviewClearance(panel, viewport, visible ? bubble : null, open, intro ? introduction : null);
return <div ref={panel} data-testid="panel"><div ref={viewport} data-testid="viewport" /></div>;
}
beforeEach(() => {
bubbleRect = rect(960, 48, 340, 164);
pageRect = rect(192, 184, 1200, 600);
introductionRect = rect(950, 50, 304, 290);
vi.stubGlobal('ResizeObserver', class {
constructor(callback: () => void) { resize = callback; }
observe() {}
@@ -26,6 +29,7 @@ beforeEach(() => {
});
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
if (this === bubble) return bubbleRect;
if (this === introduction) return introductionRect;
const offset = Number.parseFloat(this.parentElement?.style.marginTop ?? '') || 0;
return rect(pageRect.x, pageRect.y + offset, pageRect.width, pageRect.height - offset);
});
@@ -73,3 +77,22 @@ it('removes clearance when the bubble is dismissed or Work closes', () => {
view.rerender(<Harness open={false} />);
expect(screen.getByTestId('panel').style.marginTop).toBe('');
});
it('clears a hovered introduction even when no proactive bubble exists', () => {
const view = render(<Harness visible={false} intro />);
expect(screen.getByTestId('panel').style.marginTop).toBe('164px');
act(() => { resize(); resize(); });
expect(screen.getByTestId('panel').style.marginTop).toBe('164px');
view.rerender(<Harness visible={false} intro={false} />);
expect(screen.getByTestId('panel').style.marginTop).toBe('');
});
it('keeps the deeper overlapping surface clear without adding both heights', () => {
const view = render(<Harness intro />);
expect(screen.getByTestId('panel').style.marginTop).toBe('164px');
introductionRect = rect(950, 50, 304, 100);
act(() => resize());
expect(screen.getByTestId('panel').style.marginTop).toBe('36px');
view.rerender(<Harness intro={false} />);
expect(screen.getByTestId('panel').style.marginTop).toBe('36px');
});