This commit is contained in:
2026-07-12 12:20:23 +08:00
parent 7f2d5579ff
commit f923791d39
252 changed files with 9872 additions and 647 deletions
@@ -0,0 +1,37 @@
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: inline-block;
direction: ltr;
font-family: var(--mdc-icon-font, 'Material Icons');
font-feature-settings: 'liga';
font-size: var(--mdc-icon-size, 24px);
font-style: normal;
font-weight: normal;
letter-spacing: normal;
line-height: 1;
text-rendering: optimizeLegibility;
text-transform: none;
white-space: nowrap;
word-wrap: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
<slot></slot>
`;
class MaterialIconElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }).appendChild(
template.content.cloneNode(true)
);
}
}
if (!customElements.get('mwc-icon')) {
customElements.define('mwc-icon', MaterialIconElement);
}
@@ -6,15 +6,45 @@
* @throws {Error} unable to copy text to clipboard
*/
export const copyTextToClipboard = async data => {
const text =
typeof data === 'object' && data !== null
? JSON.stringify(data, null, 2)
: String(data ?? '');
let clipboardError;
try {
const text =
typeof data === 'object' && data !== null
? JSON.stringify(data, null, 2)
: String(data ?? '');
if (!navigator.clipboard?.writeText) {
throw new Error('Clipboard API is unavailable');
}
await navigator.clipboard.writeText(text);
return;
} catch (error) {
throw new Error(`Unable to copy text to clipboard: ${error.message}`);
clipboardError = error;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
document.body.appendChild(textarea);
try {
textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
if (!document.execCommand?.('copy')) {
throw new Error('Fallback copy command failed');
}
} catch {
throw new Error(
`Unable to copy text to clipboard: ${clipboardError.message}`
);
} finally {
textarea.remove();
}
};
@@ -1,15 +1,21 @@
import { copyTextToClipboard, handleOtpPaste } from '../clipboard';
const mockWriteText = vi.fn();
Object.assign(navigator, {
clipboard: {
writeText: mockWriteText,
},
});
const mockExecCommand = vi.fn();
const setClipboard = clipboard => {
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: clipboard,
});
};
describe('copyTextToClipboard', () => {
beforeEach(() => {
vi.clearAllMocks();
setClipboard({ writeText: mockWriteText });
document.execCommand = mockExecCommand;
mockExecCommand.mockReturnValue(true);
});
describe('with string input', () => {
@@ -133,23 +139,38 @@ describe('copyTextToClipboard', () => {
it('throws error when clipboard API fails', async () => {
const error = new Error('Clipboard access denied');
mockWriteText.mockRejectedValueOnce(error);
mockExecCommand.mockReturnValueOnce(false);
await expect(copyTextToClipboard('test')).rejects.toThrow(
'Unable to copy text to clipboard: Clipboard access denied'
);
});
it('handles clipboard API not available', async () => {
// Temporarily remove clipboard API
const originalClipboard = navigator.clipboard;
delete navigator.clipboard;
it('falls back when clipboard API is not available', async () => {
setClipboard(undefined);
await copyTextToClipboard('test');
expect(mockExecCommand).toHaveBeenCalledWith('copy');
expect(document.querySelector('textarea')).toBeNull();
});
it('falls back when clipboard API rejects the request', async () => {
mockWriteText.mockRejectedValueOnce(new Error('Clipboard access denied'));
await copyTextToClipboard('test');
expect(mockExecCommand).toHaveBeenCalledWith('copy');
expect(document.querySelector('textarea')).toBeNull();
});
it('throws when neither clipboard method is available', async () => {
setClipboard(undefined);
mockExecCommand.mockReturnValueOnce(false);
await expect(copyTextToClipboard('test')).rejects.toThrow(
'Unable to copy text to clipboard:'
'Unable to copy text to clipboard: Clipboard API is unavailable'
);
// Restore clipboard API
navigator.clipboard = originalClipboard;
});
});