/** * Unit tests for ContactLinks: phone/Telegram display, labels, layout. * Ported from webapp/js/contactHtml.test.js buildContactLinksHtml. */ import { describe, it, expect, beforeEach, vi } from "vitest"; import { render, screen, fireEvent, act } from "@testing-library/react"; import { ContactLinks } from "./ContactLinks"; import { TooltipProvider } from "@/components/ui/tooltip"; import { resetAppStore } from "@/test/test-utils"; function renderWithTooltip(ui: React.ReactElement) { return render({ui}); } const openPhoneLinkMock = vi.fn(); const openTelegramProfileMock = vi.fn(); const triggerHapticLightMock = vi.fn(); const copyToClipboardMock = vi.fn(); vi.mock("@/lib/open-phone-link", () => ({ openPhoneLink: (...args: unknown[]) => openPhoneLinkMock(...args), })); vi.mock("@/lib/telegram-link", () => ({ openTelegramProfile: (...args: unknown[]) => openTelegramProfileMock(...args), })); vi.mock("@/lib/telegram-haptic", () => ({ triggerHapticLight: () => triggerHapticLightMock(), })); vi.mock("@/lib/copy-to-clipboard", () => ({ copyToClipboard: (...args: unknown[]) => copyToClipboardMock(...args), })); describe("ContactLinks", () => { beforeEach(() => { resetAppStore(); openPhoneLinkMock.mockClear(); openTelegramProfileMock.mockClear(); triggerHapticLightMock.mockClear(); copyToClipboardMock.mockClear(); }); it("returns null when phone and username are missing", () => { const { container } = render( ); expect(container.firstChild).toBeNull(); }); it("renders phone only with label and tel: link", () => { render(); expect(document.querySelector('a[href^="tel:"]')).toBeInTheDocument(); expect(screen.getByText(/Phone/i)).toBeInTheDocument(); }); it("displays phone formatted for Russian numbers", () => { render(); expect(screen.getByText(/\+7 914 652-22-09/)).toBeInTheDocument(); }); it("renders username only with label and t.me link", () => { render(); expect(document.querySelector('a[href*="t.me"]')).toBeInTheDocument(); expect(screen.getByText(/alice_dev/)).toBeInTheDocument(); }); it("renders both phone and username with labels", () => { render( ); expect(document.querySelector('a[href^="tel:"]')).toBeInTheDocument(); expect(document.querySelector('a[href*="t.me"]')).toBeInTheDocument(); expect(screen.getByText(/\+7 900 111-22-33/)).toBeInTheDocument(); expect(screen.getByText(/@bob/)).toBeInTheDocument(); }); it("strips leading @ from username and displays with @", () => { render(); const link = document.querySelector('a[href*="t.me/alice"]'); expect(link).toBeInTheDocument(); expect(link?.textContent).toContain("@alice"); }); it("calls openPhoneLink and triggerHapticLight when phone link is clicked", () => { render( ); const telLink = document.querySelector('a[href^="tel:"]'); expect(telLink).toBeInTheDocument(); fireEvent.click(telLink!); expect(openPhoneLinkMock).toHaveBeenCalledWith("+79991234567"); expect(triggerHapticLightMock).toHaveBeenCalled(); }); it("calls openTelegramProfile and triggerHapticLight when Telegram link is clicked", () => { render( ); const tgLink = document.querySelector('a[href*="t.me"]'); expect(tgLink).toBeInTheDocument(); fireEvent.click(tgLink!); expect(openTelegramProfileMock).toHaveBeenCalledWith("alice_dev"); expect(triggerHapticLightMock).toHaveBeenCalled(); }); describe("showCopyButtons with layout block", () => { it("renders copy phone button with aria-label when phone is present", () => { renderWithTooltip( ); expect( screen.getByRole("button", { name: /Copy phone number|Скопировать номер/i }) ).toBeInTheDocument(); }); it("renders copy Telegram button with aria-label when username is present", () => { renderWithTooltip( ); expect( screen.getByRole("button", { name: /Copy Telegram username|Скопировать логин Telegram/i, }) ).toBeInTheDocument(); }); it("calls copyToClipboard with raw phone and triggerHapticLight when copy phone is clicked", async () => { copyToClipboardMock.mockResolvedValue(true); renderWithTooltip( ); const copyBtn = screen.getByRole("button", { name: /Copy phone number|Скопировать номер/i, }); await act(async () => { fireEvent.click(copyBtn); }); expect(copyToClipboardMock).toHaveBeenCalledWith("+79991234567"); expect(triggerHapticLightMock).toHaveBeenCalled(); }); it("calls copyToClipboard with @username when copy Telegram is clicked", async () => { copyToClipboardMock.mockResolvedValue(true); renderWithTooltip( ); const copyBtn = screen.getByRole("button", { name: /Copy Telegram username|Скопировать логин Telegram/i, }); await act(async () => { fireEvent.click(copyBtn); }); expect(copyToClipboardMock).toHaveBeenCalledWith("@alice_dev"); expect(triggerHapticLightMock).toHaveBeenCalled(); }); it("shows Copied text in tooltip after successful copy", async () => { copyToClipboardMock.mockResolvedValue(true); renderWithTooltip( ); const copyBtn = screen.getByRole("button", { name: /Copy phone number|Скопировать номер/i, }); await act(async () => { fireEvent.click(copyBtn); }); const tooltip = await screen.findByRole("tooltip", { name: /Copied|Скопировано/i, }); expect(tooltip).toBeInTheDocument(); }); it("does not show copy buttons when showCopyButtons is false", () => { render( ); expect( screen.queryByRole("button", { name: /Copy phone number|Скопировать номер/i }) ).not.toBeInTheDocument(); expect( screen.queryByRole("button", { name: /Copy Telegram username|Скопировать логин Telegram/i }) ).not.toBeInTheDocument(); }); }); });