Files
Genarrative/src/components/common/FloatingFeedbackEntry.test.tsx
T
wuxiangwanzi fcd9470dcf 加入客服反馈和社区二维码
增加桌面端全站客服反馈悬浮入口。

悬停入口时展示 QQ 和微信社区二维码浮窗。

补充入口拖拽边界、点击跳转和移动端隐藏测试。

同步全站客服悬浮入口接入文档。
2026-06-23 21:02:13 +08:00

194 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);
});