import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { FirmwareErrorView } from '@/components/firmware/firmware-error-view'; import type { FirmwareProgressEvent, FirmwareReason } from '@/types/device'; /** * M9-4 第 2 輪修改補測(Reviewer M5): * 驗證 destructive vs recoverable reason 在 FirmwareErrorView 的 UI 差異。 * * 涵蓋: * - destructive reason 顯示 brick warning * - destructive reason 不顯示 Retry 按鈕、改顯示 ContactSupport 按鈕(enabled、Reviewer M6 已修) * - recoverable reason 顯示 Retry 按鈕(onRetry 有傳) * - errorCode 顯示 * - 技術資訊 collapsible 預設收合 * - ContactSupport 按鈕點擊會 trigger window.open(mailto:) */ function makeEvent(overrides: Partial = {}): FirmwareProgressEvent { return { type: 'firmware_progress', deviceId: 'dev-1', stage: 'error', percent: -1, elapsedMs: 12000, errorCode: 'FW_E102', rawError: 'kp_update_kdp_firmware: timeout', ...overrides, }; } beforeEach(() => { // window.open mock — JSDOM 預設有 window.open、但回傳 null(不會真開新分頁)。 vi.spyOn(window, 'open').mockImplementation(() => null); }); describe('FirmwareErrorView — destructive reasons (brick warning + ContactSupport)', () => { const destructiveReasons: FirmwareReason[] = [ 'disconnect_during_op', 'verify_mismatch', 'verify_not_found', ]; it.each(destructiveReasons)( 'reason=%s 顯示 brick warning + ContactSupport(不顯示 Retry)', (reason) => { const onClose = vi.fn(); const onRetry = vi.fn(); render( , ); // brick warning(role="note") const note = screen.getByRole('note'); expect(note.textContent).toMatch(/損毀|damaged/i); // 不顯示 Retry / ReplugRetry / Rescan 按鈕 // (ContactSupport 為 destructive variant、唯一 primary action) const buttons = screen.getAllByRole('button'); const labels = buttons.map((b) => b.textContent || ''); expect(labels.some((l) => /Retry|重試/.test(l))).toBe(false); expect(labels.some((l) => /Contact|聯絡技術支援|Support/.test(l))).toBe(true); }, ); it('ContactSupport 按鈕點擊會開 mailto: handler(不再 disabled)', () => { const onClose = vi.fn(); render( , ); const contactBtn = screen.getByRole('button', { name: /Contact|聯絡技術支援|Support/ }); expect((contactBtn as HTMLButtonElement).disabled).toBe(false); fireEvent.click(contactBtn); expect(window.open).toHaveBeenCalled(); const [href] = (window.open as ReturnType).mock.calls[0]; expect(typeof href).toBe('string'); expect(href).toMatch(/^mailto:/); // body 應該帶上技術資訊 expect(decodeURIComponent(href)).toContain('stage: error'); expect(decodeURIComponent(href)).toContain('reason: verify_mismatch'); }); }); describe('FirmwareErrorView — recoverable reasons show Retry button', () => { it('reason=connect_failed 顯示 Retry 按鈕、不顯示 brick warning', () => { const onClose = vi.fn(); const onRetry = vi.fn(); render( , ); // 沒有 brick warning role=note expect(screen.queryByRole('note')).toBeNull(); // 顯示 Retry primary action 按鈕 const retryBtn = screen.getByRole('button', { name: /Retry|重試|插拔/ }); expect(retryBtn).toBeTruthy(); fireEvent.click(retryBtn); expect(onRetry).toHaveBeenCalledTimes(1); }); it('reason=scan_not_found 顯示 ReplugRetry 動作', () => { const onClose = vi.fn(); const onRetry = vi.fn(); render( , ); const btn = screen.getByRole('button', { name: /插拔|Unplug and retry/ }); expect(btn).toBeTruthy(); }); }); describe('FirmwareErrorView — common UI elements', () => { it('顯示 errorCode(mono 字型小字)', () => { render( , ); // errorCode 同時出現在「錯誤代碼」

與技術資訊

、用 getAllByText 確保至少 1 個。
    expect(screen.getAllByText(/FW_E102/).length).toBeGreaterThanOrEqual(1);
  });

  it('技術資訊 collapsible 預設收合', () => {
    const { container } = render(
      ,
    );
    const details = container.querySelector('details');
    expect(details).not.toBeNull();
    expect((details as HTMLDetailsElement).open).toBe(false);
  });

  it('Close 按鈕觸發 onClose callback', () => {
    const onClose = vi.fn();
    render(
      ,
    );
    const closeBtn = screen.getByRole('button', { name: /Close|關閉/ });
    fireEvent.click(closeBtn);
    expect(onClose).toHaveBeenCalledTimes(1);
  });
});