Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
65 changes: 64 additions & 1 deletion src/copy-to-clipboard/__tests__/copy-to-clipboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@ const defaultProps = {

describe('CopyToClipboard', () => {
const originalNavigatorClipboard = global.navigator.clipboard;
const originalNavigatorPermissions = global.navigator.permissions;

beforeEach(() => {
Object.assign(global.navigator, {
clipboard: {
writeText: (text: string) =>
new Promise<void>((resolve, reject) => (text.includes('error') ? reject() : resolve())),
},
permissions: {
query: jest.fn().mockResolvedValue({ state: 'granted' }),
},
});
});

afterEach(() => {
Object.assign(global.navigator, { clipboard: originalNavigatorClipboard });
Object.assign(global.navigator, {
clipboard: originalNavigatorClipboard,
permissions: originalNavigatorPermissions,
});
});

test('renders a normal button with button text and aria-label and no text to copy', () => {
Expand Down Expand Up @@ -220,4 +227,60 @@ describe('CopyToClipboard', () => {
});
});
});

describe('permissions API behavior', () => {
test('shows error state when clipboard-write permission is denied', async () => {
Object.assign(global.navigator, {
permissions: {
query: jest.fn().mockResolvedValue({ state: 'denied' }),
},
});

const { container } = render(<CopyToClipboard {...defaultProps} />);
const wrapper = createWrapper(container).findCopyToClipboard()!;

wrapper.findCopyButton().click();
await waitFor(() =>
expect(wrapper.findStatusText()!.getElement().textContent).toBe('Failed to copy to clipboard')
);
});

test('shows success state when clipboard-write permission is granted', async () => {
Object.assign(global.navigator, {
permissions: {
query: jest.fn().mockResolvedValue({ state: 'granted' }),
},
});

const { container } = render(<CopyToClipboard {...defaultProps} />);
const wrapper = createWrapper(container).findCopyToClipboard()!;

wrapper.findCopyButton().click();
await waitFor(() => expect(wrapper.findStatusText()!.getElement().textContent).toBe('Copied to clipboard'));
});

test('defaults to success state when permissions API is not available', async () => {
Object.assign(global.navigator, { permissions: undefined });

const { container } = render(<CopyToClipboard {...defaultProps} />);
const wrapper = createWrapper(container).findCopyToClipboard()!;

wrapper.findCopyButton().click();
await waitFor(() => expect(wrapper.findStatusText()!.getElement().textContent).toBe('Copied to clipboard'));
});

test('defaults to success state when permissions query fails', async () => {
Object.assign(global.navigator, {
permissions: {
query: jest.fn().mockRejectedValue(new Error('Permission query failed')),
},
});

const { container } = render(<CopyToClipboard {...defaultProps} />);
const wrapper = createWrapper(container).findCopyToClipboard()!;

wrapper.findCopyButton().click();
await waitFor(() => expect(wrapper.findStatusText()!.getElement().textContent).toBe('Copied to clipboard'));
});
});
});
24 changes: 19 additions & 5 deletions src/copy-to-clipboard/internal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import clsx from 'clsx';

import InternalButton from '../button/internal';
Expand Down Expand Up @@ -29,8 +29,24 @@ export default function InternalCopyToClipboard({
__internalRootRef,
...restProps
}: InternalCopyToClipboardProps) {
const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending');
const [statusText, setStatusText] = useState('');
const [status, setStatus] = useState<'pending' | 'success' | 'error'>('success');
const [statusText, setStatusText] = useState(copySuccessText);

useEffect(() => {
if (navigator.permissions) {
navigator.permissions
.query({ name: 'clipboard-write' as PermissionName })
.then(result => {
if (result.state === 'denied') {
setStatus('error');
setStatusText(copyErrorText);
}
})
.catch(() => {
// Permissions API not supported or failed.
});
}
}, [copyErrorText]);

const baseProps = getBaseProps(restProps);
const onClick = () => {
Expand All @@ -41,8 +57,6 @@ export default function InternalCopyToClipboard({
return;
}

setStatus('pending');
setStatusText('');
navigator.clipboard
.writeText(textToCopy)
.then(() => {
Expand Down