| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- /**
- * Tests for PrinterInfoModal — focused on the CopyButton clipboard fallback
- * (#1174). Bambuddy is commonly deployed over plain HTTP on a LAN, where
- * `navigator.clipboard` is gated by the secure-context requirement and the
- * previous code (which only tried the modern API and silently swallowed the
- * failure) left both copy buttons inert.
- */
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
- import { screen, waitFor } from '@testing-library/react';
- import userEvent from '@testing-library/user-event';
- import { render } from '../utils';
- import { PrinterInfoModal } from '../../components/PrinterInfoModal';
- import type { Printer } from '../../api/client';
- function mockPrinter(): Printer {
- return {
- id: 1,
- name: 'Test P1S',
- serial_number: '01S00A123456789',
- ip_address: '192.168.1.42',
- access_code: '12345678',
- model: 'P1S',
- location: null,
- nozzle_count: 1,
- is_active: true,
- auto_archive: true,
- external_camera_url: null,
- external_camera_type: null,
- external_camera_enabled: false,
- camera_rotation: 0,
- plate_detection_enabled: false,
- created_at: '2026-01-01T00:00:00Z',
- updated_at: '2026-01-01T00:00:00Z',
- };
- }
- function getCopyButtons() {
- // CopyButton renders as an icon button with title="Copy to clipboard".
- return screen.getAllByRole('button').filter(
- btn => /copy/i.test(btn.getAttribute('title') || ''),
- );
- }
- describe('PrinterInfoModal — CopyButton clipboard fallback (#1174)', () => {
- let originalIsSecureContext: PropertyDescriptor | undefined;
- let originalClipboard: PropertyDescriptor | undefined;
- let originalExecCommand: typeof document.execCommand;
- beforeEach(() => {
- originalIsSecureContext = Object.getOwnPropertyDescriptor(window, 'isSecureContext');
- originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard');
- originalExecCommand = document.execCommand;
- });
- afterEach(() => {
- if (originalIsSecureContext) {
- Object.defineProperty(window, 'isSecureContext', originalIsSecureContext);
- }
- if (originalClipboard) {
- Object.defineProperty(navigator, 'clipboard', originalClipboard);
- }
- document.execCommand = originalExecCommand;
- vi.clearAllMocks();
- });
- it('uses navigator.clipboard.writeText in a secure context (HTTPS / localhost)', async () => {
- const user = userEvent.setup();
- const writeTextMock = vi.fn().mockResolvedValue(undefined);
- Object.defineProperty(window, 'isSecureContext', { value: true, configurable: true });
- Object.defineProperty(navigator, 'clipboard', {
- value: { writeText: writeTextMock },
- configurable: true,
- });
- render(<PrinterInfoModal printer={mockPrinter()} onClose={() => {}} />);
- const buttons = getCopyButtons();
- expect(buttons.length).toBeGreaterThanOrEqual(2); // serial + ip at minimum
- // Click the first copy button (IP address row appears first).
- await user.click(buttons[0]);
- await waitFor(() => {
- expect(writeTextMock).toHaveBeenCalled();
- });
- // The value passed must be a real string from the printer fixture, not "".
- expect(writeTextMock.mock.calls[0][0]).toMatch(/^(192\.168\.1\.42|01S00A123456789)$/);
- });
- it('falls back to execCommand("copy") on plain-HTTP LAN deployments — pre-fix #1174 path', async () => {
- // Repro of the reporter's exact environment: plain-HTTP, no clipboard API.
- // Pre-fix the catch-block silently swallowed the TypeError on
- // navigator.clipboard.writeText and the icon never flipped to the tick.
- const user = userEvent.setup();
- Object.defineProperty(window, 'isSecureContext', { value: false, configurable: true });
- Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true });
- const execCommandMock = vi.fn().mockReturnValue(true);
- document.execCommand = execCommandMock;
- render(<PrinterInfoModal printer={mockPrinter()} onClose={() => {}} />);
- const buttons = getCopyButtons();
- await user.click(buttons[0]);
- await waitFor(() => {
- expect(execCommandMock).toHaveBeenCalledWith('copy');
- });
- // Off-screen textarea must be cleaned up on the success path; otherwise
- // every click would leak a hidden DOM node.
- expect(document.querySelectorAll('textarea').length).toBe(0);
- });
- it('cleans up the off-screen textarea even when execCommand throws', async () => {
- // The fallback path uses a try/finally around execCommand. The finally
- // block must remove the textarea even if the browser rejects the copy
- // (e.g. permission denied), so a hostile / restricted environment doesn't
- // leak DOM nodes per click.
- const user = userEvent.setup();
- Object.defineProperty(window, 'isSecureContext', { value: false, configurable: true });
- Object.defineProperty(navigator, 'clipboard', { value: undefined, configurable: true });
- document.execCommand = vi.fn().mockImplementation(() => {
- throw new Error('synthetic execCommand failure');
- });
- render(<PrinterInfoModal printer={mockPrinter()} onClose={() => {}} />);
- const buttons = getCopyButtons();
- await user.click(buttons[0]);
- await waitFor(() => {
- expect(document.querySelectorAll('textarea').length).toBe(0);
- });
- });
- });
|