Files
Genarrative/src/components/common/FloatingFeedbackEntry.test.tsx
T
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

208 lines
5.6 KiB
TypeScript

/* @vitest-environment jsdom */
import { fireEvent, render, screen } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { FloatingFeedbackEntry } from './FloatingFeedbackEntry';
import {
clampFloatingFeedbackPosition,
FLOATING_FEEDBACK_FORM_URL,
resolveFloatingFeedbackContactPanelOffset,
resolveInitialFloatingFeedbackPosition,
} from './floatingFeedbackEntryModel';
function mockMatchMedia(matches: boolean) {
Object.defineProperty(window, 'matchMedia', {
configurable: true,
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}
function mockViewport(width: number, height: number) {
Object.defineProperty(window, 'innerWidth', {
configurable: true,
writable: true,
value: width,
});
Object.defineProperty(window, 'innerHeight', {
configurable: true,
writable: true,
value: height,
});
}
afterEach(() => {
document.body.innerHTML = '';
vi.restoreAllMocks();
});
test('desktop renders floating feedback entry near lower left', () => {
mockMatchMedia(true);
mockViewport(1440, 900);
render(<FloatingFeedbackEntry />);
const button = screen.getByRole('button', { name: '反馈点我' });
const shell = button.closest('.floating-feedback-entry-shell') as HTMLElement;
expect(button.className).toContain('floating-feedback-entry');
expect(shell.style.left).toBe('24px');
expect(shell.style.top).toBe('746.4px');
expect(button.querySelector('img')?.getAttribute('src')).toBe(
'/branding/taonier-feedback-entry.png',
);
});
test('hover shows contact QR images and mouse leave hides them', () => {
mockMatchMedia(true);
mockViewport(1440, 900);
render(<FloatingFeedbackEntry />);
const button = screen.getByRole('button', { name: '反馈点我' });
expect(
document.querySelector('.floating-feedback-entry-contact-panel'),
).toBeNull();
fireEvent.pointerEnter(button);
const panel = document.querySelector(
'.floating-feedback-entry-contact-panel',
);
expect(panel).not.toBeNull();
expect(
(panel as HTMLElement).style.getPropertyValue(
'--floating-feedback-contact-offset',
),
).toBe('141.2px');
const images = Array.from(panel?.querySelectorAll('img') ?? []);
expect(images.map((image) => image.getAttribute('src'))).toEqual([
'/branding/taonier-feedback-qq.png',
'/branding/taonier-feedback-wechat.png',
]);
fireEvent.pointerLeave(button);
expect(
document.querySelector('.floating-feedback-entry-contact-panel'),
).toBeNull();
});
test('mobile does not render feedback entry', () => {
mockMatchMedia(false);
mockViewport(390, 780);
render(<FloatingFeedbackEntry />);
expect(screen.queryByRole('button', { name: '反馈点我' })).toBeNull();
});
test('click opens feedback form in a new page', () => {
mockMatchMedia(true);
mockViewport(1440, 900);
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
render(<FloatingFeedbackEntry />);
fireEvent.click(screen.getByRole('button', { name: '反馈点我' }));
expect(openSpy).toHaveBeenCalledWith(
FLOATING_FEEDBACK_FORM_URL,
'_blank',
'noopener,noreferrer',
);
});
test('hover contact panel does not block click opening feedback form', () => {
mockMatchMedia(true);
mockViewport(1440, 900);
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
render(<FloatingFeedbackEntry />);
const button = screen.getByRole('button', { name: '反馈点我' });
fireEvent.pointerEnter(button);
fireEvent.click(button);
expect(
document.querySelector('.floating-feedback-entry-contact-panel'),
).not.toBeNull();
expect(openSpy).toHaveBeenCalledWith(
FLOATING_FEEDBACK_FORM_URL,
'_blank',
'noopener,noreferrer',
);
});
test('drag keeps entry fully inside the viewport and does not open form', () => {
mockMatchMedia(true);
mockViewport(360, 320);
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
render(<FloatingFeedbackEntry />);
const button = screen.getByRole('button', { name: '反馈点我' });
fireEvent.mouseEnter(button);
expect(
document.querySelector('.floating-feedback-entry-contact-panel'),
).not.toBeNull();
fireEvent.mouseDown(button, {
button: 0,
clientX: 40,
clientY: 220,
});
expect(
document.querySelector('.floating-feedback-entry-contact-panel'),
).toBeNull();
fireEvent.mouseMove(window, {
clientX: 900,
clientY: 900,
});
fireEvent.mouseUp(window, {
clientX: 900,
clientY: 900,
});
expect(
document.querySelector('.floating-feedback-entry-contact-panel'),
).not.toBeNull();
fireEvent.click(button);
const shell = button.closest('.floating-feedback-entry-shell') as HTMLElement;
expect(shell.style.left).toBe('282.4px');
expect(shell.style.top).toBe('242.4px');
expect(openSpy).not.toHaveBeenCalled();
});
test('floating feedback position helpers clamp to visible viewport', () => {
expect(
clampFloatingFeedbackPosition(
{ left: -100, top: 1000 },
{ width: 360, height: 320 },
),
).toEqual({ left: 20, top: 242.4 });
expect(
resolveInitialFloatingFeedbackPosition({ width: 1440, height: 900 }),
).toEqual({ left: 24, top: 746.4 });
expect(
resolveFloatingFeedbackContactPanelOffset(
{ left: 20, top: 100 },
{ width: 1440 },
),
).toBe(145.2);
expect(
resolveFloatingFeedbackContactPanelOffset(
{ left: 20, top: 100 },
{ width: 360 },
),
).toBe(0);
});