From ce7b824364bceccdc5a8eb38ca87f319cdb958ac Mon Sep 17 00:00:00 2001 From: Tim Yates Date: Fri, 9 Aug 2019 14:30:24 -0500 Subject: [PATCH] Add typings Followed recommendations in [dtslint tool](https://github.com/microsoft/dtslint) and [TypeScript documentation](http://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html). Took tsconfig.json and tslint.json from @testing-library/dom, although it looks like they just copied the examples in the dtslint docs. Closes #60. --- package-lock.json | 145 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 ++ types/index.d.ts | 52 ++++++++++++++++ types/test.ts | 131 +++++++++++++++++++++++++++++++++++++++ types/tsconfig.json | 14 +++++ types/tslint.json | 7 +++ 6 files changed, 353 insertions(+) create mode 100644 types/index.d.ts create mode 100644 types/test.ts create mode 100644 types/tsconfig.json create mode 100644 types/tslint.json diff --git a/package-lock.json b/package-lock.json index beedc358..44dbf460 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1226,6 +1226,12 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/parsimmon": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@types/parsimmon/-/parsimmon-1.10.0.tgz", + "integrity": "sha512-bsTIJFVQv7jnvNiC42ld2pQW2KRI+pAG243L+iATvqzy3X6+NH1obz2itRKDZZ8VVhN3wjwYax/VBGCcXzgTqQ==", + "dev": true + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -1919,6 +1925,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -2558,6 +2570,16 @@ } } }, + "definitelytyped-header-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/definitelytyped-header-parser/-/definitelytyped-header-parser-1.2.0.tgz", + "integrity": "sha512-xpg8uu/2YD/reaVsZV4oJ4g7UDYFqQGWvT1W9Tsj6q4VtWBSaig38Qgah0ZMnQGF9kAsAim08EXDO1nSi0+Nog==", + "dev": true, + "requires": { + "@types/parsimmon": "^1.3.0", + "parsimmon": "^1.2.0" + } + }, "del": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/del/-/del-5.0.0.tgz", @@ -2583,6 +2605,12 @@ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", "dev": true }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, "diff-sequences": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", @@ -2629,6 +2657,55 @@ "webidl-conversions": "^4.0.2" } }, + "download-file-sync": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/download-file-sync/-/download-file-sync-1.0.4.tgz", + "integrity": "sha1-0+PFQ/g29BA5RVuQNMcuNVsDYBk=", + "dev": true + }, + "dts-critic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dts-critic/-/dts-critic-2.0.0.tgz", + "integrity": "sha512-oYo69B8NcjUoDhKh8oUl58ljMsgyWnPR0Qf3QKJmKjm5LvkQ21v4SW+QCOleI1Cs0HRN6zEYllvQEORGd45wOQ==", + "dev": true, + "requires": { + "definitelytyped-header-parser": "^1.2.0", + "download-file-sync": "^1.0.4", + "semver": "^6.2.0", + "yargs": "^12.0.5" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "dtslint": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/dtslint/-/dtslint-0.9.1.tgz", + "integrity": "sha512-IxGanh+u1psQscSHX30cDqV6oQaSs5bhCIRhE4VriRFPOvqOR3vZPq/N6238ft5rS9P64z+oCZ+8kktXc2uCbQ==", + "dev": true, + "requires": { + "definitelytyped-header-parser": "1.2.0", + "dts-critic": "^2.0.0", + "fs-extra": "^6.0.1", + "request": "^2.88.0", + "strip-json-comments": "^2.0.1", + "tslint": "5.14.0", + "typescript": "^3.6.0-dev.20190809" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + } + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -3598,6 +3675,17 @@ "map-cache": "^0.2.2" } }, + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -5630,6 +5718,15 @@ } } }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -6691,6 +6788,12 @@ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", "dev": true }, + "parsimmon": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.13.0.tgz", + "integrity": "sha512-5UIrOCW+gjbILkjKPgTgmq8LKf8TT3Iy7kN2VD7OtQ81facKn8B4gG1X94jWqXYZsxG2KbJhrv/Yq/5H6BQn7A==", + "dev": true + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -8026,6 +8129,36 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tslint": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.14.0.tgz", + "integrity": "sha512-IUla/ieHVnB8Le7LdQFRGlVJid2T/gaJe5VkjzRVSRR6pA2ODYrnfR1hmxi+5+au9l50jBwpbBL34txgv4NnTQ==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -8056,6 +8189,12 @@ "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true }, + "typescript": { + "version": "3.6.0-dev.20190809", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.0-dev.20190809.tgz", + "integrity": "sha512-cESW57Zs7B6EJspZ1jBt1yYxSY7wcCGPbaWd2KgtOYjwC+IUgow1IRCl2QrfIYy6+JUfTAkBxUh5bc4nF3D9XA==", + "dev": true + }, "uglify-js": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", @@ -8116,6 +8255,12 @@ "set-value": "^2.0.1" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", diff --git a/package.json b/package.json index 3eaf595a..4715ca94 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,18 @@ "version": "1.2.0", "description": "Simple and complete Vue DOM testing utilities that encourage good testing practices.", "main": "dist/vue-testing-library.js", + "types": "types", "scripts": { "lint": "eslint --ext .js,.vue .", "lint:fix": "npm run lint -- --fix", + "dtslint": "dtslint types", "test": "jest --coverage", "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", "prepublishOnly": "babel src --out-dir dist" }, "files": [ "dist", + "types/index.d.ts", "cleanup-after-each.js" ], "repository": { @@ -46,6 +49,7 @@ "babel-eslint": "^10.0.2", "babel-jest": "^24.8.0", "coveralls": "^3.0.5", + "dtslint": "^0.9.1", "eslint": "^6.1.0", "eslint-config-prettier": "^6.0.0", "eslint-config-standard": "^13.0.1", diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..8e2d7dcf --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,52 @@ +// TypeScript Version: 3.0 +import { ThisTypedMountOptions, VueClass } from '@vue/test-utils' +import Vue from 'vue' +import { Store, StoreOptions } from 'vuex' +import Router, { RouterOptions, RouteConfig } from 'vue-router' +import { getQueriesForElement, BoundFunctions, queries, EventType, FireFunction } from '@testing-library/dom' + +// NOTE: fireEvent is overridden below +export * from '@testing-library/dom' + +export function cleanup(): void + +export interface RenderOptions extends + // The props and store options special-cased by vue-testing-library and NOT passed to mount(). + // In TS 3.5+: Omit, "store" | "props"> + Pick, Exclude, "store" | "props">> { + props?: object + store?: StoreOptions + routes?: RouteConfig[] +} + +export type ConfigurationCallback = + (vue: V, store: Store, router: Router) => Partial> | void + +export type ComponentHarness = BoundFunctions & { + container: HTMLElement + baseElement: HTMLBodyElement + debug(el?: HTMLElement): void + unmount(): void + isUnmounted(): boolean + html(): string + emitted(): { [name: string]: any[][] } + updateProps(props: object): Promise +} + +export function render( + TestComponent: VueClass, + options?: RenderOptions, + configure?: ConfigurationCallback +): ComponentHarness + +export type AsyncFireObject = { + [K in EventType]: (element: Document | Element | Window, options?: {}) => Promise +} + +export interface VueFireObject extends AsyncFireObject { + touch(element: Document | Element | Window): Promise + update(element: HTMLOptionElement): Promise + update(element: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement, value?: string): Promise +} + +export const fireEvent: FireFunction & VueFireObject diff --git a/types/test.ts b/types/test.ts new file mode 100644 index 00000000..95a8a038 --- /dev/null +++ b/types/test.ts @@ -0,0 +1,131 @@ +import * as lib from '@testing-library/vue' +import Vue from 'vue' + +declare const elem: HTMLElement +declare const input: HTMLInputElement +declare const select: HTMLSelectElement +declare const option: HTMLOptionElement +declare const textarea: HTMLTextAreaElement + +const SomeComponent = Vue.extend({ + name: 'SomeComponent', + props: { + foo: Number, + bar: String, + }, +}) + +lib.cleanup() // $ExpectType void + +const component = lib.render(SomeComponent, { + props: { + foo: 9, + bar: "x", + }, + store: { + state: { + foos: [4, 5], + bars: ["a", "b"], + }, + getters: { + fooCount() { return this.foos.length }, + }, + }, + routes: [ + {path: '/', name: 'home', component: SomeComponent}, + {path: '/about', name: 'about', component: () => Promise.resolve(SomeComponent)}, + ], +}) + +component.container // $ExpectType HTMLElement +component.baseElement // $ExpectType HTMLBodyElement +component.debug() // $ExpectType void +component.debug(elem) // $ExpectType void +component.unmount() // $ExpectType void +component.isUnmounted() // $ExpectType boolean +component.html() // $ExpectType string +component.emitted() // $ExpectType { [name: string]: any[][]; } +component.updateProps({foo: "bar"}) // $ExpectType Promise + +// Has bound queries +component.getByLabelText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement +component.getByPlaceholderText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +component.getByAltText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +component.getByTitle(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +component.getByDisplayValue(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +component.getByRole(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +component.getByTestId(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +component.getByText("foo") // $ExpectType HTMLElement +component.getByText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement +component.getAllByText("foo") // $ExpectType HTMLElement[] +component.getAllByText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement[] +component.queryByText("foo") // $ExpectType HTMLElement | null +component.queryByText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement | null +component.queryAllByText("foo") // $ExpectType HTMLElement[] +component.queryAllByText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement[] +component.findByText("foo") // $ExpectType Promise +component.findByText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType Promise +component.findAllByText("foo") // $ExpectType Promise +component.findAllByText(/some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType Promise + +// Reexports queries from dom-testing-library +lib.getByLabelText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement +lib.getByPlaceholderText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +lib.getByAltText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +lib.getByTitle(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +lib.getByDisplayValue(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +lib.getByRole(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +lib.getByTestId(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x}) // $ExpectType HTMLElement +lib.getByText(elem, "foo") // $ExpectType HTMLElement +lib.getByText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement +lib.getAllByText(elem, "foo") // $ExpectType HTMLElement[] +lib.getAllByText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement[] +lib.queryByText(elem, "foo") // $ExpectType HTMLElement | null +lib.queryByText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement | null +lib.queryAllByText(elem, "foo") // $ExpectType HTMLElement[] +lib.queryAllByText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType HTMLElement[] +lib.findByText(elem, "foo") // $ExpectType Promise +lib.findByText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType Promise +lib.findAllByText(elem, "foo") // $ExpectType Promise +lib.findAllByText(elem, /some text/, {exact: true, trim: false, collapseWhitespace: true, normalizer: x => x, selector: '*'}) // $ExpectType Promise + +// Reexports event functions from dom-testing-library +// Note that it does NOT make the core fireEvent() function asynchronous +lib.fireEvent(elem, new Event('change')) // $ExpectType boolean +lib.createEvent.click(elem) // $ExpectType Event +lib.createEvent.click(elem, {foo: "bar"}) // $ExpectType Event +lib.createEvent.keyDown(elem) // $ExpectType Event +lib.createEvent.mouseEnter(elem) // $ExpectType Event + +// Changes fireEvent[event]() to be asynchronous +lib.fireEvent.click(elem) // $ExpectType Promise +lib.fireEvent.click(elem, {foo: "bar"}) // $ExpectType Promise +lib.fireEvent.keyDown(elem) // $ExpectType Promise +lib.fireEvent.mouseEnter(elem) // $ExpectType Promise + +// Adds update() function +lib.fireEvent.update(input, "foo") // $ExpectType Promise +lib.fireEvent.update(select, "bar") // $ExpectType Promise +lib.fireEvent.update(textarea, "some\ntext") // $ExpectType Promise +lib.fireEvent.update(option) // $ExpectType Promise + +// Adds touch() function +lib.fireEvent.touch(elem) // $ExpectType Promise + +// Reexports async functions from dom-testing-library +lib.wait() // $ExpectType Promise +lib.wait(() => { throw new Error("nope") }, {timeout: 3000, interval: 100}) +lib.waitForDomChange({container: select, timeout: 3000, mutationObserverOptions: {subtree: false}}) +lib.waitForElement(() => input) // $ExpectType Promise +lib.waitForElement(() => option, {container: select, timeout: 3000, mutationObserverOptions: {subtree: false}}) // $ExpectType Promise +lib.waitForElementToBeRemoved(() => input) // $ExpectType Promise +lib.waitForElementToBeRemoved(() => option, {container: select, timeout: 3000, mutationObserverOptions: {subtree: false}}) // $ExpectType Promise + +// Reexports utilities from dom-testing-library +lib.buildQueries((el: HTMLElement) => [el], (_: HTMLElement) => "something", (_: HTMLElement) => "error") +lib.within(elem) +lib.getQueriesForElement(elem) +lib.getNodeText(elem) // $ExpectType string +// lib.getRoles(elem) // $ExpectType { [name: string]: string[]; } +lib.prettyDOM(elem) // $ExpectType string | false +// lib.logRoles(elem) // $ExpectType string | false diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 00000000..8ec801c3 --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": ["es6", "dom"], + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "noEmit": true, + + "baseUrl": ".", + "paths": {"@testing-library/vue": ["."]} + } +} diff --git a/types/tslint.json b/types/tslint.json new file mode 100644 index 00000000..286a004f --- /dev/null +++ b/types/tslint.json @@ -0,0 +1,7 @@ +{ + "extends": "dtslint/dtslint.json", + "rules": { + "semicolon": [true, "never"], + "whitespace": [false] + } +}