diff --git a/.grok/569650b1-fed7-434f-8ff4-cbafe26f06b7.grok.json b/.grok/569650b1-fed7-434f-8ff4-cbafe26f06b7.grok.json new file mode 100644 index 00000000..7cf3039e --- /dev/null +++ b/.grok/569650b1-fed7-434f-8ff4-cbafe26f06b7.grok.json @@ -0,0 +1,260 @@ +{ + "cells": [ + { + "blockId": "10ecf944-1813-4dad-813a-4e3e3a9ce05e", + "data": { + "formatCode": false, + "isPreviewing": true, + "text": "This function's return type is tricky.

\r
If in IE11 or other environments where window/globalThis does not contain object properties for each type of DOM Event Class,\r it returns the value of window.document.createEvent(typeof eventType) & init`.

In all other environments, it simply returns the Event or Event-like object constructed by `new window[typeof eventType]()` I'm not sure how best to represent this in TS. For now, I will specify its return type as if it is invoked in the latter case.
" + }, + "id": "10ecf944-1813-4dad-813a-4e3e3a9ce05e", + "type": "markdown" + }, + { + "blockId": "22da9304-9908-4580-a3df-c1633c7be0c1", + "data": { + "contents": "File not found", + "file": "src/events.ts", + "range": { + "end": { + "character": 2147483647, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + }, + "scm": { + "authors": [], + "branch": "pr/ts/src/events", + "file": "src/events.ts", + "originalRange": { + "end": { + "character": 2147483647, + "line": 173 + }, + "start": { + "character": 0, + "line": 146 + } + }, + "remotes": [ + { + "name": "origin", + "url": "github.com/riotrah/dom-testing-library" + }, + { + "name": "upstream", + "url": "github.com/testing-library/dom-testing-library" + } + ], + "repoName": "dom-testing-library", + "revision": "458ef8738cb56de666899737d9b9aa94b32c9153", + "sameRepo": true + } + }, + "id": "22da9304-9908-4580-a3df-c1633c7be0c1", + "type": "code", + "version": 1 + }, + { + "blockId": "3dd1f069-6d23-4bac-856d-2da380292acd", + "data": { + "contents": "File not found", + "file": "src/events.ts", + "range": { + "end": { + "character": 2147483647, + "line": 0 + }, + "start": { + "character": 0, + "line": 0 + } + }, + "scm": { + "authors": [], + "branch": "pr/ts/src/events", + "file": "src/events.ts", + "originalRange": { + "end": { + "character": 2147483647, + "line": 207 + }, + "start": { + "character": 0, + "line": 207 + } + }, + "remotes": [ + { + "name": "origin", + "url": "github.com/riotrah/dom-testing-library" + }, + { + "name": "upstream", + "url": "github.com/testing-library/dom-testing-library" + } + ], + "repoName": "dom-testing-library", + "revision": "458ef8738cb56de666899737d9b9aa94b32c9153", + "sameRepo": true + } + }, + "id": "3dd1f069-6d23-4bac-856d-2da380292acd", + "type": "code", + "version": 1 + }, + { + "blockId": "603e6503-4d39-42df-a812-7786f5081c1f", + "data": { + "formatCode": false, + "isPreviewing": true, + "text": "
The type of `init` is tricky. If called in an environment that exposes an Event-extending class per Event type name in `window` or `globalThis`, its type is that of the corresponding EventInit or EventInit-like interface. \n

\r
Eg: If:\r
\r
1. Aforemention environmental conditions are met\r
2. And eventName extends keyof typeof window (eg eventName === \"MouseEvent\", etc)\r
3. Then init will/should be (at least partially) MouseEventInit, as determined by the 2nd argument for the MouseEvent constructor, etc\r

However, if in an environment (eg IE11) where there does not exist such Event classes in window/globalThis, or if said property is not a Class, `init` can be more correctly regarded as being of type Omit< MyEvent , 'bubbles' | 'cancelable' | 'detail' >, where MyEvent is the actual Event derivative being created. That is, `init` is not so much an EventInit-like object so much as an Event-like one.\n

Technically, whatever `init` is, in the IE11 case, its properties will be merged with the Event that gets created, so it could have any shape,\r including one that might violate any constraints that assumed the return of this function to be a \"legal\" DOM Event or derivative thereof.

Given that the IE11 case is the edge case, I have defined `init` somewhat broadly, but with preference given to its type as being that of an EventInit-like object.

In all cases, init may optionally define 'dataTransfer' and 'clipboardData' properties as well, which are special cases for which the created Event object is explicity assigned as properties. No attempt is made to verify whether those properties would reasonably exist on an Event of the created type.

In addition, `init` may also define a `target` property and a `detail` property.\n

`target`: should most probably take the shape of the same `node` that this Event is created against, thus, eg. a click event's `target` prop should point to the element that was clicked on, which should be the element passed as the first param to this function. However, its usage implies the existence of a couple of optional properties - `files` and `value`, whose types (taken from HTMLInputElement, their most likely containing object) I've explicitly intersected with their sole accesses below.\n

\r
`detail`: is only correctly used in the first case from the above-mentioned environmental Event class considerations, where it is passed to the event constructor.\r In those cases its type is varied, and dependent on the Event in question.
" + }, + "id": "603e6503-4d39-42df-a812-7786f5081c1f", + "type": "markdown" + }, + { + "blockId": "170a3b9f-2e74-4693-8962-9468e85c7b25", + "data": { + "contents": " *", + "file": "src/events.ts", + "range": { + "end": { + "character": 2147483647, + "line": 53 + }, + "start": { + "character": 0, + "line": 53 + } + }, + "scm": { + "authors": [], + "branch": "pr/ts/src/events", + "file": "src/events.ts", + "originalRange": { + "end": { + "character": 2147483647, + "line": 53 + }, + "start": { + "character": 0, + "line": 53 + } + }, + "remotes": [ + { + "name": "origin", + "url": "github.com/riotrah/dom-testing-library" + }, + { + "name": "upstream", + "url": "github.com/testing-library/dom-testing-library" + } + ], + "repoName": "dom-testing-library", + "revision": "458ef8738cb56de666899737d9b9aa94b32c9153", + "sameRepo": true + } + }, + "id": "170a3b9f-2e74-4693-8962-9468e85c7b25", + "type": "code", + "version": 1 + } + ], + "graphViewMetadata": { + "edges": [ + { + "id": "10ecf944-1813-4dad-813a-4e3e3a9ce05e_bottom_22da9304-9908-4580-a3df-c1633c7be0c1_top_MTjQTiyqSlaHdlCxYvI-fQ", + "source": "10ecf944-1813-4dad-813a-4e3e3a9ce05e", + "sourceHandle": "bottom", + "target": "22da9304-9908-4580-a3df-c1633c7be0c1", + "targetHandle": "top" + }, + { + "id": "3dd1f069-6d23-4bac-856d-2da380292acd_top_10ecf944-1813-4dad-813a-4e3e3a9ce05e_bottom_07DL6JvnROmfE-V0WNsDBQ", + "source": "3dd1f069-6d23-4bac-856d-2da380292acd", + "sourceHandle": "top", + "target": "10ecf944-1813-4dad-813a-4e3e3a9ce05e", + "targetHandle": "bottom" + }, + { + "id": "603e6503-4d39-42df-a812-7786f5081c1f_left_22da9304-9908-4580-a3df-c1633c7be0c1_right_ZEEz4CigQxmMbyYnB2J4RA", + "source": "603e6503-4d39-42df-a812-7786f5081c1f", + "sourceHandle": "left", + "target": "22da9304-9908-4580-a3df-c1633c7be0c1", + "targetHandle": "right" + }, + { + "id": "170a3b9f-2e74-4693-8962-9468e85c7b25_bottom_10ecf944-1813-4dad-813a-4e3e3a9ce05e_left_zuPisN4TQTmpQGeF-FVzLA", + "source": "170a3b9f-2e74-4693-8962-9468e85c7b25", + "sourceHandle": "bottom", + "target": "10ecf944-1813-4dad-813a-4e3e3a9ce05e", + "targetHandle": "left" + } + ], + "graph": { + "position": [257.09839600544933, 100.8498721136998], + "zoom": 1.0852293718818073 + }, + "nodes": [ + { + "blockId": "10ecf944-1813-4dad-813a-4e3e3a9ce05e", + "dimensions": { + "height": 224, + "width": 577 + }, + "id": "dndnode_0", + "position": { + "x": 105, + "y": 45 + } + }, + { + "blockId": "22da9304-9908-4580-a3df-c1633c7be0c1", + "dimensions": {}, + "id": "dndnode_1", + "position": { + "x": 270, + "y": 375 + } + }, + { + "blockId": "3dd1f069-6d23-4bac-856d-2da380292acd", + "dimensions": {}, + "id": "dndnode_2", + "position": { + "x": -285, + "y": 375 + } + }, + { + "blockId": "603e6503-4d39-42df-a812-7786f5081c1f", + "dimensions": { + "height": 866, + "width": 537 + }, + "id": "dndnode_3", + "position": { + "x": 930, + "y": 45 + } + }, + { + "blockId": "170a3b9f-2e74-4693-8962-9468e85c7b25", + "dimensions": {}, + "id": "dndnode_4", + "position": { + "x": -270, + "y": -90 + } + } + ] + }, + "text": "CreateEvent has a complex function signature if we try too hard to narrow the type of its `init` parameter or try to consider whether or not the environment its called in is IE11 (or otherwise doesn't define an Event-like class with the key `typeof eventType` in Window).\n\nIn general, I err on the side of assuming it's not invoked in IE11.", + "title": "The type of CreateEvent (as called as a function)", + "version": 1 +} diff --git a/.grok/5ee5f316-1db9-4a19-9b28-b8a05b512a78.grok.json b/.grok/5ee5f316-1db9-4a19-9b28-b8a05b512a78.grok.json new file mode 100644 index 00000000..de71a750 --- /dev/null +++ b/.grok/5ee5f316-1db9-4a19-9b28-b8a05b512a78.grok.json @@ -0,0 +1,10 @@ +{ + "cells": [], + "graphViewMetadata": { + "edges": [], + "nodes": [] + }, + "text": "For the definitions of both _createEvent and _fireEvent, converting from `function _creatEvent() {}` to `const _createEvent: CreateFunction = () => {}` (etc) allows us to explicitly check that the exported types (which are imported here and assigned to the functions) match their written types/signatures. This also allows us to eventually export them as themselves, but `Required`. Eg: `export const createEvent = _createEvent as Required`, instead of a stronger, potentially more likely to be incorrect, cast of `_createEvent as CreateObject & CreateFunction`.\n\nThe downsides are:\n\n1. Loss of the explicit, quick designation as function that `function myFunc() {}` provides vs `const myFunc = () => {}`\n2. Unpredictable or different expectations for `this` when fireEvent or createEvent are invoked as methods, for example, as a result of becoming arrow functions\n3. A little uglier and slightly more mental overhead when reading/modifying this code in the future", + "title": "Function definition syntax signature to arrow syntax", + "version": 1 +} diff --git a/.grok/bb22615c-5f7d-49f4-a543-76c931917d01.grok.json b/.grok/bb22615c-5f7d-49f4-a543-76c931917d01.grok.json new file mode 100644 index 00000000..2e6e529b --- /dev/null +++ b/.grok/bb22615c-5f7d-49f4-a543-76c931917d01.grok.json @@ -0,0 +1,14 @@ +{ + "cells": [], + "graphViewMetadata": { + "edges": [], + "graph": { + "position": [1103.0797527512682, -472.6089894708548], + "zoom": 0.9421312739048091 + }, + "nodes": [] + }, + "text": "In the existing `events.d.ts` `EventType` is a manually defied union of string literals. Im this portentially redundant definitions it's the keys of the the `eventMap` objet, which I suspect serves as a better source of truth for `EventType`. Stlll, so as to not break anything that depended on the existing definition as distinct from its usage in parameters of `fireEvent`/`createEvent` (however un/common that might be), I've kept their defs distinct here.", + "title": "EventType in this file is partially redundant yet distinct from another similar type", + "version": 1 +} diff --git a/.grok/contents.grok.json b/.grok/contents.grok.json new file mode 100644 index 00000000..ded0aff1 --- /dev/null +++ b/.grok/contents.grok.json @@ -0,0 +1,57 @@ +{ + "569650b1-fed7-434f-8ff4-cbafe26f06b7": { + "filePath": ".grok/569650b1-fed7-434f-8ff4-cbafe26f06b7.grok.json", + "id": "569650b1-fed7-434f-8ff4-cbafe26f06b7", + "lastModifiedCommitHash": "458ef8738cb56de666899737d9b9aa94b32c9153", + "markers": [ + { + "branch": "pr/ts/src/events", + "commitHash": "458ef8738cb56de666899737d9b9aa94b32c9153", + "file": "src/events.ts", + "location": [0, 0], + "repo": "dom-testing-library" + }, + { + "branch": "pr/ts/src/events", + "commitHash": "458ef8738cb56de666899737d9b9aa94b32c9153", + "file": "src/events.ts", + "location": [0, 0], + "repo": "dom-testing-library" + }, + { + "branch": "pr/ts/src/events", + "commitHash": "458ef8738cb56de666899737d9b9aa94b32c9153", + "file": "src/events.ts", + "location": [53, 53], + "repo": "dom-testing-library" + } + ], + "modifiedAt": 1628798017625, + "originalAuthor": "Rayat Rahman", + "text": "CreateEvent has a complex function signature if we try too hard to narrow the type of its `init` parameter or try to consider whether or not the environment its called in is IE11 (or otherwise doesn't define an Event-like class with the key `typeof eventType` in Window).\n\nIn general, I err on the side of assuming it's not invoked in IE11.", + "title": "The type of CreateEvent (as called as a function)", + "version": 1 + }, + "5ee5f316-1db9-4a19-9b28-b8a05b512a78": { + "filePath": ".grok/5ee5f316-1db9-4a19-9b28-b8a05b512a78.grok.json", + "id": "5ee5f316-1db9-4a19-9b28-b8a05b512a78", + "lastModifiedCommitHash": "458ef8738cb56de666899737d9b9aa94b32c9153", + "markers": [], + "modifiedAt": 1628799097645, + "originalAuthor": "Rayat Rahman", + "text": "For the definitions of both _createEvent and _fireEvent, converting from `function _creatEvent() {}` to `const _createEvent: CreateFunction = () => {}` (etc) allows us to explicitly check that the exported types (which are imported here and assigned to the functions) match their written types/signatures. This also allows us to eventually export them as themselves, but `Required`. Eg: `export const createEvent = _createEvent as Required`, instead of a stronger, potentially more likely to be incorrect, cast of `_createEvent as CreateObject & CreateFunction`.\n\nThe downsides are:\n\n1. Loss of the explicit, quick designation as function that `function myFunc() {}` provides vs `const myFunc = () => {}`\n2. Unpredictable or different expectations for `this` when fireEvent or createEvent are invoked as methods, for example, as a result of becoming arrow functions\n3. A little uglier and slightly more mental overhead when reading/modifying this code in the future", + "title": "Function definition syntax signature to arrow syntax", + "version": 1 + }, + "bb22615c-5f7d-49f4-a543-76c931917d01": { + "filePath": ".grok/bb22615c-5f7d-49f4-a543-76c931917d01.grok.json", + "id": "bb22615c-5f7d-49f4-a543-76c931917d01", + "lastModifiedCommitHash": "84bf52f1b2b30f34e3a709e5bdd32e14623d6f60", + "markers": [], + "modifiedAt": 1628482259034, + "originalAuthor": "Rayat Rahman", + "text": "In the existing `events.d.ts` `EventType` is a manually defied union of string literals. Im this portentially redundant definitions it's the keys of the the `eventMap` objet, which I suspect serves as a better source of truth for `EventType`. Stlll, so as to not break anything that depended on the existing definition as distinct from its usage in parameters of `fireEvent`/`createEvent` (however un/common that might be), I've kept their defs distinct here.", + "title": "EventType in this file is partially redundant yet distinct from another similar type", + "version": 1 + } +} diff --git a/src/__tests__/get-user-code-frame.js b/src/__tests__/get-user-code-frame.js index 8d2bd058..6befcc78 100644 --- a/src/__tests__/get-user-code-frame.js +++ b/src/__tests__/get-user-code-frame.js @@ -39,13 +39,13 @@ test('it returns only user code frame when code frames from node_modules are fir const userTrace = getUserCodeFrame(stack) expect(userTrace).toMatchInlineSnapshot(` - /sample-error/error-example.js:7:14 - 5 | document.createTextNode('Hello world') - 6 | ) - > 7 | screen.debug() - | ^ - - `) +"/sample-error/error-example.js:7:14 + 5 | document.createTextNode('Hello world') + 6 | ) +> 7 | screen.debug() + | ^ +" +`) }) test('it returns only user code frame when node code frames are present afterwards', () => { @@ -59,13 +59,13 @@ test('it returns only user code frame when node code frames are present afterwar const userTrace = getUserCodeFrame() expect(userTrace).toMatchInlineSnapshot(` - /sample-error/error-example.js:7:14 - 5 | document.createTextNode('Hello world') - 6 | ) - > 7 | screen.debug() - | ^ - - `) +"/sample-error/error-example.js:7:14 + 5 | document.createTextNode('Hello world') + 6 | ) +> 7 | screen.debug() + | ^ +" +`) }) test("it returns empty string if file from code frame can't be read", () => { diff --git a/src/event-map.js b/src/event-map.ts similarity index 99% rename from src/event-map.js rename to src/event-map.ts index a4d5a915..6052e8fa 100644 --- a/src/event-map.js +++ b/src/event-map.ts @@ -347,8 +347,8 @@ export const eventMap = { EventType: 'PopStateEvent', defaultInit: {bubbles: true, cancelable: false}, }, -} +} as const; export const eventAliasMap = { - doubleClick: 'dblClick', + doubleClick: 'dblClick' as const, } diff --git a/src/events.js b/src/events.js deleted file mode 100644 index 57446bf8..00000000 --- a/src/events.js +++ /dev/null @@ -1,128 +0,0 @@ -import {getConfig} from './config' -import {getWindowFromNode} from './helpers' -import {eventMap, eventAliasMap} from './event-map' - -function fireEvent(element, event) { - return getConfig().eventWrapper(() => { - if (!event) { - throw new Error( - `Unable to fire an event - please provide an event object.`, - ) - } - if (!element) { - throw new Error( - `Unable to fire a "${event.type}" event - please provide a DOM element.`, - ) - } - return element.dispatchEvent(event) - }) -} - -function createEvent( - eventName, - node, - init, - {EventType = 'Event', defaultInit = {}} = {}, -) { - if (!node) { - throw new Error( - `Unable to fire a "${eventName}" event - please provide a DOM element.`, - ) - } - const eventInit = {...defaultInit, ...init} - const {target: {value, files, ...targetProperties} = {}} = eventInit - if (value !== undefined) { - setNativeValue(node, value) - } - if (files !== undefined) { - // input.files is a read-only property so this is not allowed: - // input.files = [file] - // so we have to use this workaround to set the property - Object.defineProperty(node, 'files', { - configurable: true, - enumerable: true, - writable: true, - value: files, - }) - } - Object.assign(node, targetProperties) - const window = getWindowFromNode(node) - const EventConstructor = window[EventType] || window.Event - let event - /* istanbul ignore else */ - if (typeof EventConstructor === 'function') { - event = new EventConstructor(eventName, eventInit) - } else { - // IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill - event = window.document.createEvent(EventType) - const {bubbles, cancelable, detail, ...otherInit} = eventInit - event.initEvent(eventName, bubbles, cancelable, detail) - Object.keys(otherInit).forEach(eventKey => { - event[eventKey] = otherInit[eventKey] - }) - } - - // DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568 - const dataTransferProperties = ['dataTransfer', 'clipboardData'] - dataTransferProperties.forEach(dataTransferKey => { - const dataTransferValue = eventInit[dataTransferKey] - - if (typeof dataTransferValue === 'object') { - /* istanbul ignore if */ - if (typeof window.DataTransfer === 'function') { - Object.defineProperty(event, dataTransferKey, { - value: Object.getOwnPropertyNames(dataTransferValue).reduce( - (acc, propName) => { - Object.defineProperty(acc, propName, { - value: dataTransferValue[propName], - }) - return acc - }, - new window.DataTransfer(), - ), - }) - } else { - Object.defineProperty(event, dataTransferKey, { - value: dataTransferValue, - }) - } - } - }) - - return event -} - -Object.keys(eventMap).forEach(key => { - const {EventType, defaultInit} = eventMap[key] - const eventName = key.toLowerCase() - - createEvent[key] = (node, init) => - createEvent(eventName, node, init, {EventType, defaultInit}) - fireEvent[key] = (node, init) => fireEvent(node, createEvent[key](node, init)) -}) - -// function written after some investigation here: -// https://github.com/facebook/react/issues/10135#issuecomment-401496776 -function setNativeValue(element, value) { - const {set: valueSetter} = - Object.getOwnPropertyDescriptor(element, 'value') || {} - const prototype = Object.getPrototypeOf(element) - const {set: prototypeValueSetter} = - Object.getOwnPropertyDescriptor(prototype, 'value') || {} - if (prototypeValueSetter && valueSetter !== prototypeValueSetter) { - prototypeValueSetter.call(element, value) - } /* istanbul ignore next (I don't want to bother) */ else if (valueSetter) { - valueSetter.call(element, value) - } else { - throw new Error('The given element does not have a value setter') - } -} - -Object.keys(eventAliasMap).forEach(aliasKey => { - const key = eventAliasMap[aliasKey] - fireEvent[aliasKey] = (...args) => fireEvent[key](...args) -}) - -export {fireEvent, createEvent} - -/* eslint complexity:["error", 9] */ diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 00000000..07489a4b --- /dev/null +++ b/src/events.ts @@ -0,0 +1,295 @@ +import type { + AnyEventTarget, + ContrivedEventTarget, + CreateFunctionInit, + CreateObject, + EventMapValue, + EventMapValueEvent, + EventType, + FireFunction, + FireObject, +} from '../types' +import {getConfig} from './config' +import {eventAliasMap, eventMap} from './event-map' +import {getWindowFromNode} from './helpers' + +const _fireEvent: FireFunction & Partial = ( + element?: AnyEventTarget, + event?: Event, +): boolean => { + return getConfig().eventWrapper(() => { + if (!event) { + throw new Error( + `Unable to fire an event - please provide an event object.`, + ) + } + if (!element) { + throw new Error( + `Unable to fire a "${event.type}" event - please provide a DOM element.`, + ) + } + return element.dispatchEvent(event) + }) +} + +/** + * + * This function's return type is tricky. + * If in IE11 or other environments where window/globalThis does not contain properties for each type of DOM Event Class, + * it returns the value of `window.document.createEvent(typeof eventType) & init`. + * In all other environments, it simply returns the Event or Event-like object constructed by `new window[typeof eventType]()` + * + * I'm not sure how best to represent this in TS. For now, I will specify its return type as if it is invoked in the latter case. + */ +const __createEvent = ( + eventName: EVTNAME, + node?: N, + /** + * Caveat: the type of `init` is tricky. + * If in an environment that exposes an Event-extending class per Event type name in `window` or `globalThis`, + * its type is that of the corresponding EventInit or EventInit-like interface. + * + * Eg: If: + * + * 1. Aforemention environmental conditions are met + * 2. And eventName extends keyof typeof window (eg eventName === "MouseEvent", etc) + * 3. Then init will/should be (at least partially) MouseEventInit, as determined by the 2nd argument for the MouseEvent constructor, etc + * + * However, if in an environment (eg IE11) where there does not exist such Event classes in window/globalThis, or if said property is not a Class, + * `init` can be more correctly regarded as being of type Omit< MyEvent , 'bubbles' | 'cancelable' | 'detail' >, + * where MyEvent is the actual Event derivative being created. That is, `init` is not so much an EventInit-like object so much as an Event-like one. + * Technically, whatever `init` is, in the IE11 case, its properties will be merged with the Event that gets created, so it could have any shape, + * including one that might violate any constraints that assumed the return of this function to be a "legal" DOM Event or derivative thereof. + * + * Given that the IE11 case is the edge case, I have defined `init` somewhat broadly, but with preference given to its type as being that of an EventInit-like object. + * + * In all cases, init may optionally define 'dataTransfer' and 'clipboardData' properties as well, + * which are special cases for which the created Event object is explicity assigned as properties. + * No attempt is made to verify whether those properties would reasonably exist on an Event of the created type. + * + * In addition, `init` may also define a `target` property and a `detail` property. + * + * `target`: should most probably take the shape of the same `node` that this Event is created against, + * thus, eg. a click event's `target` prop should point to the element that was clicked on, + * which should be the element passed as the first param to this function. However, + * its usage implies the existence of a couple of optional properties - `files` and `value`, + * whose types (taken from HTMLInputElement, their most likely containing object) I've explicitly intersected with their sole accesses below. + * + * `detail`: is only correctly used in the first case from the above-mentioned environmental Event class considerations, where it is passed to the event constructor. + * In those cases its type is varied, and dependent on the Event in question. + * + * Part of their justification lies within usages of their types in this function, instances of which will be commented on inline. + * + */ + init?: CreateFunctionInit, + { + EventType: eventType = 'Event', + defaultInit = {}, + }: { + EventType?: EventMapValue['EventType'] + defaultInit?: Partial['defaultInit']> + } = {}, +): EventMapValueEvent => { + if (!node) { + throw new Error( + `Unable to fire a "${eventName}" event - please provide a DOM element.`, + ) + } + + const eventInit = { + ...defaultInit, + ...init, + } + const target = eventInit.target + const {value, files, ...targetProperties} = (target ?? + {}) as ContrivedEventTarget> + + // value is non-nullable, according to its type + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (value !== undefined) { + setNativeValue(node, value) + } + // files is non-nullable, according to its type + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (files !== undefined) { + // input.files is a read-only property so this is not allowed: + // input.files = [file] + // so we have to use this workaround to set the property + Object.defineProperty(node, 'files', { + configurable: true, + enumerable: true, + writable: true, + value: files, + }) + } + Object.assign(node, targetProperties) + // if/when `src/helpers.js` is TS-ified explicit type annotation for `window` will not be required. + // In addition, usage of window below involve no null-checks, + // suggesting that a nil type for `window` will never be the case, so I've non null asserted it here + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const window: Window & typeof globalThis = getWindowFromNode(node)! + // ostensibly window[eventType] is never undefined/null, + // so the falling back to window.Event can only exist if eslint is ignored here + // Though I am not sure, it might be an IE thing 🤷‍♀️ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const EventConstructor = window[eventType] ?? window.Event + let event: typeof EventConstructor['prototype'] + /* istanbul ignore else */ + if (typeof EventConstructor === 'function') { + event = new EventConstructor(eventName, eventInit) + } else { + // IE11 polyfill from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill + event = window.document.createEvent(eventType) + const {bubbles, cancelable, detail, ...otherInit} = eventInit + + event.initEvent( + eventName, + bubbles, + cancelable, + // @ts-expect-error I cannot find any resource on the internet or in lib.dom.d.ts of this mysterious 4th argument, + // except for the similar method `initCustomEvent` on `CustomEvent`. + // Is this a mistake? Or is `event` supposed to be a CustomEvent? Still wouldn't work, as method name is different! + detail, + ) + Object.keys(otherInit).forEach( + // @ts-expect-error `eventKey`'s type being explicitly cast to + // `keyof typeof Omit` + // is as good as what it actually is. For what it's worth, + // it would have been cast as such in each instance of its use to index both `event` and `otherInit` as Object.keys() returns an array of simple strings, + // which are not specified in any index signatures for either object. + // + // The reason `otherInit` is cast to Omit + // is because in this branch of the if-else, + // otherInit is a simple object whose propertes are intended to be merged with `event`, + // therefore, in terms of its usage in Object.keys().forEach(), + // it is as good as being a subset of `event`, + // for the purposes of easily using its keys to index `event` + // + // The other less verbose option is to explicitly asert `otherInit`'s type once during its assignment above + ( + eventKey: keyof Omit, + ) => { + event[eventKey] = ( + otherInit as Omit + )[eventKey] + }, + ) + } + + // DataTransfer is not supported in jsdom: https://github.com/jsdom/jsdom/issues/1568 + const dataTransferProperties = [ + 'dataTransfer' as const, + 'clipboardData' as const, + ] + dataTransferProperties.forEach(dataTransferKey => { + const dataTransferValue: null | DataTransfer = ( + eventInit as DragEvent & ClipboardEvent + )[dataTransferKey] + + if (typeof dataTransferValue === 'object') { + /* istanbul ignore if */ + if (typeof window.DataTransfer === 'function') { + Object.defineProperty(event, dataTransferKey, { + value: Object.getOwnPropertyNames(dataTransferValue).reduce( + (acc, propName) => { + Object.defineProperty(acc, propName, { + value: dataTransferValue?.[propName as keyof DataTransfer], + }) + return acc + }, + new window.DataTransfer(), + ), + }) + } else { + Object.defineProperty(event, dataTransferKey, { + value: dataTransferValue, + }) + } + } + }) + + return event +} + +const _createEvent: typeof __createEvent & Partial = __createEvent + +Object.keys(eventMap).forEach((( + key: Extract, +) => { + const evt = eventMap[key] + const {EventType: eType, defaultInit} = evt + const eventName = key.toLowerCase() as Lowercase + + // @ts-expect-error For some reason, I annot assign the RHS to the LHS for the following arg: + // Error below: + // + // Type '(node: N, init?: CreateFunctionInit<"copy" | "cut" | "paste" | "compositionEnd" | "compositionStart" | "compositionUpdate" | "keyDown" | "keyPress" | "keyUp" | "focus" | ... 72 more ... | "popState", N> | undefined) => Event | ... 14 more ... | PopStateEvent' is not assignable to type '((node: N, eventProperties?: CreateFunctionInit<"copy", N> | undefined) => ClipboardEvent) & ... 81 more ... & ((node: N, eventProperties?: CreateFunctionInit<...> | undefined) => PopStateEvent)'. + // Type '(node: N, init?: CreateFunctionInit<"copy" | "cut" | "paste" | "compositionEnd" | "compositionStart" | "compositionUpdate" | "keyDown" | "keyPress" | "keyUp" | "focus" | ... 72 more ... | "popState", N> | undefined) => Event | ... 14 more ... | PopStateEvent' is not assignable to type '(node: N, eventProperties?: CreateFunctionInit<"copy", N> | undefined) => ClipboardEvent'. + // Type 'Event | ClipboardEvent | CompositionEvent | UIEvent | KeyboardEvent | FocusEvent | InputEvent | ... 8 more ... | PopStateEvent' is not assignable to type 'ClipboardEvent'. + // Property 'clipboardData' is missing in type 'Event' but required in type 'ClipboardEvent'.ts(2322) + // lib.dom.d.ts(3593, 14): 'clipboardData' is declared here. + _createEvent[key] = ( + node: N, + init?: CreateFunctionInit, + ) => + _createEvent(eventName as typeof key, node, init, { + EventType: eType, + defaultInit, + }) + + _fireEvent[key] = ( + node: N, + /** + * This is in one sense CreateFunctionInit, + * but it is also technically and literally Parameters>[1] + * ( the second parameter of the function at this property ), + * the latter of which raises no errors. For whatever reason, + * the former does, even if that's what it has to be asserted as when passed to _createEvent[key]() + */ + init?: Parameters>[1], + ) => + _fireEvent( + node, + // non null assertion due to guaranteed assignment in previous line + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + _createEvent[key]!(node, init as CreateFunctionInit), + ) +}) as (value: string, index: number, array: string[]) => void) + +// function written after some investigation here: +// https://github.com/facebook/react/issues/10135#issuecomment-401496776 +function setNativeValue(element: AnyEventTarget, value: string) { + // eslint-disable-next-line @typescript-eslint/unbound-method + const {set: valueSetter} = + Object.getOwnPropertyDescriptor(element, 'value') ?? {} + const prototype: unknown = Object.getPrototypeOf(element) + // eslint-disable-next-line @typescript-eslint/unbound-method + const {set: prototypeValueSetter} = + Object.getOwnPropertyDescriptor(prototype, 'value') ?? {} + if (prototypeValueSetter && valueSetter !== prototypeValueSetter) { + prototypeValueSetter.call(element, value) + } /* istanbul ignore next (I don't want to bother) */ else if (valueSetter) { + valueSetter.call(element, value) + } else { + throw new Error('The given element does not have a value setter') + } +} + +;(Object.keys(eventAliasMap) as Array).forEach( + aliasKey => { + const key = eventAliasMap[aliasKey] + // @ts-expect-error What do we do here? Technically, _fireEvent[key] might not exist by this time, + // in which case the assignment will have the type of the correct Function | undefined, + // which violates the expected type. + // However, exposing fireEvent which this type would be a poor UX for users, + // and doesn't make sense given automated tssts would ensure the accessed "methods" do exist. + // Should I just ignore the error like this? + _fireEvent[aliasKey] = (...args: Parameters) => + _fireEvent[key]?.(...args) + }, +) + +export const fireEvent = _fireEvent as typeof _fireEvent & + Required> +export const createEvent = _createEvent as typeof _createEvent & + Required> diff --git a/types/__tests__/type-tests.ts b/types/__tests__/type-tests.ts index 9b5c81e8..8e99d907 100644 --- a/types/__tests__/type-tests.ts +++ b/types/__tests__/type-tests.ts @@ -134,6 +134,7 @@ export function testA11yHelper() { export function eventTest() { fireEvent.popState(window, { + // @ts-expect-error I cannot explain the presence of `location` in this object, as it is not part of PopStateEvent location: 'http://www.example.com/?page=1', state: {page: 1}, }) diff --git a/types/config.d.ts b/types/config.d.ts index c9c33633..c0c29db1 100644 --- a/types/config.d.ts +++ b/types/config.d.ts @@ -3,7 +3,7 @@ export interface Config { // eslint-disable-next-line @typescript-eslint/no-explicit-any asyncWrapper(cb: (...args: any[]) => any): Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any - eventWrapper(cb: (...args: any[]) => any): void + eventWrapper(cb: (...args: any[]) => any): ReturnType asyncUtilTimeout: number computedStyleSupportsPseudoElements: boolean defaultHidden: boolean diff --git a/types/events.d.ts b/types/events.d.ts index e9d57632..1bebe15d 100644 --- a/types/events.d.ts +++ b/types/events.d.ts @@ -1,110 +1,97 @@ +import {eventAliasMap, eventMap} from '../src/event-map' + export type EventType = - | 'copy' - | 'cut' - | 'paste' - | 'compositionEnd' - | 'compositionStart' - | 'compositionUpdate' - | 'keyDown' - | 'keyPress' - | 'keyUp' - | 'focus' - | 'blur' - | 'focusIn' - | 'focusOut' - | 'change' - | 'input' - | 'invalid' - | 'submit' - | 'reset' - | 'click' - | 'contextMenu' - | 'dblClick' - | 'drag' - | 'dragEnd' - | 'dragEnter' - | 'dragExit' - | 'dragLeave' - | 'dragOver' - | 'dragStart' - | 'drop' - | 'mouseDown' - | 'mouseEnter' - | 'mouseLeave' - | 'mouseMove' - | 'mouseOut' - | 'mouseOver' - | 'mouseUp' - | 'popState' - | 'select' - | 'touchCancel' - | 'touchEnd' - | 'touchMove' - | 'touchStart' - | 'resize' - | 'scroll' - | 'wheel' - | 'abort' - | 'canPlay' - | 'canPlayThrough' - | 'durationChange' - | 'emptied' - | 'encrypted' - | 'ended' - | 'loadedData' - | 'loadedMetadata' - | 'loadStart' - | 'pause' - | 'play' - | 'playing' - | 'progress' - | 'rateChange' - | 'seeked' - | 'seeking' - | 'stalled' - | 'suspend' - | 'timeUpdate' - | 'volumeChange' - | 'waiting' - | 'load' - | 'error' - | 'animationStart' - | 'animationEnd' - | 'animationIteration' - | 'transitionEnd' - | 'doubleClick' - | 'pointerOver' - | 'pointerEnter' - | 'pointerDown' - | 'pointerMove' - | 'pointerUp' - | 'pointerCancel' - | 'pointerOut' - | 'pointerLeave' - | 'gotPointerCapture' - | 'lostPointerCapture' + | keyof typeof eventMap + | keyof typeof eventAliasMap + | 'customEvent' + +export type EventMapValue = + EVTNAME extends keyof typeof eventMap + ? typeof eventMap[EVTNAME] + : EVTNAME extends keyof typeof eventAliasMap + ? typeof eventMap[typeof eventAliasMap[EVTNAME]] + : { + EventType: 'Event' + defaultInit: { + bubbles?: boolean + cancelable?: boolean + composed?: boolean + } + } + +export type EventMapValueEventClass = + typeof window[EventMapValue['EventType']] + +export type EventMapValueEvent = + EventMapValueEventClass['prototype'] + +export type EventMapValueEventInit = + EventMapValueEventClass extends { + new (arg0: string, arg1: infer initArg): EventMapValueEvent + } + ? NonNullable + : any + +export type EventInitFromEventClass = + E extends { + new (arg0: string, arg1?: infer initArg): Event + } + ? NonNullable + : any + +export type AnyEventTarget = HTMLElement | Node | Window + +export type ContrivedEventTarget< + E extends Partial = Partial, +> = Partial & { + value?: string + files?: FileList +} + +export type FireFunction = (element: AnyEventTarget, event?: Event) => boolean -export type FireFunction = ( - element: Document | Element | Window | Node, - event: Event, -) => boolean export type FireObject = { - [K in EventType]: ( - element: Document | Element | Window | Node, - options?: {}, + [K in EventType]: ( + element: N, + init?: CreateFunctionInit, ) => boolean } -export type CreateFunction = ( - eventName: string, - node: Document | Element | Window | Node, - init?: {}, - options?: {EventType?: string; defaultInit?: {}}, -) => Event + +export type CreateFunctionInit< + EVTNAME extends EventType = EventType, + N extends AnyEventTarget = HTMLElement, +> = Partial> & { + target?: ContrivedEventTarget | null + /** + * Should technically be `any` or some inferred type based on EventMapValueEventInit['detail'], + * but for some reason, the 2nd arg of the union of the Event or Event-derivative constructors in + * events.ts#__createEvent ( `event = new EventConstructor(eventName, eventInit)` ) expects details to be a number | undefined + * + * @type {number} + */ + detail?: number | undefined +} + +export type CreateFunction< + EVTNAME extends EventType = EventType, + N extends AnyEventTarget = HTMLElement, +> = ( + eventName: EVTNAME, + node?: N, + init?: CreateFunctionInit, + options?: { + EventType?: EventMapValue['EventType'] + defaultInit?: Partial['defaultInit']> + }, +) => EventMapValueEvent + export type CreateObject = { - [K in EventType]: ( - element: Document | Element | Window | Node, - options?: {}, - ) => Event + [K in EventType]: < + N extends AnyEventTarget = Document | Element | Window | Node, + >( + node: N, + eventProperties?: CreateFunctionInit, + ) => EventMapValueEvent } export const createEvent: CreateObject & CreateFunction