Skip to content

Commit

Permalink
fix: work around shadowed globals (#892)
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed Mar 31, 2022
1 parent 810c0d8 commit 126d2e7
Show file tree
Hide file tree
Showing 15 changed files with 104 additions and 61 deletions.
12 changes: 8 additions & 4 deletions src/clipboard/paste.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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(
Expand All @@ -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
}
11 changes: 9 additions & 2 deletions src/event/behavior/click.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/pointer/pointerPress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion src/utils/dataTransfer/Blob.ts
Original file line number Diff line number Diff line change
@@ -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<string>((res, rej) => {
const fr = new FileReader()
fr.onerror = rej
Expand Down
66 changes: 43 additions & 23 deletions src/utils/dataTransfer/Clipboard.ts
Original file line number Diff line number Diff line change
@@ -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<string, Blob | string | Promise<Blob | string>>

class ClipboardItemStub implements ClipboardItem {
Expand All @@ -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() {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -69,22 +79,25 @@ class ClipboardStub extends EventTarget implements Clipboard {
// lib.dom.d.ts lists only Promise<Blob|string>
// https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/ClipboardItem#syntax
export function createClipboardItem(
window: Window & typeof globalThis,
...blobs: Array<Blob | string>
): 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) {
Expand All @@ -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: () => {
Expand Down Expand Up @@ -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
Expand All @@ -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 =
Expand Down
17 changes: 11 additions & 6 deletions src/utils/dataTransfer/DataTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,26 +133,31 @@ 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
}
let data: string = ''
item.getAsString(s => {
data = s
})
return new Blob([data], {type: item.type})
return new window.Blob([data], {type: item.type})
}
3 changes: 2 additions & 1 deletion src/utils/focus/copySelection.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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])
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
5 changes: 5 additions & 0 deletions src/utils/misc/getWindow.ts
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions src/utils/misc/isVisible.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/utils/pointer/cssPointerEvents.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
3 changes: 0 additions & 3 deletions src/utils/pointer/dom-helpers.d.ts

This file was deleted.

2 changes: 1 addition & 1 deletion tests/clipboard/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ test('do not trigger input for paste with file data', async () => {
const {getEvents, user} = setup(`<input/>`)

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)
Expand Down
9 changes: 5 additions & 4 deletions tests/utils/dataTransfer/Clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'}),
),
Expand All @@ -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()

Expand Down
Loading

0 comments on commit 126d2e7

Please sign in to comment.