Skip to content

Commit

Permalink
Merge pull request #1300 from capricorn86/1153-copy-to-clipboard-fails
Browse files Browse the repository at this point in the history
fix: [#1153] Fixes problem with ClipboardItem not supporting text and…
  • Loading branch information
capricorn86 committed Mar 12, 2024
2 parents 8c56b04 + 03c7b92 commit 08cd426
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 15 deletions.
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion packages/happy-dom/src/clipboard/Clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ export default class Clipboard {
let text = '';
for (const item of this.#data) {
if (item.types.includes('text/plain')) {
text += await (await item.getType('text/plain')).text();
const data = await item.getType('text/plain');
if (typeof data === 'string') {
text += data;
} else {
// Instance of Blob
text += await data.text();
}
}
}
return text;
Expand Down
11 changes: 3 additions & 8 deletions packages/happy-dom/src/clipboard/ClipboardItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Blob from '../file/Blob.js';
*/
export default class ClipboardItem {
public readonly presentationStyle: 'unspecified' | 'inline' | 'attachment' = 'unspecified';
#data: { [mimeType: string]: Blob };
#data: { [mimeType: string]: Blob | string | Promise<Blob | string> };

/**
* Constructor.
Expand All @@ -19,14 +19,9 @@ export default class ClipboardItem {
* @param [options.presentationStyle] Presentation style.
*/
constructor(
data: { [mimeType: string]: Blob },
data: { [mimeType: string]: Blob | string | Promise<Blob | string> },
options?: { presentationStyle?: 'unspecified' | 'inline' | 'attachment' }
) {
for (const mimeType of Object.keys(data)) {
if (mimeType !== data[mimeType].type) {
throw new DOMException(`Type ${mimeType} does not match the blob's type`);
}
}
this.#data = data;
if (options?.presentationStyle) {
this.presentationStyle = options.presentationStyle;
Expand All @@ -48,7 +43,7 @@ export default class ClipboardItem {
* @param type Type.
* @returns Data.
*/
public async getType(type: string): Promise<Blob> {
public async getType(type: string): Promise<Blob | string> {
if (!this.#data[type]) {
throw new DOMException(
`Failed to execute 'getType' on 'ClipboardItem': The type '${type}' was not found`
Expand Down
41 changes: 36 additions & 5 deletions packages/happy-dom/test/clipboard/Clipboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,37 @@ describe('Clipboard', () => {
it('Reads from the clipboard.', async () => {
const items = [
new ClipboardItem({
'text/plain': new Blob(['test'], { type: 'text/plain' })
'text/plain': new Blob(['test-a'], { type: 'text/plain' })
}),
new ClipboardItem({
'text/html': new Blob(['<b>test</b>'], { type: 'text/html' })
'text/html': new Blob(['<b>test-b</b>'], { type: 'text/html' })
}),
new ClipboardItem({
'text/plain': 'test-c'
}),
new ClipboardItem({
'text/plain': Promise.resolve('test-d')
}),
new ClipboardItem({
'text/plain': Promise.resolve(new Blob(['test-e'], { type: 'text/plain' }))
})
];
await window.navigator.clipboard.write(items);
const data = await window.navigator.clipboard.read();
expect(data).toEqual(items);

let text = '';

for (const item of data) {
const data = await item.getType(item.types[0]);
if (typeof data === 'string') {
text += data;
} else {
text += await data.text();
}
}

expect(text).toBe('test-a<b>test-b</b>test-ctest-dtest-e');
});

it('Throws an error if the permission is denied.', async () => {
Expand All @@ -50,15 +72,24 @@ describe('Clipboard', () => {
it('Reads text from the clipboard.', async () => {
const items = [
new ClipboardItem({
'text/plain': new Blob(['test'], { type: 'text/plain' })
'text/plain': new Blob(['test-a'], { type: 'text/plain' })
}),
new ClipboardItem({
'text/html': new Blob(['<b>test</b>'], { type: 'text/html' })
'text/html': new Blob(['<b>test-b</b>'], { type: 'text/html' })
}),
new ClipboardItem({
'text/plain': 'test-c'
}),
new ClipboardItem({
'text/plain': Promise.resolve('test-d')
}),
new ClipboardItem({
'text/plain': Promise.resolve(new Blob(['test-e'], { type: 'text/plain' }))
})
];
await window.navigator.clipboard.write(items);
const data = await window.navigator.clipboard.readText();
expect(data).toBe('test');
expect(data).toBe('test-atest-ctest-dtest-e');
});

it('Throws an error if the permission is denied.', async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/jest-environment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"prettier": "^2.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"usehooks-ts": "^3.0.1",
"rxjs": "^6.5.3",
"ts-jest": "^29.1.1",
"typescript": "^5.0.4",
Expand Down
15 changes: 14 additions & 1 deletion packages/jest-environment/test/react/React.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import ReactDOM from 'react-dom/client';
import * as ReactTestingLibrary from '@testing-library/react';
import ReactTestingLibraryUserEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
import { ReactDivComponent, ReactSelectComponent, ReactInputComponent } from './ReactComponents';
import {
ReactDivComponent,
ReactSelectComponent,
ReactInputComponent,
ReactClipboardComponent
} from './ReactComponents';
import * as Select from '@radix-ui/react-select';

/* eslint-disable @typescript-eslint/consistent-type-assertions */
Expand Down Expand Up @@ -97,4 +102,12 @@ describe('React', () => {
'<app><button type="button" role="combobox" aria-controls="radix-:r0:" aria-expanded="false" aria-autocomplete="none" dir="ltr" data-state="closed" data-placeholder=""><span style="pointer-events: none;"></span><span aria-hidden="true">▼</span></button></app>'
);
});

it('Can use copy to clipboard hook component', async () => {
const { getByRole } = ReactTestingLibrary.render(<ReactClipboardComponent />);
expect(document.querySelector('p span').textContent).toBe('Nothing');
const button: HTMLButtonElement = getByRole('button') as HTMLButtonElement;
await TESTING_LIBRARY_USER.click(button);
expect(document.querySelector('p span').textContent).toBe('test');
});
});
21 changes: 21 additions & 0 deletions packages/jest-environment/test/react/ReactComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { useCopyToClipboard } from 'usehooks-ts';

/* eslint-disable @typescript-eslint/consistent-type-assertions */

Expand Down Expand Up @@ -73,3 +74,23 @@ export class ReactInputComponent extends React.Component<{}, {}> {
return <input placeholder="input field" />;
}
}

/**
*
*/
export function ReactClipboardComponent(): React.ReactElement {
const [copiedText, copy] = useCopyToClipboard();

const handleCopy = (text: string) => () => {
copy(text);
};

return (
<>
<button onClick={handleCopy('test')}>A</button>
<p>
Copied value: <span>{copiedText ?? 'Nothing'}</span>
</p>
</>
);
}

0 comments on commit 08cd426

Please sign in to comment.