From b52017ab1ea7fa4a5618e41a78fa882cbecf5ab5 Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Thu, 31 Mar 2022 11:56:07 +0000 Subject: [PATCH] fix: work around shadowed globals --- src/clipboard/paste.ts | 12 +++-- src/event/behavior/click.ts | 11 +++- src/pointer/pointerPress.ts | 2 +- src/utils/dataTransfer/Blob.ts | 2 +- src/utils/dataTransfer/Clipboard.ts | 66 +++++++++++++++--------- src/utils/dataTransfer/DataTransfer.ts | 17 +++--- src/utils/focus/copySelection.ts | 3 +- src/utils/index.ts | 1 + src/utils/misc/getWindow.ts | 5 ++ src/utils/misc/isVisible.ts | 4 +- src/utils/pointer/cssPointerEvents.ts | 4 +- src/utils/pointer/dom-helpers.d.ts | 3 -- tests/clipboard/paste.ts | 2 +- tests/utils/dataTransfer/Clipboard.ts | 9 ++-- tests/utils/dataTransfer/DataTransfer.ts | 24 +++++---- 15 files changed, 104 insertions(+), 61 deletions(-) create mode 100644 src/utils/misc/getWindow.ts delete mode 100644 src/utils/pointer/dom-helpers.d.ts diff --git a/src/clipboard/paste.ts b/src/clipboard/paste.ts index 2635b5f3..54f03381 100644 --- a/src/clipboard/paste.ts +++ b/src/clipboard/paste.ts @@ -1,5 +1,9 @@ import {Config, Instance} from '../setup' -import {createDataTransfer, readDataTransferFromClipboard} from '../utils' +import { + createDataTransfer, + getWindow, + readDataTransferFromClipboard, +} from '../utils' export async function paste( this: Instance, @@ -10,7 +14,7 @@ export async function paste( const dataTransfer: DataTransfer = (typeof clipboardData === 'string' - ? getClipboardDataFromString(clipboardData) + ? getClipboardDataFromString(doc, clipboardData) : clipboardData) ?? (await readDataTransferFromClipboard(doc).catch(() => { throw new Error( @@ -23,8 +27,8 @@ export async function paste( }) } -function getClipboardDataFromString(text: string) { - const dt = createDataTransfer() +function getClipboardDataFromString(doc: Document, text: string) { + const dt = createDataTransfer(getWindow(doc)) dt.setData('text', text) return dt } diff --git a/src/event/behavior/click.ts b/src/event/behavior/click.ts index 28919ba1..ae41843a 100644 --- a/src/event/behavior/click.ts +++ b/src/event/behavior/click.ts @@ -1,4 +1,11 @@ -import {blur, cloneEvent, focus, isElementType, isFocusable} from '../../utils' +import { + blur, + cloneEvent, + focus, + getWindow, + isElementType, + isFocusable, +} from '../../utils' import {dispatchEvent} from '../dispatchEvent' import {behavior} from './registry' @@ -17,7 +24,7 @@ behavior.click = (event, target, config) => { // blur fires when the file selector pops up blur(target) - target.dispatchEvent(new Event('fileDialog')) + target.dispatchEvent(new (getWindow(target).Event)('fileDialog')) // focus fires after the file selector has been closed focus(target) diff --git a/src/pointer/pointerPress.ts b/src/pointer/pointerPress.ts index ad025b0b..da101fa4 100644 --- a/src/pointer/pointerPress.ts +++ b/src/pointer/pointerPress.ts @@ -315,7 +315,7 @@ function mousedownDefaultBehavior({ offset: end, }) - const range = new Range() + const range = target.ownerDocument.createRange() range.setStart(startNode, startOffset) range.setEnd(endNode, endOffset) diff --git a/src/utils/dataTransfer/Blob.ts b/src/utils/dataTransfer/Blob.ts index ab223ff5..89ef9906 100644 --- a/src/utils/dataTransfer/Blob.ts +++ b/src/utils/dataTransfer/Blob.ts @@ -1,6 +1,6 @@ // jsdom does not implement Blob.text() -export function readBlobText(blob: Blob) { +export function readBlobText(blob: Blob, FileReader: {new (): FileReader}) { return new Promise((res, rej) => { const fr = new FileReader() fr.onerror = rej diff --git a/src/utils/dataTransfer/Clipboard.ts b/src/utils/dataTransfer/Clipboard.ts index cfc19a2f..0694ae22 100644 --- a/src/utils/dataTransfer/Clipboard.ts +++ b/src/utils/dataTransfer/Clipboard.ts @@ -1,9 +1,12 @@ // Clipboard is not available in jsdom import {createDataTransfer, getBlobFromDataTransferItem, readBlobText} from '..' +import {getWindow} from '../misc/getWindow' // Clipboard API is only fully available in secure context or for browser extensions. +const Window = Symbol('Window reference') + type ItemData = Record> class ClipboardItemStub implements ClipboardItem { @@ -25,13 +28,17 @@ class ClipboardItemStub implements ClipboardItem { ) } - return data instanceof Blob ? data : new Blob([data], {type}) + return data instanceof this[Window].Blob + ? data + : new this[Window].Blob([data], {type}) } + + [Window] = window } const ClipboardStubControl = Symbol('Manage ClipboardSub') -class ClipboardStub extends EventTarget implements Clipboard { +class ClipboardStub extends window.EventTarget implements Clipboard { private items: ClipboardItem[] = [] async read() { @@ -45,7 +52,9 @@ class ClipboardStub extends EventTarget implements Clipboard { ? 'text/plain' : item.types.find(t => t.startsWith('text/')) if (type) { - text += await item.getType(type).then(b => readBlobText(b)) + text += await item + .getType(type) + .then(b => readBlobText(b, this[Window].FileReader)) } } return text @@ -56,9 +65,10 @@ class ClipboardStub extends EventTarget implements Clipboard { } async writeText(text: string) { - this.items = [createClipboardItem(text)] + this.items = [createClipboardItem(this[Window], text)] } + [Window] = window; [ClipboardStubControl]: { resetClipboardStub: () => void detachClipboardStub: () => void @@ -69,22 +79,25 @@ class ClipboardStub extends EventTarget implements Clipboard { // lib.dom.d.ts lists only Promise // https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/ClipboardItem#syntax export function createClipboardItem( + window: Window & typeof globalThis, ...blobs: Array ): ClipboardItem { - // use real ClipboardItem if available - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - const constructor = - typeof ClipboardItem === 'undefined' - ? ClipboardItemStub - : /* istanbul ignore next */ ClipboardItem - return new constructor( - Object.fromEntries( - blobs.map(b => [ - typeof b === 'string' ? 'text/plain' : b.type, - Promise.resolve(b), - ]), - ), + const data = Object.fromEntries( + blobs.map(b => [ + typeof b === 'string' ? 'text/plain' : b.type, + Promise.resolve(b), + ]), ) + + // use real ClipboardItem if available + /* istanbul ignore else */ + if (typeof window.ClipboardItem === 'undefined') { + const item = new ClipboardItemStub(data) + item[Window] = window + return item + } else { + return new window.ClipboardItem(data) + } } export function attachClipboardStubToView(window: Window & typeof globalThis) { @@ -98,9 +111,11 @@ export function attachClipboardStubToView(window: Window & typeof globalThis) { ) let stub = new ClipboardStub() + stub[Window] = window const control = { resetClipboardStub: () => { stub = new ClipboardStub() + stub[Window] = window stub[ClipboardStubControl] = control }, detachClipboardStub: () => { @@ -140,17 +155,21 @@ export function detachClipboardStubFromView( } export async function readDataTransferFromClipboard(document: Document) { - const clipboard = document.defaultView?.navigator.clipboard + const window = document.defaultView + const clipboard = window?.navigator.clipboard const items = clipboard && (await clipboard.read()) if (!items) { throw new Error('The Clipboard API is unavailable.') } - const dt = createDataTransfer() + const dt = createDataTransfer(window) for (const item of items) { for (const type of item.types) { - dt.setData(type, await item.getType(type).then(b => readBlobText(b))) + dt.setData( + type, + await item.getType(type).then(b => readBlobText(b, window.FileReader)), + ) } } return dt @@ -160,13 +179,14 @@ export async function writeDataTransferToClipboard( document: Document, clipboardData: DataTransfer, ) { - const clipboard = document.defaultView?.navigator.clipboard + const window = getWindow(document) + const clipboard = window.navigator.clipboard as Clipboard | undefined const items = [] for (let i = 0; i < clipboardData.items.length; i++) { const dtItem = clipboardData.items[i] - const blob = getBlobFromDataTransferItem(dtItem) - items.push(createClipboardItem(blob)) + const blob = getBlobFromDataTransferItem(window, dtItem) + items.push(createClipboardItem(window, blob)) } const written = diff --git a/src/utils/dataTransfer/DataTransfer.ts b/src/utils/dataTransfer/DataTransfer.ts index 547eff53..fe622a12 100644 --- a/src/utils/dataTransfer/DataTransfer.ts +++ b/src/utils/dataTransfer/DataTransfer.ts @@ -133,20 +133,25 @@ class DataTransferStub implements DataTransfer { setDragImage() {} } -export function createDataTransfer(files: File[] = []): DataTransfer { +export function createDataTransfer( + window: Window & typeof globalThis, + files: File[] = [], +): DataTransfer { // Use real DataTransfer if available - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const dt = - typeof DataTransfer === 'undefined' + typeof window.DataTransfer === 'undefined' ? (new DataTransferStub() as DataTransfer) - : /* istanbul ignore next */ new DataTransfer() + : /* istanbul ignore next */ new window.DataTransfer() Object.defineProperty(dt, 'files', {get: () => createFileList(files)}) return dt } -export function getBlobFromDataTransferItem(item: DataTransferItem) { +export function getBlobFromDataTransferItem( + window: Window & typeof globalThis, + item: DataTransferItem, +) { if (item.kind === 'file') { return item.getAsFile() as File } @@ -154,5 +159,5 @@ export function getBlobFromDataTransferItem(item: DataTransferItem) { item.getAsString(s => { data = s }) - return new Blob([data], {type: item.type}) + return new window.Blob([data], {type: item.type}) } diff --git a/src/utils/focus/copySelection.ts b/src/utils/focus/copySelection.ts index 3fd67a70..bc221e52 100644 --- a/src/utils/focus/copySelection.ts +++ b/src/utils/focus/copySelection.ts @@ -1,6 +1,7 @@ import {getUISelection, getUIValue} from '../../document' import {createDataTransfer} from '../dataTransfer/DataTransfer' import {EditableInputType} from '../edit/isEditable' +import {getWindow} from '../misc/getWindow' import {hasOwnSelection} from './selection' export function copySelection(target: Element) { @@ -9,7 +10,7 @@ export function copySelection(target: Element) { : // TODO: We could implement text/html copying of DOM nodes here {'text/plain': String(target.ownerDocument.getSelection())} - const dt = createDataTransfer() + const dt = createDataTransfer(getWindow(target)) for (const type in data) { if (data[type]) { dt.setData(type, data[type]) diff --git a/src/utils/index.ts b/src/utils/index.ts index abff1632..4c132869 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -31,6 +31,7 @@ export * from './misc/cloneEvent' export * from './misc/eventWrapper' export * from './misc/findClosest' export * from './misc/getDocumentFromNode' +export * from './misc/getWindow' export * from './misc/isDescendantOrSelf' export * from './misc/isElementType' export * from './misc/isVisible' diff --git a/src/utils/misc/getWindow.ts b/src/utils/misc/getWindow.ts new file mode 100644 index 00000000..4dcde0b0 --- /dev/null +++ b/src/utils/misc/getWindow.ts @@ -0,0 +1,5 @@ +import {getWindowFromNode} from '@testing-library/dom/dist/helpers.js' + +export function getWindow(node: Node) { + return getWindowFromNode(node) as Window & typeof globalThis +} diff --git a/src/utils/misc/isVisible.ts b/src/utils/misc/isVisible.ts index 41be011c..022a45e1 100644 --- a/src/utils/misc/isVisible.ts +++ b/src/utils/misc/isVisible.ts @@ -1,7 +1,7 @@ -import {getWindowFromNode} from '@testing-library/dom/dist/helpers.js' +import {getWindow} from './getWindow' export function isVisible(element: Element): boolean { - const window = getWindowFromNode(element) + const window = getWindow(element) for ( let el: Element | null = element; diff --git a/src/utils/pointer/cssPointerEvents.ts b/src/utils/pointer/cssPointerEvents.ts index 74badbf6..ae1d8772 100644 --- a/src/utils/pointer/cssPointerEvents.ts +++ b/src/utils/pointer/cssPointerEvents.ts @@ -1,10 +1,10 @@ -import {getWindowFromNode} from '@testing-library/dom/dist/helpers.js' import {PointerEventsCheckLevel} from '../../options' import {Config} from '../../setup' import {ApiLevel, getLevelRef} from '..' +import {getWindow} from '../misc/getWindow' export function hasPointerEvents(element: Element): boolean { - const window = getWindowFromNode(element) + const window = getWindow(element) for ( let el: Element | null = element; diff --git a/src/utils/pointer/dom-helpers.d.ts b/src/utils/pointer/dom-helpers.d.ts deleted file mode 100644 index fd0b3f29..00000000 --- a/src/utils/pointer/dom-helpers.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module '@testing-library/dom/dist/helpers.js' { - export function getWindowFromNode(node: Node): Window -} diff --git a/tests/clipboard/paste.ts b/tests/clipboard/paste.ts index 813e0a1d..c792ae1a 100644 --- a/tests/clipboard/paste.ts +++ b/tests/clipboard/paste.ts @@ -16,7 +16,7 @@ test('do not trigger input for paste with file data', async () => { const {getEvents, user} = setup(``) const f0 = new File(['bar'], 'bar0.txt', {type: 'text/plain'}) - const dt = createDataTransfer([f0]) + const dt = createDataTransfer(window, [f0]) await user.paste(dt) expect(getEvents('paste')).toHaveLength(1) diff --git a/tests/utils/dataTransfer/Clipboard.ts b/tests/utils/dataTransfer/Clipboard.ts index 9d71b21d..bf2dcd8e 100644 --- a/tests/utils/dataTransfer/Clipboard.ts +++ b/tests/utils/dataTransfer/Clipboard.ts @@ -18,10 +18,11 @@ describe('read from and write to clipboard', () => { test('read and write item', async () => { const items = [ - createClipboardItem(new Blob(['foo'], {type: 'text/plain'})), - createClipboardItem(new Blob(['bar'], {type: 'text/html'})), - createClipboardItem(new Blob(['PNG'], {type: 'image/png'})), + createClipboardItem(window, new Blob(['foo'], {type: 'text/plain'})), + createClipboardItem(window, new Blob(['bar'], {type: 'text/html'})), + createClipboardItem(window, new Blob(['PNG'], {type: 'image/png'})), createClipboardItem( + window, new Blob(['baz1'], {type: 'text/plain'}), new Blob(['baz2'], {type: 'text/html'}), ), @@ -30,7 +31,7 @@ describe('read from and write to clipboard', () => { expect(items[3]).toHaveProperty('types', ['text/plain', 'text/html']) await expect(items[3].getType('text/html')).resolves.toBeInstanceOf(Blob) await expect( - readBlobText(await items[3].getType('text/html')), + readBlobText(await items[3].getType('text/html'), FileReader), ).resolves.toBe('baz2') await expect(items[3].getType('image/png')).rejects.toThrowError() diff --git a/tests/utils/dataTransfer/DataTransfer.ts b/tests/utils/dataTransfer/DataTransfer.ts index eb619ab0..4afdb9d7 100644 --- a/tests/utils/dataTransfer/DataTransfer.ts +++ b/tests/utils/dataTransfer/DataTransfer.ts @@ -2,7 +2,7 @@ import {createDataTransfer, getBlobFromDataTransferItem} from '#src/utils' describe('create DataTransfer', () => { test('plain string', async () => { - const dt = createDataTransfer() + const dt = createDataTransfer(window) dt.setData('text/plain', 'foo') expect(dt.getData('text/plain')).toBe('foo') @@ -13,7 +13,7 @@ describe('create DataTransfer', () => { }) test('multi format', async () => { - const dt = createDataTransfer() + const dt = createDataTransfer(window) dt.setData('text/plain', 'foo') dt.setData('text/html', 'bar') @@ -31,7 +31,7 @@ describe('create DataTransfer', () => { }) test('overwrite item', async () => { - const dt = createDataTransfer() + const dt = createDataTransfer(window) dt.setData('text/plain', 'foo') dt.setData('text/plain', 'bar') @@ -42,7 +42,7 @@ describe('create DataTransfer', () => { test('files operation', async () => { const f0 = new File(['bar'], 'bar0.txt', {type: 'text/plain'}) const f1 = new File(['bar'], 'bar1.txt', {type: 'text/plain'}) - const dt = createDataTransfer([f0, f1]) + const dt = createDataTransfer(window, [f0, f1]) dt.setData('text/html', 'foo') expect(dt.types).toEqual(['Files', 'text/html']) @@ -51,7 +51,7 @@ describe('create DataTransfer', () => { test('files item', async () => { const f0 = new File(['bar'], 'bar0.txt', {type: 'text/plain'}) - const dt = createDataTransfer() + const dt = createDataTransfer(window) dt.setData('text/html', 'foo') dt.items.add(f0) @@ -67,7 +67,7 @@ describe('create DataTransfer', () => { test('clear data', async () => { const f0 = new File(['bar'], 'bar0.txt', {type: 'text/plain'}) - const dt = createDataTransfer() + const dt = createDataTransfer(window) dt.setData('text/html', 'foo') dt.items.add(f0) @@ -86,18 +86,20 @@ describe('create DataTransfer', () => { }) test('get Blob from DataTransfer', async () => { - const dt = createDataTransfer() + const dt = createDataTransfer(window) dt.items.add('foo', 'text/plain') dt.items.add(new File(['bar'], 'bar.txt', {type: 'text/plain'})) - expect(getBlobFromDataTransferItem(dt.items[0])).toHaveProperty( + expect(getBlobFromDataTransferItem(window, dt.items[0])).toHaveProperty( 'type', 'text/plain', ) - expect(getBlobFromDataTransferItem(dt.items[0])).not.toBeInstanceOf(File) - expect(getBlobFromDataTransferItem(dt.items[1])).toHaveProperty( + expect(getBlobFromDataTransferItem(window, dt.items[0])).not.toBeInstanceOf( + File, + ) + expect(getBlobFromDataTransferItem(window, dt.items[1])).toHaveProperty( 'type', 'text/plain', ) - expect(getBlobFromDataTransferItem(dt.items[1])).toBeInstanceOf(File) + expect(getBlobFromDataTransferItem(window, dt.items[1])).toBeInstanceOf(File) })