Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: work around shadowed globals #892

Merged
merged 2 commits into from Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 8 additions & 4 deletions 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,
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
@@ -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
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
@@ -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
@@ -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
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
@@ -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
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
@@ -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
@@ -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
@@ -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
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
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