Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4549c99
Initial plan
Copilot Jan 5, 2026
de9324c
Unify clipboard implementations with fallback for non-HTTPS/non-local…
Copilot Jan 5, 2026
12f2d90
Address code review feedback for clipboard utility
Copilot Jan 5, 2026
c552e5c
Refactor clipboard tests to use vitest mocking best practices
Copilot Jan 5, 2026
5d8b3d8
Add useCopyToClipboard hook to eliminate clipboard code duplication
Copilot Jan 5, 2026
e5c383c
Address code review feedback - add cleanup effect and fix linting
Copilot Jan 5, 2026
250384e
Simplify useCopyToClipboard hook per review feedback
Copilot Jan 5, 2026
84d40a6
Fix toast message consistency - add 'to clipboard' suffix
Copilot Jan 5, 2026
c246fd4
Update martin/martin-ui/__tests__/hooks/use-copy-to-clipboard.test.tsx
CommanderStorm Jan 5, 2026
4c43d03
Replace copiedText with copied boolean and add CopyCheck icons
Copilot Jan 5, 2026
bc4fafe
Merge branch 'main' into copilot/fix-clipboard-copy-functionality
CommanderStorm Jan 6, 2026
e08f47d
Apply suggestions from code review
CommanderStorm Jan 6, 2026
675015b
Update martin/martin-ui/src/components/sprite/SpriteCanvas.tsx
CommanderStorm Jan 6, 2026
5c43c6a
chore(fmt): apply pre-commit formatting fixes
pre-commit-ci[bot] Jan 6, 2026
b6ff3e3
Update martin/martin-ui/src/components/sprite/SpriteCanvas.tsx
CommanderStorm Jan 6, 2026
9698664
Fix sprite-download showing copied state for all items
Copilot Jan 6, 2026
43a038d
polish the way the interactions work a bit
CommanderStorm Jan 6, 2026
3a72895
chore(fmt): apply pre-commit formatting fixes
pre-commit-ci[bot] Jan 6, 2026
3971752
remove unused imports
CommanderStorm Jan 6, 2026
54c82e9
Update martin/martin-ui/src/components/ui/tooltip-copy-text.tsx
CommanderStorm Jan 6, 2026
aeccb4f
increase readability slightly
CommanderStorm Jan 6, 2026
a550902
update snaps
CommanderStorm Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ exports[`SpriteCatalog Component > matches snapshot for loaded state with mock d
map-icons
</div>
<div
class="text-xs pt-3 flex flex-row text-slate-400 p-0.5"
class="text-xs pt-3 flex flex-row gap-1 text-slate-400 p-0.5"
>
<span
role="img"
Expand Down Expand Up @@ -228,7 +228,7 @@ exports[`SpriteCatalog Component > matches snapshot for loaded state with mock d
transportation
</div>
<div
class="text-xs pt-3 flex flex-row text-slate-400 p-0.5"
class="text-xs pt-3 flex flex-row gap-1 text-slate-400 p-0.5"
>
<span
role="img"
Expand Down Expand Up @@ -390,7 +390,7 @@ exports[`SpriteCatalog Component > matches snapshot for loaded state with mock d
ui-elements
</div>
<div
class="text-xs pt-3 flex flex-row text-slate-400 p-0.5"
class="text-xs pt-3 flex flex-row gap-1 text-slate-400 p-0.5"
>
<span
role="img"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ exports[`InlineErrorState Component > renders with retry button when onRetry is
</span>
</div>
<button
class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-hidden focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 border bg-background shadow-2xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-hidden focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-8 rounded-md px-3 has-[>svg]:px-2.5 border bg-background shadow-2xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 gap-1"
data-slot="button"
>
<span
Expand Down Expand Up @@ -445,7 +445,7 @@ exports[`InlineErrorState Component > shows retrying state when isRetrying is tr
</span>
</div>
<button
class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-hidden focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5 border bg-background shadow-2xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
class="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-hidden focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-8 rounded-md px-3 has-[>svg]:px-2.5 border bg-background shadow-2xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 gap-1"
data-slot="button"
disabled=""
>
Expand Down
72 changes: 72 additions & 0 deletions martin/martin-ui/__tests__/hooks/use-copy-to-clipboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mockToast = vi.fn();
vi.mock('@/hooks/use-toast', () => ({
useToast: () => ({ toast: mockToast }),
}));

const mockCopyToClipboard = vi.fn();
vi.mock('@/lib/utils', () => ({
copyToClipboard: (text: string) => mockCopyToClipboard(text),
}));

const { useCopyToClipboard } = await import('@/hooks/use-copy-to-clipboard');

describe('useCopyToClipboard', () => {
beforeEach(() => {
vi.clearAllMocks();
mockCopyToClipboard.mockResolvedValue(undefined);
});

it('copies text and shows toast', async () => {
const { result } = renderHook(() => useCopyToClipboard());

expect(result.current.copied).toBe(false);

let success: boolean | undefined;
await act(async () => {
success = await result.current.copy('test text');
});

expect(success).toBe(true);
expect(result.current.copied).toBe(true);
expect(mockCopyToClipboard).toHaveBeenCalledWith('test text');
expect(mockToast).toHaveBeenCalledWith({
description: 'Copied!',
title: 'Copied!',
});
});

it('uses custom success message from options', async () => {
const { result } = renderHook(() => useCopyToClipboard({ successMessage: 'URL copied!' }));

await act(async () => {
await result.current.copy('http://example.com');
});

expect(mockToast).toHaveBeenCalledWith({
description: 'URL copied!',
title: 'Copied!',
});
});

it('handles errors and shows error toast', async () => {
mockCopyToClipboard.mockRejectedValue(new Error('Copy failed'));

const { result } = renderHook(() => useCopyToClipboard());

let success: boolean | undefined;
await act(async () => {
success = await result.current.copy('test text');
});

expect(success).toBe(false);
expect(result.current.copied).toBe(false);
expect(mockToast).toHaveBeenCalledWith({
description: 'Failed to copy to clipboard',
title: 'Error',
variant: 'destructive',
});
});
});
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the copiedValue state tracking. The hook exposes copiedValue which is critical for per-item copy state in sprite-download.tsx (checking if copiedValue === format.url), but there are no tests verifying this functionality. Tests should verify that copiedValue is set correctly and reset after the timeout.

Copilot uses AI. Check for mistakes.
60 changes: 58 additions & 2 deletions martin/martin-ui/__tests__/lib/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { formatFileSize } from '@/lib/utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { copyToClipboard, formatFileSize } from '@/lib/utils';

describe('formatFileSize', () => {
it("returns '0 Bytes' for 0", () => {
Expand Down Expand Up @@ -42,3 +42,59 @@ describe('formatFileSize', () => {
expect(formatFileSize(Number.MAX_SAFE_INTEGER)).toBe('File too large');
});
});

describe('copyToClipboard', () => {
const execCommandMock = vi.fn().mockReturnValue(true);
// Store reference to original execCommand if it exists
// biome-ignore lint/suspicious/noExplicitAny: jsdom doesn't define execCommand, we need to add it
const originalDocument = globalThis.document as any;

beforeEach(() => {
vi.clearAllMocks();
execCommandMock.mockReturnValue(true);
// Add execCommand to the document object (jsdom doesn't have this)
originalDocument.execCommand = execCommandMock;
});

afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
// Clean up execCommand
originalDocument.execCommand = undefined;
});

it('uses navigator.clipboard.writeText when available and succeeds', async () => {
const writeTextMock = vi.fn().mockResolvedValue(undefined);
vi.stubGlobal('navigator', {
clipboard: { writeText: writeTextMock },
});

await copyToClipboard('test text');
expect(writeTextMock).toHaveBeenCalledWith('test text');
});

it('falls back to execCommand when navigator.clipboard is unavailable', async () => {
vi.stubGlobal('navigator', { clipboard: undefined });

await copyToClipboard('fallback text');
expect(execCommandMock).toHaveBeenCalledWith('copy');
});

it('falls back to execCommand when navigator.clipboard.writeText fails', async () => {
const writeTextMock = vi.fn().mockRejectedValue(new Error('Clipboard API error'));
vi.stubGlobal('navigator', {
clipboard: { writeText: writeTextMock },
});

await copyToClipboard('test text');
expect(writeTextMock).toHaveBeenCalledWith('test text');
expect(execCommandMock).toHaveBeenCalledWith('copy');
});

it('throws when both clipboard API and execCommand fail', async () => {
vi.stubGlobal('navigator', { clipboard: undefined });
execCommandMock.mockReturnValue(false);

await expect(copyToClipboard('will fail')).rejects.toThrow('Copy command failed');
});
});
Loading
Loading