From 5675b8668c09345e064001784338a85b7bf9f2af Mon Sep 17 00:00:00 2001 From: Jake Boone Date: Mon, 22 Jan 2024 10:31:46 -0700 Subject: [PATCH] fix: Standalone types for "./matchers" export and add Bun support (#566) --- package.json | 12 +- .../bun/bun-custom-expect-types.test.ts | 98 +++++++++++++++ types/__tests__/bun/bun-types.test.ts | 118 ++++++++++++++++++ types/__tests__/bun/tsconfig.json | 9 ++ types/bun.d.ts | 11 ++ types/matchers-standalone.d.ts | 30 +++++ 6 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 types/__tests__/bun/bun-custom-expect-types.test.ts create mode 100644 types/__tests__/bun/bun-types.test.ts create mode 100644 types/__tests__/bun/tsconfig.json create mode 100644 types/bun.d.ts create mode 100644 types/matchers-standalone.d.ts diff --git a/package.json b/package.json index 1f6df71..f272177 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,11 @@ }, "./matchers": { "require": { - "types": "./types/matchers.d.ts", + "types": "./types/matchers-standalone.d.ts", "default": "./dist/matchers.js" }, "import": { - "types": "./types/matchers.d.ts", + "types": "./types/matchers-standalone.d.ts", "default": "./dist/matchers.mjs" } }, @@ -60,7 +60,7 @@ "setup": "npm install && npm run validate -s", "test": "kcd-scripts test", "test:update": "npm test -- --updateSnapshot --coverage", - "test:types": "tsc -p types/__tests__/jest && tsc -p types/__tests__/jest-globals && tsc -p types/__tests__/vitest", + "test:types": "tsc -p types/__tests__/jest && tsc -p types/__tests__/jest-globals && tsc -p types/__tests__/vitest && tsc -p types/__tests__/bun", "validate": "kcd-scripts validate && npm run test:types" }, "files": [ @@ -92,6 +92,8 @@ "devDependencies": { "@jest/globals": "^29.6.2", "@rollup/plugin-commonjs": "^25.0.4", + "@types/bun": "latest", + "@types/web": "latest", "expect": "^29.6.2", "jest-environment-jsdom-sixteen": "^1.0.3", "jest-watch-select-projects": "^2.0.0", @@ -105,6 +107,7 @@ }, "peerDependencies": { "@jest/globals": ">= 28", + "@types/bun": "latest", "@types/jest": ">= 28", "jest": ">= 28", "vitest": ">= 0.32" @@ -113,6 +116,9 @@ "@jest/globals": { "optional": true }, + "@types/bun": { + "optional": true + }, "@types/jest": { "optional": true }, diff --git a/types/__tests__/bun/bun-custom-expect-types.test.ts b/types/__tests__/bun/bun-custom-expect-types.test.ts new file mode 100644 index 0000000..051d785 --- /dev/null +++ b/types/__tests__/bun/bun-custom-expect-types.test.ts @@ -0,0 +1,98 @@ +/** + * File that tests whether the TypeScript typings work as expected. + */ + +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import {expect} from 'bun:test' +import * as matchersStandalone from '../../matchers-standalone' +import * as originalMatchers from '../../matchers' + +expect.extend(matchersStandalone) + +const element: HTMLElement = document.body + +function customExpect( + _actual: HTMLElement, +): + | originalMatchers.TestingLibraryMatchers + | originalMatchers.TestingLibraryMatchers> { + throw new Error('Method not implemented.') +} + +customExpect(element).toBeInTheDOM() +customExpect(element).toBeInTheDOM(document.body) +customExpect(element).toBeInTheDocument() +customExpect(element).toBeVisible() +customExpect(element).toBeEmpty() +customExpect(element).toBeDisabled() +customExpect(element).toBeEnabled() +customExpect(element).toBeInvalid() +customExpect(element).toBeRequired() +customExpect(element).toBeValid() +customExpect(element).toContainElement(document.body) +customExpect(element).toContainElement(null) +customExpect(element).toContainHTML('body') +customExpect(element).toHaveAttribute('attr') +customExpect(element).toHaveAttribute('attr', true) +customExpect(element).toHaveAttribute('attr', 'yes') +customExpect(element).toHaveClass() +customExpect(element).toHaveClass('cls1') +customExpect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') +customExpect(element).toHaveClass('cls1', {exact: true}) +customExpect(element).toHaveDisplayValue('str') +customExpect(element).toHaveDisplayValue(['str1', 'str2']) +customExpect(element).toHaveDisplayValue(/str/) +customExpect(element).toHaveDisplayValue([/str1/, 'str2']) +customExpect(element).toHaveFocus() +customExpect(element).toHaveFormValues({foo: 'bar', baz: 1}) +customExpect(element).toHaveStyle('display: block') +customExpect(element).toHaveStyle({display: 'block', width: 100}) +customExpect(element).toHaveTextContent('Text') +customExpect(element).toHaveTextContent(/Text/) +customExpect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) +customExpect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) +customExpect(element).toHaveValue() +customExpect(element).toHaveValue('str') +customExpect(element).toHaveValue(['str1', 'str2']) +customExpect(element).toHaveValue(1) +customExpect(element).toHaveValue(null) +customExpect(element).toBeChecked() +customExpect(element).toHaveDescription('some description') +customExpect(element).toHaveDescription(/some description/) +customExpect(element).toHaveDescription(expect.stringContaining('partial')) +customExpect(element).toHaveDescription() +customExpect(element).toHaveAccessibleDescription('some description') +customExpect(element).toHaveAccessibleDescription(/some description/) +customExpect(element).toHaveAccessibleDescription( + expect.stringContaining('partial'), +) +customExpect(element).toHaveAccessibleDescription() + +customExpect(element).toHaveAccessibleErrorMessage() +customExpect(element).toHaveAccessibleErrorMessage( + 'Invalid time: the time must be between 9:00 AM and 5:00 PM', +) +customExpect(element).toHaveAccessibleErrorMessage(/invalid time/i) +customExpect(element).toHaveAccessibleErrorMessage( + expect.stringContaining('Invalid time'), +) + +customExpect(element).toHaveAccessibleName('a label') +customExpect(element).toHaveAccessibleName(/a label/) +customExpect(element).toHaveAccessibleName( + expect.stringContaining('partial label'), +) +customExpect(element).toHaveAccessibleName() +customExpect(element).toHaveErrorMessage( + 'Invalid time: the time must be between 9:00 AM and 5:00 PM', +) +customExpect(element).toHaveErrorMessage(/invalid time/i) +customExpect(element).toHaveErrorMessage( + expect.stringContaining('Invalid time'), +) + +// @ts-expect-error The types accidentally allowed any property by falling back to "any" +customExpect(element).nonExistentProperty() diff --git a/types/__tests__/bun/bun-types.test.ts b/types/__tests__/bun/bun-types.test.ts new file mode 100644 index 0000000..ae03a17 --- /dev/null +++ b/types/__tests__/bun/bun-types.test.ts @@ -0,0 +1,118 @@ +/** + * File that tests whether the TypeScript typings for @types/jest work as expected. + */ + +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +import {expect} from 'bun:test' +import '../../bun' + +const element: HTMLElement = document.body + +expect(element).toBeInTheDOM() +expect(element).toBeInTheDOM(document.body) +expect(element).toBeInTheDocument() +expect(element).toBeVisible() +expect(element).toBeEmpty() +expect(element).toBeDisabled() +expect(element).toBeEnabled() +expect(element).toBeInvalid() +expect(element).toBeRequired() +expect(element).toBeValid() +expect(element).toContainElement(document.body) +expect(element).toContainElement(null) +expect(element).toContainHTML('body') +expect(element).toHaveAttribute('attr') +expect(element).toHaveAttribute('attr', true) +expect(element).toHaveAttribute('attr', 'yes') +expect(element).toHaveClass() +expect(element).toHaveClass('cls1') +expect(element).toHaveClass('cls1', 'cls2', 'cls3', 'cls4') +expect(element).toHaveClass('cls1', {exact: true}) +expect(element).toHaveDisplayValue('str') +expect(element).toHaveDisplayValue(['str1', 'str2']) +expect(element).toHaveDisplayValue(/str/) +expect(element).toHaveDisplayValue([/str1/, 'str2']) +expect(element).toHaveFocus() +expect(element).toHaveFormValues({foo: 'bar', baz: 1}) +expect(element).toHaveStyle('display: block') +expect(element).toHaveStyle({display: 'block', width: 100}) +expect(element).toHaveTextContent('Text') +expect(element).toHaveTextContent(/Text/) +expect(element).toHaveTextContent('Text', {normalizeWhitespace: true}) +expect(element).toHaveTextContent(/Text/, {normalizeWhitespace: true}) +expect(element).toHaveValue() +expect(element).toHaveValue('str') +expect(element).toHaveValue(['str1', 'str2']) +expect(element).toHaveValue(1) +expect(element).toHaveValue(null) +expect(element).toBeChecked() +expect(element).toHaveDescription('some description') +expect(element).toHaveDescription(/some description/) +expect(element).toHaveDescription(expect.stringContaining('partial')) +expect(element).toHaveDescription() +expect(element).toHaveAccessibleDescription('some description') +expect(element).toHaveAccessibleDescription(/some description/) +expect(element).toHaveAccessibleDescription(expect.stringContaining('partial')) +expect(element).toHaveAccessibleDescription() +expect(element).toHaveAccessibleName('a label') +expect(element).toHaveAccessibleName(/a label/) +expect(element).toHaveAccessibleName(expect.stringContaining('partial label')) +expect(element).toHaveAccessibleName() +expect(element).toHaveErrorMessage( + 'Invalid time: the time must be between 9:00 AM and 5:00 PM', +) +expect(element).toHaveErrorMessage(/invalid time/i) +expect(element).toHaveErrorMessage(expect.stringContaining('Invalid time')) + +expect(element).not.toBeInTheDOM() +expect(element).not.toBeInTheDOM(document.body) +expect(element).not.toBeInTheDocument() +expect(element).not.toBeVisible() +expect(element).not.toBeEmpty() +expect(element).not.toBeEmptyDOMElement() +expect(element).not.toBeDisabled() +expect(element).not.toBeEnabled() +expect(element).not.toBeInvalid() +expect(element).not.toBeRequired() +expect(element).not.toBeValid() +expect(element).not.toContainElement(document.body) +expect(element).not.toContainElement(null) +expect(element).not.toContainHTML('body') +expect(element).not.toHaveAttribute('attr') +expect(element).not.toHaveAttribute('attr', true) +expect(element).not.toHaveAttribute('attr', 'yes') +expect(element).not.toHaveClass() +expect(element).not.toHaveClass('cls1') +expect(element).not.toHaveClass('cls1', 'cls2', 'cls3', 'cls4') +expect(element).not.toHaveClass('cls1', {exact: true}) +expect(element).not.toHaveDisplayValue('str') +expect(element).not.toHaveDisplayValue(['str1', 'str2']) +expect(element).not.toHaveDisplayValue(/str/) +expect(element).not.toHaveDisplayValue([/str1/, 'str2']) +expect(element).not.toHaveFocus() +expect(element).not.toHaveFormValues({foo: 'bar', baz: 1}) +expect(element).not.toHaveStyle('display: block') +expect(element).not.toHaveTextContent('Text') +expect(element).not.toHaveTextContent(/Text/) +expect(element).not.toHaveTextContent('Text', {normalizeWhitespace: true}) +expect(element).not.toHaveTextContent(/Text/, {normalizeWhitespace: true}) +expect(element).not.toHaveValue() +expect(element).not.toHaveValue('str') +expect(element).not.toHaveValue(['str1', 'str2']) +expect(element).not.toHaveValue(1) +expect(element).not.toBeChecked() +expect(element).not.toHaveDescription('some description') +expect(element).not.toHaveDescription() +expect(element).not.toHaveAccessibleDescription('some description') +expect(element).not.toHaveAccessibleDescription() +expect(element).not.toHaveAccessibleName('a label') +expect(element).not.toHaveAccessibleName() +expect(element).not.toBePartiallyChecked() +expect(element).not.toHaveErrorMessage() +expect(element).not.toHaveErrorMessage('Pikachu!') + +// @ts-expect-error The types accidentally allowed any property by falling back to "any" +expect(element).nonExistentProperty() diff --git a/types/__tests__/bun/tsconfig.json b/types/__tests__/bun/tsconfig.json new file mode 100644 index 0000000..2a7ced0 --- /dev/null +++ b/types/__tests__/bun/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["bun", "web"] + }, + "include": ["*.ts"] +} diff --git a/types/bun.d.ts b/types/bun.d.ts new file mode 100644 index 0000000..676a0ac --- /dev/null +++ b/types/bun.d.ts @@ -0,0 +1,11 @@ +import {type expect} from 'bun:test' +import {type TestingLibraryMatchers} from './matchers' + +export {} +declare module 'bun:test' { + interface Matchers + extends TestingLibraryMatchers< + ReturnType, + T + > {} +} diff --git a/types/matchers-standalone.d.ts b/types/matchers-standalone.d.ts new file mode 100644 index 0000000..b8dea83 --- /dev/null +++ b/types/matchers-standalone.d.ts @@ -0,0 +1,30 @@ +import {type TestingLibraryMatchers} from './matchers' + +type TLM = TestingLibraryMatchers + +interface MatcherReturnType { + pass: boolean + message: () => string +} + +interface OverloadedMatchers { + toHaveClass: (expected: any, ...rest: string[]) => MatcherReturnType + toHaveClass: ( + expected: any, + className: string, + options?: {exact: boolean}, + ) => MatcherReturnType +} + +declare namespace matchersStandalone { + type MatchersStandalone = { + [T in keyof TLM]: ( + expected: any, + ...rest: Parameters + ) => MatcherReturnType + } & OverloadedMatchers +} + +declare const matchersStandalone: matchersStandalone.MatchersStandalone & + Record +export = matchersStandalone