feat: add copy functionality for phone and Telegram username in ContactLinks component

- Implemented copy buttons for phone number and Telegram username in the ContactLinks component, enhancing user interaction.
- Integrated tooltip feedback to indicate successful copy actions.
- Updated tests to cover new copy functionality and ensure proper rendering of copy buttons based on props.
- Added localization support for new copy-related strings in the i18n messages.
This commit is contained in:
2026-03-06 18:55:56 +03:00
parent 34001d22d9
commit 24d6ecbedb
7 changed files with 388 additions and 20 deletions

View File

@@ -4,13 +4,19 @@
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
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(<TooltipProvider>{ui}</TooltipProvider>);
}
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),
@@ -21,6 +27,9 @@ vi.mock("@/lib/telegram-link", () => ({
vi.mock("@/lib/telegram-haptic", () => ({
triggerHapticLight: () => triggerHapticLightMock(),
}));
vi.mock("@/lib/copy-to-clipboard", () => ({
copyToClipboard: (...args: unknown[]) => copyToClipboardMock(...args),
}));
describe("ContactLinks", () => {
beforeEach(() => {
@@ -28,6 +37,7 @@ describe("ContactLinks", () => {
openPhoneLinkMock.mockClear();
openTelegramProfileMock.mockClear();
triggerHapticLightMock.mockClear();
copyToClipboardMock.mockClear();
});
it("returns null when phone and username are missing", () => {
@@ -100,4 +110,118 @@ describe("ContactLinks", () => {
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(
<ContactLinks
phone="+79991234567"
username={null}
layout="block"
showCopyButtons
/>
);
expect(
screen.getByRole("button", { name: /Copy phone number|Скопировать номер/i })
).toBeInTheDocument();
});
it("renders copy Telegram button with aria-label when username is present", () => {
renderWithTooltip(
<ContactLinks
phone={null}
username="alice_dev"
layout="block"
showCopyButtons
/>
);
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(
<ContactLinks
phone="+79991234567"
username={null}
layout="block"
showCopyButtons
/>
);
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(
<ContactLinks
phone={null}
username="alice_dev"
layout="block"
showCopyButtons
/>
);
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(
<ContactLinks
phone="+79991234567"
username={null}
layout="block"
showCopyButtons
/>
);
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(
<ContactLinks
phone="+79991234567"
username="bob"
layout="block"
showCopyButtons={false}
/>
);
expect(
screen.queryByRole("button", { name: /Copy phone number|Скопировать номер/i })
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /Copy Telegram username|Скопировать логин Telegram/i })
).not.toBeInTheDocument();
});
});
});