fcd9470dcf
增加桌面端全站客服反馈悬浮入口。 悬停入口时展示 QQ 和微信社区二维码浮窗。 补充入口拖拽边界、点击跳转和移动端隐藏测试。 同步全站客服悬浮入口接入文档。
194 lines
5.6 KiB
TypeScript
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);
|
|
});
|