diff --git a/biome.jsonc b/biome.jsonc index 58b45c79c..82c117945 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,6 +1,7 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, + "organizeImports": { "enabled": true }, "formatter": { "formatWithErrors": true, "indentStyle": "space", @@ -8,35 +9,41 @@ "lineEnding": "lf", "lineWidth": 80 }, - "organizeImports": { "enabled": true }, - "javascript": { - "globals": ["chrome"], - "formatter": { - "semicolons": "always", - "trailingCommas": "all", - "quoteStyle": "single" - } - }, "linter": { "rules": { "recommended": true, "complexity": { "noForEach": "off", - "useSimplifiedLogicExpression": "info" + "noUselessStringConcat": "warn", + "noUselessUndefinedInitialization": "warn", + "useSimplifiedLogicExpression": "info", + "useDateNow": "error" + }, + "correctness": { + "noUndeclaredDependencies": "warn", + "noUndeclaredVariables": "warn", + "noUnusedFunctionParameters": "warn", + "noUnusedImports": "error" }, "performance": { "noBarrelFile": "info", "noReExportAll": "info" }, "style": { + "noDoneCallback": "warn", "noNamespace": "error", + "noNamespaceImport": "warn", "noNegationElse": "error", "noNonNullAssertion": "off", "noParameterProperties": "error", "noRestrictedGlobals": "error", "noShoutyConstants": "error", + "noYodaExpression": "warn", "useCollapsedElseIf": "error", + "useConsistentBuiltinInstantiation": "error", + "useDefaultSwitchClause": "warn", "useEnumInitializers": "off", + "useExplicitLengthCheck": "warn", "useNamingConvention": { "level": "error", "options": { "strictCase": false } @@ -44,7 +51,9 @@ "useShorthandArrayType": "error", "useShorthandAssign": "error", "useSingleCaseStatement": "info", - "useTemplate": "off" + "useTemplate": "off", + "useThrowNewError": "error", + "useThrowOnlyError": "warn" }, "suspicious": { "noApproximativeNumericConstant": "error", @@ -52,53 +61,40 @@ "noConfusingVoidType": "off", "noConsoleLog": "warn", "noConstEnum": "off", - "noExplicitAny": "off", - "noFocusedTests": "error", - "noMisrefactoredShorthandAssign": "error" - }, - "nursery": { - "noDoneCallback": "warn", "noDuplicateAtImportRules": "error", - "noDuplicateElseIf": "error", "noDuplicateFontNames": "error", - "noDuplicateJsonKeys": "error", "noDuplicateSelectorsKeyframeBlock": "error", "noEmptyBlock": "warn", "noEvolvingTypes": "warn", - "noImportantInKeyframe": "error", - "noInvalidDirectionInLinearGradient": "error", - "noInvalidPositionAtImportRule": "error", - "noLabelWithoutControl": "warn", + "noExplicitAny": "off", + "noFocusedTests": "error", "noMisplacedAssertion": "error", - "noShorthandPropertyOverrides": "warn", + "noMisrefactoredShorthandAssign": "error", + "useErrorMessage": "warn" + }, + "nursery": { + "noCommonJs": "error", + "noDuplicateElseIf": "error", + "noDynamicNamespaceImportAccess": "error", + "noEnum": "error", + "noExportedImports": "error", + "noSecrets": "warn", // TODO: Change to "error" when more stable "noSubstr": "warn", - "noUndeclaredDependencies": "warn", - "noUnknownFunction": "warn", - "noUnknownMediaFeatureName": "warn", - "noUnknownProperty": "warn", - "noUnknownPseudoClassSelector": "warn", - "noUnknownSelectorPseudoElement": "warn", - "noUnknownUnit": "error", - "noUnmatchableAnbSelector": "warn", - "noUnusedFunctionParameters": "warn", - "noUselessStringConcat": "warn", - "noUselessUndefinedInitialization": "warn", - "noYodaExpression": "warn", + "noUnknownPseudoClass": "warn", + "noUnknownPseudoElement": "warn", "useAdjacentOverloadSignatures": "error", - "useConsistentBuiltinInstantiation": "error", - "useConsistentGridAreas": "warn", - "useDateNow": "error", - "useDefaultSwitchClause": "warn", - "useErrorMessage": "warn", - // "useExplicitLengthCheck": "warn", - "useFocusableInteractive": "warn", - "useGenericFontNames": "error", - "useThrowNewError": "error", - "useThrowOnlyError": "warn", "useValidAutocomplete": "warn" } } }, + "javascript": { + "globals": ["chrome", "Timer"], // TODO: Remove `Timer`; only used as type (from Bun globals) + "formatter": { + "semicolons": "always", + "trailingCommas": "all", + "quoteStyle": "single" + } + }, "overrides": [ { "include": [".vscode/*.json", "tsconfig*.json"], @@ -116,26 +112,33 @@ }, "linter": { "rules": { - "nursery": { + "correctness": { "noUndeclaredDependencies": "off" } } + }, + "javascript": { + "globals": ["$console", "Bun", "chrome", "happyDOM", "Loader"] } }, { - "include": ["build.ts", "*.config.ts"], + "include": ["*.config.mjs", "*.config.ts", "*.d.ts", "build.ts"], "linter": { "rules": { + "correctness": { + "noUndeclaredDependencies": "off" + }, "style": { + "noNamespaceImport": "off", "useNamingConvention": "off" }, "suspicious": { "noConsoleLog": "off" - }, - "nursery": { - "noUndeclaredDependencies": "off" } } + }, + "javascript": { + "globals": ["Bun", "chrome"] } } ] diff --git a/build.ts b/build.ts index df19a4f63..4fb63ed30 100644 --- a/build.ts +++ b/build.ts @@ -31,12 +31,13 @@ function compileCSS(src: string, from: string) { } } + // TODO: Migrate to bun CSS handling (which is based on lightningcss). const minified = lightningcss.transform({ filename: from, - code: Buffer.from(compiled.css), + code: new TextEncoder().encode(compiled.css), minify: !dev, // eslint-disable-next-line no-bitwise - targets: { chrome: 116 << 16 }, // matches manifest minimum_chrome_version + targets: { chrome: 123 << 16 }, // matches manifest minimum_chrome_version }); for (const warning of minified.warnings) { @@ -136,7 +137,7 @@ const out = await Bun.build({ outdir: 'dist', target: 'browser', minify: !dev, - sourcemap: dev ? 'external' : 'none', + sourcemap: dev ? 'linked' : 'none', }); console.timeEnd('build'); console.log(out); @@ -148,6 +149,7 @@ const out2 = await Bun.build({ outdir: 'dist', target: 'browser', minify: !dev, + sourcemap: dev ? 'linked' : 'none', }); console.timeEnd('build2'); console.log(out2); diff --git a/bun.lockb b/bun.lockb index cdc586c54..ecf5e7edd 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.mjs b/eslint.config.mjs index 7899b38ed..8ededf70a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,69 +1,35 @@ -import { fixupPluginRules } from '@eslint/compat'; -import { FlatCompat } from '@eslint/eslintrc'; import eslint from '@eslint/js'; +import mm from '@maxmilton/eslint-config'; import unicorn from 'eslint-plugin-unicorn'; -// eslint-disable-next-line import/no-unresolved -import tseslint from 'typescript-eslint'; - -const compat = new FlatCompat({ - baseDirectory: import.meta.dirname, -}); +import ts from 'typescript-eslint'; const OFF = 0; -const WARN = 1; +// const WARN = 1; const ERROR = 2; -export default tseslint.config( +export default ts.config( eslint.configs.recommended, - ...compat.extends('airbnb-base').map((config) => ({ - ...config, - plugins: {}, // delete - })), - ...compat.extends('airbnb-typescript/base'), - ...tseslint.configs.strictTypeChecked, - ...tseslint.configs.stylisticTypeChecked, - // @ts-expect-error - no types - // eslint-disable-next-line + ...ts.configs.strictTypeChecked, + ...ts.configs.stylisticTypeChecked, unicorn.configs['flat/recommended'], + mm.configs.recommended, { linterOptions: { - reportUnusedDisableDirectives: WARN, + reportUnusedDisableDirectives: ERROR, }, languageOptions: { parserOptions: { project: ['tsconfig.json', 'tsconfig.node.json'], + projectService: { + allowDefaultProject: ['*.js', '*.cjs', '*.mjs'], + // defaultProject: './tsconfig.node.json', + }, tsconfigRootDir: import.meta.dirname, }, }, - plugins: { - import: fixupPluginRules( - compat.plugins('eslint-plugin-import')[0].plugins?.import ?? {}, - ), - }, rules: { - '@typescript-eslint/explicit-module-boundary-types': ERROR, - '@typescript-eslint/no-confusing-void-expression': WARN, - '@typescript-eslint/no-non-null-assertion': WARN, - '@typescript-eslint/no-use-before-define': WARN, - 'import/prefer-default-export': OFF, - 'no-restricted-syntax': OFF, - 'no-void': OFF, - 'unicorn/filename-case': OFF, - 'unicorn/import-style': WARN, - 'unicorn/no-abusive-eslint-disable': WARN, - 'unicorn/no-null': OFF, - 'unicorn/prefer-module': WARN, - 'unicorn/prefer-top-level-await': WARN, - 'unicorn/prevent-abbreviations': OFF, - - /* Covered by biome formatter */ - '@typescript-eslint/indent': OFF, - 'function-paren-newline': OFF, - 'implicit-arrow-linebreak': OFF, - 'max-len': OFF, - 'object-curly-newline': OFF, - 'operator-linebreak': OFF, - 'unicorn/no-nested-ternary': OFF, + // FIXME: Remove this once fixed upstream (incorrectly reports chrome as deprecated). + '@typescript-eslint/no-deprecated': OFF, /* Performance and byte savings */ // alternatives offer byte savings and better performance @@ -89,6 +55,9 @@ export default tseslint.config( // byte savings (minification doesn't currently automatically remove) 'unicorn/switch-case-braces': [ERROR, 'avoid'], + // prefer to clearly separate Bun and DOM + 'unicorn/prefer-global-this': OFF, + /* stage1 */ // underscores in synthetic event handler names 'no-underscore-dangle': OFF, @@ -97,20 +66,6 @@ export default tseslint.config( 'unicorn/prefer-query-selector': OFF, }, }, - { - files: [ - '*.config.mjs', - '*.config.ts', - '*.d.ts', - '**/*.spec.ts', - '**/*.test.ts', - 'build.ts', - 'test/**', - ], - rules: { - 'import/no-extraneous-dependencies': OFF, - }, - }, { files: ['build.ts'], rules: { @@ -119,6 +74,6 @@ export default tseslint.config( }, }, { - ignores: ['**/*.bak', 'dist/**'], + ignores: ['**/*.bak', 'coverage/**', 'dist/**'], }, ); diff --git a/manifest.config.ts b/manifest.config.ts index 0283a5c91..893ee0788 100644 --- a/manifest.config.ts +++ b/manifest.config.ts @@ -38,7 +38,7 @@ export const createManifest = ( version: pkg.version, // shippable releases should not have a named version version_name: debug ? gitRef() : undefined, - minimum_chrome_version: '116', // for new password manager link + minimum_chrome_version: '123', // for light-dark() CSS function icons: { 16: 'icon16.png', 48: 'icon48.png', @@ -80,5 +80,6 @@ export const createManifest = ( cross_origin_opener_policy: { value: 'same-origin' }, // https://chrome.google.com/webstore/detail/new-tab/cpcibnbdmpmcmnkhoiilpnlaepkepknb + // biome-ignore lint/nursery/noSecrets: not a secret key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk9BfRa5CXuCX1ElY0yu9kJSqxFirFtSy79ZR/fyKHdOzZurQXNmhIyxVnQXd2bxHvuKUyZGahm/gwgyyzGuxhsQEue6wTD9TnOvvM2vusXpnoCr6Ili7sBwUo9vA2aPI77NB0eArXz9WWNmoDWW5WEqI/rk26Tinl8SNU9iDJISbL+dMses1QPw64oYFWB1J4JeB1MhXnzTxECpGZTn33LhgBU4J3ooT6eoqrsJdRvuc0vjPMxq/jfqLkdBbzlsnrMbgtDoJ9WiWj2lA0MzHGDAQ8HgnMEi3SpXRNnod9CCBnxmkHqv3u4u7Tvp/WLAgJ+QjCt+9yYyw3nOYHpEweQIDAQAB', }); diff --git a/package.json b/package.json index f2ee0b468..89f3743bf 100644 --- a/package.json +++ b/package.json @@ -21,36 +21,28 @@ "patchedDependencies": { "happy-dom@14.12.3": "patches/happy-dom@14.12.3.patch" }, - "overrides": { - "lightningcss": "1.23.0" - }, "dependencies": { "stage1": "0.8.0-next.13" }, "devDependencies": { - "@biomejs/biome": "1.8.3", - "@ekscss/plugin-import": "0.0.14", - "@eslint/compat": "1.2.0", - "@eslint/eslintrc": "3.1.0", - "@eslint/js": "9.12.0", + "@biomejs/biome": "1.9.4", + "@ekscss/plugin-import": "0.0.15", + "@eslint/js": "9.13.0", + "@maxmilton/eslint-config": "0.0.3", "@maxmilton/stylelint-config": "0.1.2", - "@playwright/test": "1.48.0", - "@types/bun": "1.1.11", - "@types/chrome": "0.0.271", - "@types/eslint__eslintrc": "2.1.2", - "@types/eslint__js": "8.42.3", - "ekscss": "0.0.18", - "eslint": "9.12.0", - "eslint-config-airbnb-base": "15.0.0", - "eslint-config-airbnb-typescript": "18.0.0", - "eslint-plugin-import": "2.31.0", - "eslint-plugin-unicorn": "55.0.0", + "@maxmilton/test-utils": "0.0.2", + "@playwright/test": "1.48.2", + "@types/bun": "1.1.12", + "@types/chrome": "0.0.279", + "ekscss": "0.0.20", + "eslint": "9.13.0", + "eslint-plugin-unicorn": "56.0.0", "happy-dom": "14.12.3", - "lightningcss": "1.25.1", + "lightningcss": "1.27.0", "stylelint": "16.10.0", "stylelint-config-standard": "36.0.1", - "terser": "5.34.1", - "typescript": "5.5.3", - "typescript-eslint": "7.16.1" + "terser": "5.36.0", + "typescript": "5.6.3", + "typescript-eslint": "8.11.0" } } diff --git a/src/components/BookmarkBar.ts b/src/components/BookmarkBar.ts index 8e9ea03b3..0e474be6d 100644 --- a/src/components/BookmarkBar.ts +++ b/src/components/BookmarkBar.ts @@ -92,6 +92,7 @@ export const BookmarkBar = (): BookmarkBarComponent => { // before the CSS has loaded. Styles are needed to calculate the bookmark // item widths, so wait until the CSS is ready. const waitForStylesThenResize = () => { + // biome-ignore lint/style/useExplicitLengthCheck: byte savings if (document.styleSheets.length) { resize(); } else { diff --git a/src/components/BookmarkNode.ts b/src/components/BookmarkNode.ts index 7346cf307..664995a54 100644 --- a/src/components/BookmarkNode.ts +++ b/src/components/BookmarkNode.ts @@ -17,7 +17,7 @@ folderPopupView.className = 'sf'; const FolderPopup = ( parent: HTMLElement, children: BookmarkTreeNode[], - nested?: boolean | undefined, + nested?: boolean, ): FolderPopupComponent => { const root = clone(folderPopupView); const parentRect = parent.getBoundingClientRect(); @@ -38,6 +38,7 @@ const FolderPopup = ( window.innerHeight - top }px`; + // biome-ignore lint/style/useExplicitLengthCheck: byte savings if (children.length) { children.forEach((item) => append(BookmarkNode(item, true), root)); } else { diff --git a/src/components/Search.ts b/src/components/Search.ts index a0af899d4..3e299c59f 100644 --- a/src/components/Search.ts +++ b/src/components/Search.ts @@ -139,6 +139,12 @@ export const Search = (): SearchComponent => { // TODO: Keep? Causes significantly worse page load speed! + // TODO: Alternative implementation? One of: + // ↳ Simple lock to disable updates when the page isn't active + // ↳ Web Locks API to prevent multiple pages from updating together + // ↳ Shared worker to manage the state of the open tabs + // ↳ Only update specific change from listener Event + // // When the page isn't active stop the "Open Tabs" section from updating to // // prevent performance issues when users open many new-tab pages. // document.onvisibilitychange = () => { diff --git a/src/settings.ts b/src/settings.ts index aa3a8c4c2..8a9dbd115 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -219,7 +219,7 @@ const Settings = () => { const handleDrop = (list: 0 | 1) => (event: DragEvent) => { event.preventDefault(); - if (state.order[list].length !== 0) return; + if (state.order[list].length > 0) return; const from = JSON.parse( event.dataTransfer!.getData(DRAG_TYPE), diff --git a/src/sw.ts b/src/sw.ts index 98686dafb..1504971f9 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -7,7 +7,7 @@ import type { ThemesData, UserStorageData } from './types'; chrome.runtime.onInstalled.addListener(async () => { const [themes, settings] = await Promise.all([ fetch('themes.json').then((res) => res.json() as Promise), - chrome.storage.local.get() as Promise, + chrome.storage.local.get(), ]); // TODO: Remove once most users have updated. diff --git a/test/TestComponent.ts b/test/TestComponent.ts deleted file mode 100644 index c1025669c..000000000 --- a/test/TestComponent.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { collect, h } from 'stage1'; -import { compile } from 'stage1/macro' with { type: 'macro' }; - -type TestComponent = HTMLDivElement; - -interface TestProps { - text: string; -} - -interface Refs { - t: Text; -} - -const meta = compile(` -
- @t -
-`); -const view = h(meta.html); - -export function Test(props: TestProps): TestComponent { - const root = view; - const refs = collect(root, meta.k, meta.d); - - refs.t.nodeValue = props.text; - - return root; -} diff --git a/test/e2e/screenshot.css b/test/e2e/screenshot.css index d7e8b045e..1acd3d310 100644 --- a/test/e2e/screenshot.css +++ b/test/e2e/screenshot.css @@ -1,3 +1,3 @@ body { - font-family: 'Noto Sans', Arial, sans-serif !important; + font-family: "Noto Sans", Arial, sans-serif !important; } diff --git a/test/setup.ts b/test/setup.ts index 903b5c1b8..cb50b1a51 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,183 +1,12 @@ -import { expect } from 'bun:test'; -import { GlobalWindow, type Window } from 'happy-dom'; +import '@maxmilton/test-utils/extend'; -/* eslint-disable no-var, vars-on-top */ -declare global { - /** Real bun console. `console` is mapped to happy-dom's virtual console. */ - var $console: Console; - var happyDOM: Window['happyDOM']; -} -/* eslint-enable */ - -/** - * Get the total number of parameters of a function including optional - * parameters with default values. - * - * @remarks Native functions will only return the number of required parameters; - * optional parameters cannot be determined. - * - * @returns The number of parameters, including optional parameters. - */ -export function parameters(func: unknown): number { - if (typeof func !== 'function') { - throw new TypeError('Expected a function'); - } - - const str = func.toString(); - const len = str.length; - const start = str.indexOf('('); - let index = start; - let count = 1; - let nested = 0; - let char: string; - - // FIXME: Handle nested string template literals. - const string = (quote: '"' | "'" | '`') => { - while (index++ < len) { - char = str[index]; - - if (char === quote) { - break; - } - // skip escaped characters - if (char === '\\') { - index++; - } - } - }; - - while (index++ < len) { - char = str[index]; - - if (!nested) { - if (char === ')') { - break; - } - if (char === ',') { - count++; - continue; // eslint-disable-line no-continue - } - } - - switch (char) { - case '"': - case "'": - case '`': - string(char); - break; - case '(': - case '[': - case '{': - nested++; - break; - case ')': - case ']': - case '}': - nested--; - break; - default: - break; - } - } - - if (index >= len || nested !== 0) { - throw new Error('Invalid function signature'); - } - - // handle no parameters - if (str.slice(start + 1, index).trim().length === 0) { - // eslint-disable-next-line @typescript-eslint/prefer-includes - if (str.indexOf('[native code]', index) >= 0) { - count = func.length; - // eslint-disable-next-line no-console - console.warn('Optional parameters cannot be determined for native functions'); - } else { - count = 0; - } - } - - return count; -} - -declare module 'bun:test' { - interface Matchers { - /** Asserts that a value is a plain `object`. */ - toBePlainObject(): void; - /** Asserts that a value is a `class`. */ - toBeClass(): void; - /** Asserts that a function has a specific number of parameters. */ - toHaveParameters(required: number, optional: number): void; - } -} - -expect.extend({ - // XXX: Bun's `toBeObject` matcher is the equivalent of `typeof x === 'object'`. - toBePlainObject(received: unknown) { - return Object.prototype.toString.call(received) === '[object Object]' - ? { pass: true } - : { - pass: false, - message: () => `expected ${String(received)} to be a plain object`, - }; - }, - - toBeClass(received: unknown) { - return typeof received === 'function' && - /^class\s/.test(Function.prototype.toString.call(received)) - ? { pass: true } - : { - pass: false, - message: () => `expected ${String(received)} to be a class`, - }; - }, - - toHaveParameters(received: unknown, required: number, optional: number) { - if (typeof received !== 'function') { - return { - pass: false, - message: () => `expected ${String(received)} to be a function`, - }; - } - - const actualRequired = received.length; - const actualOptional = parameters(received) - actualRequired; +import { setupDOM } from '@maxmilton/test-utils/dom'; - return actualRequired === required && actualOptional === optional - ? { pass: true } - : { - pass: false, - message: () => - `expected ${received.name} to have ${required}/${optional} required/optional parameters, but it has ${actualRequired}/${actualOptional}`, - }; - }, -}); - -export const originalConsoleCtor = global.console.Console; - -const originalConsole = global.console; const noop = () => {}; const noopAsync = () => Promise.resolve(); const noopAsyncObj = () => Promise.resolve({}); const noopAsyncArr = () => Promise.resolve([]); -function setupDOM() { - const dom = new GlobalWindow({ - url: 'chrome-extension://cpcibnbdmpmcmnkhoiilpnlaepkepknb/', - }); - global.happyDOM = dom.happyDOM; - global.$console = originalConsole; - // @ts-expect-error - happy-dom only implements a subset of the DOM API - global.window = dom.window.document.defaultView; - global.document = window.document; - global.console = window.console; // https://github.com/capricorn86/happy-dom/wiki/Virtual-Console - global.fetch = window.fetch; - global.setTimeout = window.setTimeout; - global.clearTimeout = window.clearTimeout; - global.DocumentFragment = window.DocumentFragment; - global.CSSStyleSheet = window.CSSStyleSheet; - global.Text = window.Text; -} - function setupMocks(): void { // @ts-expect-error - noop stub global.performance.mark = noop; @@ -251,7 +80,9 @@ export async function reset(): Promise { window.close(); } - setupDOM(); + setupDOM({ + url: 'chrome-extension://cpcibnbdmpmcmnkhoiilpnlaepkepknb/', + }); setupMocks(); } diff --git a/test/unit/BookmarkBar.test.ts b/test/unit/BookmarkBar.test.ts index c80493cac..85266a5d0 100644 --- a/test/unit/BookmarkBar.test.ts +++ b/test/unit/BookmarkBar.test.ts @@ -1,6 +1,6 @@ import { afterAll, afterEach, beforeAll, expect, test } from 'bun:test'; +import { cleanup, render } from '@maxmilton/test-utils/dom'; import { BookmarkBar } from '../../src/components/BookmarkBar'; -import { cleanup, render } from './utils'; let style: HTMLStyleElement; diff --git a/test/unit/BookmarkNode.test.ts b/test/unit/BookmarkNode.test.ts index 4cc32ba50..47a8f54b1 100644 --- a/test/unit/BookmarkNode.test.ts +++ b/test/unit/BookmarkNode.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, test } from 'bun:test'; +import { cleanup, render } from '@maxmilton/test-utils/dom'; import { BookmarkNode, type BookmarkTreeNode } from '../../src/components/BookmarkNode'; import type { LinkProps } from '../../src/components/Link'; -import { cleanup, render } from './utils'; afterEach(cleanup); diff --git a/test/unit/Link.test.ts b/test/unit/Link.test.ts index 8eb6f6c2e..9b27125a7 100644 --- a/test/unit/Link.test.ts +++ b/test/unit/Link.test.ts @@ -1,6 +1,6 @@ import { afterEach, expect, test } from 'bun:test'; +import { cleanup, render } from '@maxmilton/test-utils/dom'; import { Link, type LinkProps } from '../../src/components/Link'; -import { cleanup, render } from './utils'; afterEach(cleanup); diff --git a/test/unit/Menu.test.ts b/test/unit/Menu.test.ts index 406e82074..933684c03 100644 --- a/test/unit/Menu.test.ts +++ b/test/unit/Menu.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, spyOn, test } from 'bun:test'; +import { cleanup, render } from '@maxmilton/test-utils/dom'; import { Menu } from '../../src/components/Menu'; import { handleClick } from '../../src/utils'; -import { cleanup, render } from './utils'; afterEach(cleanup); diff --git a/test/unit/Search.test.ts b/test/unit/Search.test.ts index 3125021fb..8760529e1 100644 --- a/test/unit/Search.test.ts +++ b/test/unit/Search.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, expect, test } from 'bun:test'; -import { cleanup, render } from './utils'; +import { cleanup, render } from '@maxmilton/test-utils/dom'; // HACK: The Search component is designed to be rendered once (does not clone // its view), for byte savings. Given its mutation of the view (affecting global diff --git a/test/unit/css-engine.ts b/test/unit/css-engine.ts deleted file mode 100644 index e9b0ad346..000000000 --- a/test/unit/css-engine.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * @overview CSS engine and utilities for writing CSS tests. - */ - -import { DECLARATION, type Element, LAYER, MEDIA, RULESET, SCOPE, SUPPORTS, compile } from 'stylis'; - -// biome-ignore lint/performance/noBarrelFile: prefer nice DX in tests -// biome-ignore lint/performance/noReExportAll: prefer nice DX in tests -export * from 'stylis'; - -export const CONTAINER = '@container'; -export const STARTING_STYLE = '@starting-style'; - -export const SKIP = Symbol('SKIP'); - -/** - * Clones the element, stripping out references to other elements (e.g., - * "parent") for cleaner logging. **Intended for debugging only.** - */ -export const cleanElement = (element: T): T => { - const { root, parent, children, siblings, ...rest } = element; - // @ts-expect-error - TODO: Fix "children" prop type - rest.children = Array.isArray(children) ? children.length : children; - return rest as T; -}; - -// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -type VisitorFunction = (element: Element) => typeof SKIP | void; - -function visit(element: Element, visitor: VisitorFunction): void { - if (visitor(element) === SKIP) return; - if (Array.isArray(element.children)) { - for (const child of element.children) { - visit(child, visitor); - } - } -} - -/** - * Walks the AST and calls the visitor function for each element. - */ -export function walk(root: Element[], visitor: VisitorFunction): void { - for (const element of root) { - visit(element, visitor); - } -} - -const cache = new WeakMap>(); - -function load(root: Element[]): void { - const map = new Map(); - let tmp: Element[] | undefined; - cache.set(root, map); - - walk(root, (element) => { - if (element.type[0] === '@') { - switch (element.type) { - case CONTAINER: - case LAYER: - case MEDIA: - case SCOPE: - case STARTING_STYLE: - case SUPPORTS: - return; - default: - // eslint-disable-next-line consistent-return - return SKIP; - } - } - - if (element.type === RULESET) { - for (const selector of element.props) { - // eslint-disable-next-line no-cond-assign - if ((tmp = map.get(selector))) { - tmp.push(element); - } else { - map.set(selector, [element]); - } - } - } - // eslint-disable-next-line consistent-return - return SKIP; - }); -} - -/** - * Returns a list of elements matching the given CSS selector. - */ -export function lookup(root: Element[], cssSelector: string): Element[] | undefined { - if (!cache.has(root)) load(root); - - // parse the selector to ensure it's valid and normalized - const ast = compile(cssSelector + '{}'); - - if (ast.length !== 1 || ast[0].type !== RULESET) { - throw new TypeError('Expected a single CSS selector'); - } - - const selector = ast[0].props; - - if (selector.length !== 1) { - throw new TypeError('Expected a single CSS selector'); - } - - return cache.get(root)?.get(selector[0]); -} - -/** - * Combines the given elements into a single declaration block. - * - * Declarations are overwritten in the order they are given; the last - * declaration for a given property wins. - * - * NOTE: `@media`, `@layer`, `@supports`, etc. rules are currently not handled. - * All declarations will be merged regardless of their parent rules. - * - */ -// FIXME: Evaluate at-rules and handle them appropriately. This adds a lot of -// complexity, so consider using happy-dom if they have support for it. -export function reduce(elements: Element[]): Record { - if (elements.length === 0) return {}; - - const decls: Record = {}; - - for (const element of elements) { - if (element.type === RULESET) { - for (const child of element.children as Element[]) { - if (child.type === DECLARATION) { - decls[child.props as string] = child.children as string; - } else { - // eslint-disable-next-line no-console - console.warn('Unexpected child element type:', child.type); - } - } - } else { - // eslint-disable-next-line no-console - console.warn('Unexpected element type:', element.type); - } - } - - return decls; -} - -export function isHexColor(color: string): boolean { - return /^#[\da-f]{6,8}$/i.test(color); -} - -export function hexToRgb(hex: string): [r: number, g: number, b: number] { - const int = Number.parseInt(hex.slice(1, 7), 16); - // eslint-disable-next-line no-bitwise - return [(int >> 16) & 255, (int >> 8) & 255, int & 255]; -} - -export function linearize(color: number): number { - const v = color / 255; // normalize - return v <= 0.039_28 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; // gamma correction -} - -export function luminance([r, g, b]: [number, number, number]): number { - return linearize(r) * 0.2126 + linearize(g) * 0.7152 + linearize(b) * 0.0722; -} - -export function isLightOrDark(hexColor: string): 'light' | 'dark' { - return luminance(hexToRgb(hexColor)) > 0.179 ? 'light' : 'dark'; -} diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 75d623a43..343d5c2bf 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -24,7 +24,6 @@ describe('dist files', () => { ]; for (const [filename, type, minBytes, maxBytes] of distFiles) { - // eslint-disable-next-line @typescript-eslint/no-loop-func describe(filename, () => { const file = Bun.file(`dist/${filename}`); diff --git a/test/unit/newtab.test.ts b/test/unit/newtab.test.ts index 0ec110eed..610574f26 100644 --- a/test/unit/newtab.test.ts +++ b/test/unit/newtab.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, spyOn, test } from 'bun:test'; +import { DECLARATION, compile, lookup, walk } from '@maxmilton/test-utils/css'; +import { performanceSpy } from '@maxmilton/test-utils/spy'; import { reset } from '../setup'; -import { DECLARATION, compile, lookup, walk } from './css-engine'; -import { performanceSpy } from './utils'; // Completely reset DOM and global state between tests afterEach(reset); diff --git a/test/unit/settings.test.ts b/test/unit/settings.test.ts index fef3e87f7..78e0d5906 100644 --- a/test/unit/settings.test.ts +++ b/test/unit/settings.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, mock, spyOn, test } from 'bun:test'; +import { DECLARATION, compile, lookup, walk } from '@maxmilton/test-utils/css'; +import { performanceSpy } from '@maxmilton/test-utils/spy'; import { reset } from '../setup'; -import { DECLARATION, compile, lookup, walk } from './css-engine'; -import { performanceSpy } from './utils'; // Completely reset DOM and global state between tests afterEach(reset); diff --git a/test/unit/test-css-engine.test.ts b/test/unit/test-css-engine.test.ts deleted file mode 100644 index db52165b8..000000000 --- a/test/unit/test-css-engine.test.ts +++ /dev/null @@ -1,367 +0,0 @@ -import { describe, expect, mock, test } from 'bun:test'; -import { - DECLARATION, - type Element, - RULESET, - cleanElement, - compile, - hexToRgb, - isHexColor, - isLightOrDark, - linearize, - lookup, - luminance, - reduce, - walk, -} from './css-engine'; - -const css = ` - .foo { - color: red; - } - - .bar { - color: blue; - } - - .baz { - color: pink; - } - - .baz { - color: green; - } - - @media (min-width: 768px) { - .baz { - color: purple; - } - } - - .qux { - color: yellow; - - & > .qax { - color: orange; - } - } - - @font-face { - font-family: 'Example'; - src: url('fonts/Example.woff') format('woff2'); - } -`; -const ast = compile(css); - -describe('lookup', () => { - test('is a function', () => { - expect.assertions(2); - expect(lookup).toBeFunction(); - expect(lookup).not.toBeClass(); - }); - - test('expects 2 parameters', () => { - expect.assertions(1); - expect(lookup).toHaveParameters(2, 0); - }); - - test('returns an array when has matching elements', () => { - expect.assertions(1); - expect(lookup(ast, '.foo')).toBeArray(); - }); - - test('returns undefined when no matching elements', () => { - expect.assertions(1); - expect(lookup(ast, '.missing')).toBeUndefined(); - }); - - test('throws if selector is invalid', () => { - expect.assertions(6); - expect(() => lookup(ast, '')).toThrow(); - expect(() => lookup(ast, ' ')).toThrow(); - expect(() => lookup(ast, ';')).toThrow(); - expect(() => lookup(ast, '{}')).toThrow(); - expect(() => lookup(ast, '@')).toThrow(); - expect(() => lookup(ast, '&')).toThrow(); - // FIXME: These should also throw, but they don't - // expect(() => lookup(ast, '[]')).toThrow(); - // expect(() => lookup(ast, '#')).toThrow(); - // expect(() => lookup(ast, '.')).toThrow(); - }); - - test('throws if multiple selectors are passed', () => { - expect.assertions(1); - expect(() => lookup(ast, '.foo, .bar')).toThrow('Expected a single CSS selector'); - }); - - test('throws if multiple rulesets are found', () => { - expect.assertions(1); - expect(() => lookup(ast, '.bar{} .baz')).toThrow('Expected a single CSS selector'); - }); - - test('finds all matching elements', () => { - expect.assertions(6); - expect(lookup(ast, '.foo')).toHaveLength(1); - expect(lookup(ast, '.bar')).toHaveLength(1); - expect(lookup(ast, '.baz')).toHaveLength(3); // three rulesets have this selector - expect(lookup(ast, '.qux')).toHaveLength(1); - expect(lookup(ast, '.qax')).toBeUndefined(); // actual selector is .qux>.qax - expect(lookup(ast, '.quux')).toBeUndefined(); // no matching selector - }); -}); - -describe('walk', () => { - test('is a function', () => { - expect.assertions(2); - expect(walk).toBeFunction(); - expect(walk).not.toBeClass(); - }); - - test('expects 2 parameters', () => { - expect.assertions(1); - expect(walk).toHaveParameters(2, 0); - }); - - test('has no return value', () => { - expect.assertions(1); - // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression - expect(walk(ast, () => {})).toBeUndefined(); - }); - - test('calls visitor function for each AST node', () => { - expect.assertions(1); - const visitor = mock(); - walk(ast, visitor); - expect(visitor).toHaveBeenCalledTimes(18); - }); - - test('visits all elements', () => { - expect.assertions(1); - const selectors: string[] = []; - walk(ast, (element) => { - if (element.type === RULESET) { - selectors.push(...element.props); - } - }); - expect(selectors).toEqual(['.foo', '.bar', '.baz', '.baz', '.baz', '.qux', '.qux>.qax']); - }); -}); - -describe('reduce', () => { - test('is a function', () => { - expect.assertions(2); - expect(reduce).toBeFunction(); - expect(reduce).not.toBeClass(); - }); - - test('expects 1 parameter', () => { - expect.assertions(1); - expect(reduce).toHaveParameters(1, 0); - }); - - test('returns an object', () => { - expect.assertions(1); - const reduced = reduce([ast[0]]); - expect(reduced).toBePlainObject(); - }); - - test('throws when passed null or undefined', () => { - expect.assertions(2); - // @ts-expect-error - intentionally passing wrong type - expect(() => reduce(null)).toThrow(); - // @ts-expect-error - intentionally passing wrong type - expect(() => reduce()).toThrow(); - }); - - test('merges all elements, overriding earlier values', () => { - expect.assertions(2); - const elements = lookup(ast, '.baz'); - expect(elements).toHaveLength(3); - const reduced = reduce(elements!); - // FIXME: It should be green since it's outside the media query - // expect(reduced).toEqual({ color: 'green' }); // last one wins - expect(reduced).toEqual({ color: 'purple' }); // last one wins - }); -}); - -describe('cleanElement', () => { - test('is a function', () => { - expect.assertions(2); - expect(cleanElement).toBeFunction(); - expect(cleanElement).not.toBeClass(); - }); - - test('expects 1 parameter', () => { - expect.assertions(1); - expect(cleanElement).toHaveParameters(1, 0); - }); - - test('returns an object', () => { - expect.assertions(1); - const cleaned = cleanElement(ast[0]); - expect(cleaned).toBePlainObject(); - }); - - for (const prop of ['root', 'parent', 'siblings'] as const) { - test(`removes "${prop}" property without mutating original object`, () => { - expect.assertions(2); - const cleaned = cleanElement(ast[0]); - expect(ast[0]).toHaveProperty(prop); - expect(cleaned).not.toHaveProperty(prop); - }); - } - - test('replaces "children" property with count of child elements when children is array', () => { - expect.assertions(3); - const element = lookup(ast, '.qux')![0]; - const cleaned = cleanElement(element); - expect(element).toHaveProperty('children'); - expect(element.children).toBeArray(); - expect(cleaned).toHaveProperty('children', 1); - }); - - test('leaves "children" property alone when children is not array', () => { - expect.assertions(5); - const element = ast[0].children[0] as Element; - expect(element).toBePlainObject(); - expect(element.type).toBe(DECLARATION); - const cleaned = cleanElement(element); - expect(element).toHaveProperty('children', 'red'); - expect(element.children).toBeString(); - expect(cleaned).toHaveProperty('children', element.children); - }); -}); - -const hexColors = [ - '#ffffff', - '#ffffffff', - '#000000', - '#00000000', - '#ff000000', - '#ff0000', - '#00ff00', - '#0000ff', - '#000000ff', - '#ff00ff', - '#00ffff', - '#ffff00', - '#abcdef', -]; -const notHexColors = [ - '@000000', - '000000', - '00000', - '0000', - '000', - '00', - '0', - '', - ' ', - 'abcdef', - 'null', -]; - -describe('isHexColor', () => { - test('is a function', () => { - expect.assertions(2); - expect(isHexColor).toBeFunction(); - expect(isHexColor).not.toBeClass(); - }); - - test('expects 1 parameter', () => { - expect.assertions(1); - expect(isHexColor).toHaveParameters(1, 0); - }); - - test('returns a boolean', () => { - expect.assertions(1); - expect(isHexColor('#ffffff')).toBeBoolean(); - }); - - test.each(hexColors)('returns true for %s', (value) => { - expect.assertions(1); - expect(isHexColor(value)).toBeTrue(); - }); - - test.each(notHexColors)('returns false for %s', (value) => { - expect.assertions(1); - expect(isHexColor(value)).toBeFalse(); - }); -}); - -describe('hexToRgb', () => { - test('is a function', () => { - expect.assertions(2); - expect(hexToRgb).toBeFunction(); - expect(hexToRgb).not.toBeClass(); - }); - - test('expects 1 parameter', () => { - expect.assertions(1); - expect(hexToRgb).toHaveParameters(1, 0); - }); - - test('returns an array', () => { - expect.assertions(1); - expect(hexToRgb('#ffffff')).toBeArrayOfSize(3); - }); -}); - -describe('linearize', () => { - test('is a function', () => { - expect.assertions(2); - expect(linearize).toBeFunction(); - expect(linearize).not.toBeClass(); - }); - - test('expects 1 parameter', () => { - expect.assertions(1); - expect(linearize).toHaveParameters(1, 0); - }); - - test('returns a number', () => { - expect.assertions(1); - expect(linearize(0)).toBeNumber(); - }); -}); - -describe('luminance', () => { - test('is a function', () => { - expect.assertions(2); - expect(luminance).toBeFunction(); - expect(luminance).not.toBeClass(); - }); - - test('expects 1 parameter', () => { - expect.assertions(1); - expect(luminance).toHaveParameters(1, 0); - }); - - test('returns a number', () => { - expect.assertions(1); - expect(luminance([0, 0, 0])).toBeNumber(); - }); -}); - -describe('isLightOrDark', () => { - test('is a function', () => { - expect.assertions(2); - expect(isLightOrDark).toBeFunction(); - expect(isLightOrDark).not.toBeClass(); - }); - - test('expects 1 parameter', () => { - expect.assertions(1); - expect(isLightOrDark).toHaveParameters(1, 0); - }); - - test('returns string "light" for #ffffff', () => { - expect.assertions(1); - expect(isLightOrDark('#ffffff')).toBe('light'); - }); - - test('returns string "dark" for #000000', () => { - expect.assertions(1); - expect(isLightOrDark('#000000')).toBe('dark'); - }); -}); diff --git a/test/unit/test-setup.test.ts b/test/unit/test-setup.test.ts deleted file mode 100644 index 497417e0a..000000000 --- a/test/unit/test-setup.test.ts +++ /dev/null @@ -1,1494 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars, max-classes-per-file, no-console, unicorn/consistent-function-scoping */ - -import { describe, expect, spyOn, test } from 'bun:test'; -import { VirtualConsole } from 'happy-dom'; -import * as setupExports from '../setup'; -import { originalConsoleCtor, parameters, reset } from '../setup'; - -describe('exports', () => { - const exports = ['originalConsoleCtor', 'parameters', 'reset']; - - test.each(exports)('has "%s" named export', (exportName) => { - expect.assertions(1); - expect(setupExports).toHaveProperty(exportName); - }); - - test('does not have a default export', () => { - expect.assertions(1); - expect(setupExports).not.toHaveProperty('default'); - }); - - test('does not export anything else', () => { - expect.assertions(1); - expect(Object.keys(setupExports)).toHaveLength(exports.length); - }); -}); - -describe('matcher: toBePlainObject', () => { - const plainObjects = [ - {}, - { foo: 'bar' }, - Object.create(null), - Object.create({}), - // eslint-disable-next-line no-new-object - new Object(), - ]; - const notPlainObjects = [ - null, - // eslint-disable-next-line unicorn/no-new-array - new Array(1), - [[{}]], // double array due to quirk of bun test; resolves to [{}] - [[null]], // double array due to quirk of bun test; resolves to [null] - () => {}, - // eslint-disable-next-line @typescript-eslint/no-implied-eval - new Function(), - Function, - Object, - /(?:)/, - new Date(), - // biome-ignore lint/nursery/useErrorMessage: simple test case - new Error(), // eslint-disable-line unicorn/error-message - new Map(), - new Set(), - new WeakMap(), - new WeakSet(), - new Promise(() => {}), - new Int8Array(), - ]; - const notObjects = [ - 'Hello', - 123, - true, - false, - undefined, - Symbol('sym'), - BigInt(1234), - // biome-ignore lint/style/useNumberNamespace: for tests - NaN, // eslint-disable-line unicorn/prefer-number-properties - // biome-ignore lint/style/useNumberNamespace: for tests - Infinity, - ]; - - test.each(plainObjects)('matches plain object %#', (item) => { - expect.assertions(1); - expect(item).toBePlainObject(); - }); - - test.each(notPlainObjects)('does not match non-plain object %#', (item) => { - expect.assertions(1); - expect(item).not.toBePlainObject(); - }); - - test.each(notObjects)('does not match non-object %#', (item) => { - expect.assertions(1); - expect(item).not.toBePlainObject(); - }); -}); - -describe('matcher: toBeClass', () => { - // eslint-disable-next-line @typescript-eslint/no-extraneous-class - class Foo {} - const classes = [ - Foo, - class Bar extends Foo {}, - // eslint-disable-next-line @typescript-eslint/no-extraneous-class - class {}, - class extends Foo {}, - Foo.prototype.constructor, - ]; - const notClasses = [ - 'Hello', - 123, - true, - false, - undefined, - Symbol('sym'), - BigInt(1234), - // biome-ignore lint/style/useNumberNamespace: for tests - NaN, // eslint-disable-line unicorn/prefer-number-properties - // biome-ignore lint/style/useNumberNamespace: for tests - Infinity, - {}, - { foo: 'bar' }, - Object.create(null), - Object.create({}), - // eslint-disable-next-line no-new-object - new Object(), - null, - // eslint-disable-next-line unicorn/no-new-array - new Array(1), - [[{}]], // double array due to quirk of bun test; resolves to [{}] - [[null]], // double array due to quirk of bun test; resolves to [null] - function foo() {}, - () => {}, - // eslint-disable-next-line @typescript-eslint/no-implied-eval - new Function(), - Function, - Object, - /(?:)/, - new Date(), - // biome-ignore lint/nursery/useErrorMessage: simple test case - new Error(), // eslint-disable-line unicorn/error-message - new Map(), - new Set(), - new WeakMap(), - new WeakSet(), - new Promise(() => {}), - new Int8Array(), - - // XXX: These are built-in classes but accessing directly calls their - // constructor, so they behave like functions. - Function, - Object, - Array, - String, - Number, - Boolean, - Symbol, - BigInt, - Buffer, - ]; - - test.each(classes)('matches class %#: %p', (item) => { - expect.assertions(1); - expect(item).toBeClass(); - }); - - test.each(notClasses)('does not match non-class %#: %p', (item) => { - expect.assertions(1); - expect(item).not.toBeClass(); - }); -}); - -describe('matcher: toHaveParameters', () => { - const funcs: [required: number, optional: number, func: unknown][] = [ - [0, 0, function foo() {}], - [1, 0, function foo(_a: unknown) {}], - [0, 1, function foo(_a = 1) {}], - [2, 0, function foo(_a: unknown, _b: unknown) {}], - [1, 1, function foo(_a: unknown, _b = 1) {}], - [0, 2, function foo(_a = 1, _b = 2) {}], - [0, 3, function foo(_a = 1, _b = 2, ..._rest: unknown[]) {}], - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [0, 0, function () {}], // eslint-disable-line func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [1, 0, function (_a: unknown) {}], // eslint-disable-line func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [0, 1, function (_a = 1) {}], // eslint-disable-line func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [2, 0, function (_a: unknown, _b: unknown) {}], // eslint-disable-line func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [1, 1, function (_a: unknown, _b = 1) {}], // eslint-disable-line func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [0, 2, function (_a = 1, _b = 2) {}], // eslint-disable-line func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [0, 3, function (_a = 1, _b = 2, ..._rest: unknown[]) {}], // eslint-disable-line func-names - [0, 0, () => {}], - [1, 0, (_a: unknown) => {}], - [0, 1, (_a = 1) => {}], - [2, 0, (_a: unknown, _b: unknown) => {}], - [1, 1, (_a: unknown, _b = 1) => {}], - [0, 2, (_a = 1, _b = 2) => {}], - [0, 3, (_a = 1, _b = 2, ..._rest: unknown[]) => {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 0, function* foo() {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [1, 0, function* foo(_a: unknown) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 1, function* foo(_a = 1) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [2, 0, function* foo(_a: unknown, _b: unknown) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [1, 1, function* foo(_a: unknown, _b = 1) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 2, function* foo(_a = 1, _b = 2) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 3, function* foo(_a = 1, _b = 2, ..._rest: unknown[]) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 0, async function foo() {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [1, 0, async function foo(_a: unknown) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 1, async function foo(_a = 1) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [2, 0, async function foo(_a: unknown, _b: unknown) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [1, 1, async function foo(_a: unknown, _b = 1) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 2, async function foo(_a = 1, _b = 2) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 3, async function foo(_a = 1, _b = 2, ..._rest: unknown[]) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 0, async function* foo() {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [1, 0, async function* foo(_a: unknown) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 1, async function* foo(_a = 1) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [2, 0, async function* foo(_a: unknown, _b: unknown) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [1, 1, async function* foo(_a: unknown, _b = 1) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 2, async function* foo(_a = 1, _b = 2) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function - [0, 3, async function* foo(_a = 1, _b = 2, ..._rest: unknown[]) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names - [0, 0, function* () {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names - [1, 0, function* (_a: unknown) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names - [0, 1, function* (_a = 1) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names - [2, 0, function* (_a: unknown, _b: unknown) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names - [1, 1, function* (_a: unknown, _b = 1) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names - [0, 2, function* (_a = 1, _b = 2) {}], - // eslint-disable-next-line @typescript-eslint/no-empty-function, func-names - [0, 3, function* (_a = 1, _b = 2, ..._rest: unknown[]) {}], - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [0, 0, async function () {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [1, 0, async function (_a: unknown) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [0, 1, async function (_a = 1) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [2, 0, async function (_a: unknown, _b: unknown) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [1, 1, async function (_a: unknown, _b = 1) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [0, 2, async function (_a = 1, _b = 2) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - // biome-ignore lint/complexity/useArrowFunction: explicit test case - [0, 3, async function (_a = 1, _b = 2, ..._rest: unknown[]) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - [0, 0, async function* () {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - [1, 0, async function* (_a: unknown) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - [0, 1, async function* (_a = 1) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - [2, 0, async function* (_a: unknown, _b: unknown) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - [1, 1, async function* (_a: unknown, _b = 1) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - [0, 2, async function* (_a = 1, _b = 2) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - [0, 3, async function* (_a = 1, _b = 2, ..._rest: unknown[]) {}], // eslint-disable-line @typescript-eslint/no-empty-function, func-names - [0, 0, async () => {}], - [1, 0, async (_a: unknown) => {}], - [0, 1, async (_a = 1) => {}], - [2, 0, async (_a: unknown, _b: unknown) => {}], - [1, 1, async (_a: unknown, _b = 1) => {}], - [0, 2, async (_a = 1, _b = 2) => {}], - [0, 3, async (_a = 1, _b = 2, ..._rest: unknown[]) => {}], - ]; - - test.each(funcs)( - 'matches function %# with %i required and %i optional parameters', - (required, optional, func) => { - expect.assertions(2); - expect(func).toHaveParameters(required, optional); - expect(func).toHaveLength(required); - }, - ); - - // TODO: Add test for failing case when passing non-function once bun supports it -}); - -describe('$console', () => { - test('global exists', () => { - expect.assertions(1); - expect($console).toBeDefined(); - }); - - test('is the original console', () => { - expect.assertions(1); - expect($console).toBeInstanceOf(originalConsoleCtor); - }); - - test('is not the happy-dom virtual console', () => { - expect.assertions(3); - expect($console).not.toBeInstanceOf(VirtualConsole); - expect($console).not.toBe(console); - expect($console).not.toBe(window.console); - }); -}); - -describe('happy-dom', () => { - const globals = [ - 'happyDOM', - 'window', - 'document', - 'console', - 'fetch', - 'setTimeout', - 'clearTimeout', - 'DocumentFragment', - 'CSSStyleSheet', - 'Text', - ]; - - test.each(globals)('"%s" global exists', (global) => { - expect.assertions(1); - expect(global).toBeDefined(); - }); - - test('console is a virtual console', () => { - expect.assertions(3); - expect(window.console).toBeInstanceOf(VirtualConsole); - expect(console).toBeInstanceOf(VirtualConsole); - expect(console).toBe(window.console); // same instance - }); - - test('console is not the original console', () => { - expect.assertions(2); - expect(console).not.toBeInstanceOf(originalConsoleCtor); - expect(console).not.toBe($console); - }); - - describe('virtual console', () => { - test('has no log entries by default', () => { - expect.assertions(2); - const logs = happyDOM.virtualConsolePrinter.read(); - expect(logs).toBeArray(); - expect(logs).toHaveLength(0); - }); - - // types shouldn't include @types/node Console['Console'] property - const methods: (keyof Omit)[] = [ - 'assert', - // 'clear', // clears log entries so we can't test it - 'count', - 'countReset', - 'debug', - 'dir', - 'dirxml', - 'error', - // @ts-expect-error - alias for console.error - 'exception', - 'group', - 'groupCollapsed', - // 'groupEnd', // doesn't log anything - 'info', - 'log', - // 'profile', // not implemented in happy-dom - // 'profileEnd', - 'table', - // 'time', // doesn't log anything - // 'timeStamp', - // 'timeLog', - // 'timeEnd', - 'trace', - 'warn', - ]; - - test.each(methods)('has log entry after "%s" call', (method) => { - expect.assertions(1); - console[method](); - expect(happyDOM.virtualConsolePrinter.read()).toHaveLength(1); - }); - - test('clears log entries after read', () => { - expect.assertions(3); - expect(happyDOM.virtualConsolePrinter.read()).toHaveLength(0); - // biome-ignore lint/suspicious/noConsoleLog: for testing - console.log(); - expect(happyDOM.virtualConsolePrinter.read()).toHaveLength(1); - expect(happyDOM.virtualConsolePrinter.read()).toHaveLength(0); - }); - }); -}); - -describe('reset', () => { - test('is a function', () => { - expect.assertions(2); - expect(reset).toBeFunction(); - expect(reset).not.toBeClass(); - }); - - test('expects no parameters', () => { - expect.assertions(1); - expect(reset).toHaveParameters(0, 0); - }); - - test('resets global chrome instance', async () => { - expect.assertions(2); - (chrome as typeof chrome & { foo?: string }).foo = 'bar'; - expect(chrome).toHaveProperty('foo', 'bar'); - await reset(); - expect(chrome).not.toHaveProperty('foo'); - }); - - test('resets global window instance', async () => { - expect.assertions(2); - (window as Window & { foo?: string }).foo = 'bar'; - expect(window).toHaveProperty('foo', 'bar'); - await reset(); - expect(window).not.toHaveProperty('foo'); - }); - - test('resets global document instance', async () => { - expect.assertions(2); - const h1 = document.createElement('h1'); - h1.textContent = 'foo'; - document.body.appendChild(h1); - expect(document.documentElement.innerHTML).toBe('

foo

'); - await reset(); - expect(document.documentElement.innerHTML).toBe(''); - }); - - test('resets expected globals instances', async () => { - expect.assertions(9); - const oldChrome = chrome; - const oldHappyDOM = happyDOM; - const oldWindow = window; - const oldDocument = document; - const oldConsole = console; - const oldFetch = fetch; - const oldSetTimeout = setTimeout; - const oldClearTimeout = clearTimeout; - const oldDocumentFragment = DocumentFragment; - await reset(); - expect(chrome).not.toBe(oldChrome); - expect(happyDOM).not.toBe(oldHappyDOM); - expect(window).not.toBe(oldWindow); - expect(document).not.toBe(oldDocument); - expect(console).not.toBe(oldConsole); - expect(fetch).not.toBe(oldFetch); - expect(setTimeout).not.toBe(oldSetTimeout); - expect(clearTimeout).not.toBe(oldClearTimeout); - expect(DocumentFragment).not.toBe(oldDocumentFragment); - }); -}); - -describe('parameters', () => { - describe('no parameters', () => { - test('simple function', () => { - expect.assertions(1); - function foo() {} - expect(parameters(foo)).toBe(0); - }); - - test('generator function', () => { - expect.assertions(1); - function* foo() { - yield null; - } - expect(parameters(foo)).toBe(0); - }); - - test('async function', () => { - expect.assertions(1); - async function foo() { - await Promise.resolve(); - } - expect(parameters(foo)).toBe(0); - }); - - test('async generator function', () => { - expect.assertions(1); - async function* foo() { - await Promise.resolve(); - yield null; - } - expect(parameters(foo)).toBe(0); - }); - - test('arrow function', () => { - expect.assertions(1); - const foo = () => {}; - expect(parameters(foo)).toBe(0); - }); - - test('async arrow function', () => { - expect.assertions(1); - const foo = async () => { - await Promise.resolve(); - }; - expect(parameters(foo)).toBe(0); - }); - }); - - describe('default parameters', () => { - test('basic', () => { - expect.assertions(1); - function foo(_a = 1, _b = 2) {} - expect(parameters(foo)).toBe(2); - }); - - test('scoped variables', () => { - expect.assertions(1); - const x = 1; - const y = 2; - function foo(_a = x, _b = y) {} - expect(parameters(foo)).toBe(2); - }); - - // FIXME: How to test this? Bun trims the whitespace - test.skip('excess whitespace', () => { - expect.assertions(1); - // biome-ignore format: explicit test case - // eslint-disable-next-line no-multi-spaces, @typescript-eslint/space-before-function-paren, space-in-parens - function foo ( _a = - // eslint-disable-next-line no-multi-spaces, @typescript-eslint/comma-spacing - 1 , - // eslint-disable-next-line @typescript-eslint/comma-dangle - _b = 2 - - // x - - ) {} - // console.log('#####', foo.toString()); - expect(parameters(foo)).toBe(2); - }); - }); - - describe('rest parameters', () => { - test('case 1', () => { - expect.assertions(1); - function foo(..._args: unknown[]) {} - expect(parameters(foo)).toBe(1); - }); - - test('case 2', () => { - expect.assertions(1); - function foo(_a: unknown, _b: unknown, ..._args: unknown[]) {} - expect(parameters(foo)).toBe(3); - }); - }); - - describe('destructured parameters', () => { - describe('Object destructuring', () => { - test('case 1', () => { - expect.assertions(1); - function foo({ _a, _b }: Record) {} - expect(parameters(foo)).toBe(1); - }); - - test('case 2', () => { - expect.assertions(1); - function foo({ _a, _b }: Record = {}) {} - expect(parameters(foo)).toBe(1); - }); - }); - - describe('Array destructuring', () => { - test('case 1', () => { - expect.assertions(1); - function foo([_a, _b]: unknown[]) {} - expect(parameters(foo)).toBe(1); - }); - - test('case 2', () => { - expect.assertions(1); - function foo([_a, _b]: unknown[] = []) {} - expect(parameters(foo)).toBe(1); - }); - }); - }); - - describe('nested destructuring', () => { - test('case 1', () => { - expect.assertions(1); - // @ts-expect-error - explicit test case - function foo({ a: { _b, _c } }) {} - expect(parameters(foo)).toBe(1); - }); - - test('case 2', () => { - expect.assertions(1); - // @ts-expect-error - explicit test case - function foo([_a, [_b, _c]]) {} - expect(parameters(foo)).toBe(1); - }); - - test('case 3', () => { - expect.assertions(1); - // @ts-expect-error - explicit test case - function foo({ a: { _b, _c } }, [[_d, _e]]) {} - expect(parameters(foo)).toBe(2); - }); - }); - - describe('default values in destructuring', () => { - test('case 1', () => { - expect.assertions(1); - function foo({ _a = 1, _b = 2 }: Record = {}) {} - expect(parameters(foo)).toBe(1); - }); - - test('case 2', () => { - expect.assertions(1); - function foo([_a = 1, _b = 2]: unknown[] = []) {} - expect(parameters(foo)).toBe(1); - }); - - test('case 3', () => { - expect.assertions(1); - function foo({ _a = 1, _b = 2 }, [_c = 3, _d = 4]) {} - expect(parameters(foo)).toBe(2); - }); - - test('case 4', () => { - expect.assertions(1); - // eslint-disable-next-line unicorn/no-object-as-default-parameter - function foo({ _a = 1, _b = 2 } = { _a: 5 }, [_c = 3, _d = 4] = [6]) {} - expect(parameters(foo)).toBe(2); - }); - }); - - describe('trailing commas', () => { - test('case 1', () => { - expect.assertions(1); - // biome-ignore format: explicit test case - // eslint-disable-next-line @typescript-eslint/comma-dangle - function foo(_a: unknown, _b: unknown,) {} - expect(parameters(foo)).toBe(2); - }); - - test('case 2', () => { - expect.assertions(1); - // biome-ignore format: explicit test case - // eslint-disable-next-line @typescript-eslint/comma-dangle, space-in-parens - function foo(_a: unknown, _b: unknown, ) {} - expect(parameters(foo)).toBe(2); - }); - - test('case 3', () => { - expect.assertions(1); - // biome-ignore format: explicit test case - function foo( - _a: unknown, - _b: unknown, - ) {} - expect(parameters(foo)).toBe(2); - }); - }); - - describe('parameter without parentheses in arrow functions', () => { - test('case 1', () => { - expect.assertions(1); - // biome-ignore format: explicit test case - const foo: ((_a: unknown) => void) = _a => {}; // eslint-disable-line arrow-parens - expect(parameters(foo)).toBe(1); - }); - }); - - describe('multiple arrow function syntaxes', () => { - test('case 1', () => { - expect.assertions(1); - const foo = (_a: unknown, _b: unknown) => {}; - expect(parameters(foo)).toBe(2); - }); - - test('case 2', () => { - expect.assertions(1); - const foo = (_a = 1, _b = 2) => {}; - expect(parameters(foo)).toBe(2); - }); - - test('case 3', () => { - expect.assertions(1); - const foo = ([_a, _b]: unknown[]) => {}; - expect(parameters(foo)).toBe(1); - }); - }); - - describe('strings within parameters', () => { - test('case 1', () => { - expect.assertions(1); - function foo(_a = '', _b = '') {} - expect(parameters(foo)).toBe(2); - }); - - test('case 2', () => { - expect.assertions(1); - function foo(_a = ',', _b = ',,,') {} - expect(parameters(foo)).toBe(2); - }); - - test('case 3', () => { - expect.assertions(1); - function foo(_a = ')', _b = ')') {} - expect(parameters(foo)).toBe(2); - }); - - test('case 3', () => { - expect.assertions(1); - function foo(_a = '(){}[]({[]})', _b = '(){}[]({[]})') {} - expect(parameters(foo)).toBe(2); - }); - - test('nested string template literals simple', () => { - expect.assertions(1); - // NOTE: Bun optimizes simple template literals into a single string - // biome-ignore lint/style/noUnusedTemplateLiteral: explicit test case - function foo(_a = `x,${`y,${`z,`},`},`, _b = ``) {} // eslint-disable-line @typescript-eslint/no-unnecessary-template-expression, @typescript-eslint/quotes - expect(parameters(foo)).toBe(2); - }); - - // FIXME: Don't skip once we support nested string template literals. - test.skip('nested string template literals with interpolation', () => { - expect.assertions(1); - const x = 'x'; - const y = 'y'; - const z = 'z'; - function foo(_a = `${x},${`,${y},${`,${z},`},`},`) {} // eslint-disable-line @typescript-eslint/no-unnecessary-template-expression - expect(parameters(foo)).toBe(1); - }); - - test("escaped '", () => { - expect.assertions(1); - // biome-ignore format: explicit test case - function foo(_a = '\'', _b = '\'') {} - expect(parameters(foo)).toBe(2); - }); - - test('escaped "', () => { - expect.assertions(1); - // biome-ignore format: explicit test case - // eslint-disable-next-line @typescript-eslint/quotes - function foo(_a = "\"", _b = "\"") {} - expect(parameters(foo)).toBe(2); - }); - - test('escaped `', () => { - expect.assertions(1); - // biome-ignore lint/style/noUnusedTemplateLiteral: explicit test case - function foo(_a = `\``, _b = `\``) {} // eslint-disable-line @typescript-eslint/quotes - expect(parameters(foo)).toBe(2); - }); - - test(String.raw`escaped \ case 1`, () => { - expect.assertions(1); - // biome-ignore format: explicit test case - function foo(_a = '\\', _b = '\\') {} - expect(parameters(foo)).toBe(2); - }); - - test(String.raw`escaped \ case 2`, () => { - expect.assertions(1); - // biome-ignore format: explicit test case - function foo(_a = 'bar\\', _b = 'baz\\') {} - expect(parameters(foo)).toBe(2); - }); - - test('escaped all', () => { - expect.assertions(1); - // biome-ignore format: explicit test case - // eslint-disable-next-line no-useless-escape - function foo(_a = '\'\"\`', _b = '') {} - expect(parameters(foo)).toBe(2); - }); - }); - - describe('functions within parameters', () => { - test('case 1', () => { - expect.assertions(1); - function foo(_a = () => {}) {} - expect(parameters(foo)).toBe(1); - }); - - test('case 2', () => { - expect.assertions(1); - // biome-ignore lint/style/useDefaultParameterLast: explicit test case - function foo(_a = () => {}, _b: unknown) {} // eslint-disable-line @typescript-eslint/default-param-last - expect(parameters(foo)).toBe(2); - }); - - test('case 3', () => { - expect.assertions(1); - function foo(_a = () => {}, _b = Date.now(), _c = Date.now()) {} - expect(parameters(foo)).toBe(3); - }); - }); - - describe('functions as parameters', () => { - test('case 1', () => { - expect.assertions(1); - function foo(_callback: () => void) {} - expect(parameters(foo)).toBe(1); - }); - }); - - describe('parameters with expressions', () => { - test('case 1', () => { - expect.assertions(1); - function foo(_a = 1 + 2) {} - expect(parameters(foo)).toBe(1); - }); - }); - - describe('complex combinations', () => { - test('case 1', () => { - expect.assertions(1); - const z = 3; - async function foo( - /* eslint-disable @typescript-eslint/default-param-last */ - // biome-ignore lint/style/useDefaultParameterLast: explicit test case - _a = { x: 1, y: 2, z }, // eslint-disable-line unicorn/no-object-as-default-parameter - // biome-ignore lint/style/useDefaultParameterLast: explicit test case - _b = [1, 2, 3], - // biome-ignore lint/style/useDefaultParameterLast: explicit test case - _c = () => {}, - // biome-ignore lint/style/useDefaultParameterLast: explicit test case - _d = Date.now(), - // biome-ignore lint/style/useDefaultParameterLast: explicit test case - _e = z, - // biome-ignore lint/style/useDefaultParameterLast: explicit test case - _f = z + 1 - (2 * 3) / 4, - // biome-ignore lint/style/useDefaultParameterLast: explicit test case - _g = Number.parseInt('123.456', 10), - _h: unknown, - _i = `,${String(z)},${String(z)},${String(z)},`, - _j = '{{[[(())]]}}),),],],},}"""```\\\'', - /* eslint-enable @typescript-eslint/default-param-last */ - ) { - await Promise.resolve(); - } - expect(parameters(foo)).toBe(10); - }); - }); - - describe('scope and shadowing', () => { - test('case 1', () => { - expect.assertions(1); - const x = 1; - // eslint-disable-next-line @typescript-eslint/no-shadow - function foo(x: unknown) { - // biome-ignore lint/suspicious/noConsoleLog: explicit test case - console.log(x); - } - expect(parameters(foo)).toBe(1); - }); - }); - - // describe('parameters with the eval keyword', () => { - // test('case 1', () => { - // expect.assertions(1); - // function foo(a, eval) {} - // expect(parameters(foo)).toBe(2); - // }); - // }); - - describe('non-ASCII identifiers', () => { - test('case 1', () => { - expect.assertions(1); - // biome-ignore lint/nursery/noUnusedFunctionParameters: explicit test case - function 𝑓𝑜𝑜(𝑎: unknown, 𝑏: unknown) {} - expect(parameters(𝑓𝑜𝑜)).toBe(2); - }); - - test('case 2', () => { - expect.assertions(1); - // biome-ignore lint/nursery/noUnusedFunctionParameters: explicit test case - const 𝑓𝑜𝑜 = (𝑎: unknown, 𝑏: unknown) => {}; - expect(parameters(𝑓𝑜𝑜)).toBe(2); - }); - }); - - // describe('invalid parameter lists', () => { - // test('case 1', () => { - // expect.assertions(1); - // function foo(_a: unknown, _a: unknown) {} // Syntax error in strict mode - // expect(parameters(foo)).toBe(2); - // }); - // }); - - // describe('strict mode considerations', () => { - // test('case 1', () => { - // expect.assertions(1); - // 'use strict'; - // function foo(_a: unknown, _a: unknown) {} // Syntax error - // expect(parameters(foo)).toBe(2); - // }); - // }); - - // describe('parameter names matching reserved words', () => { - // test('case 1', () => { - // expect.assertions(1); - // function foo(class, delete, if) {} // Syntax error - // expect(parameters(foo)).toBe(3); - // }); - // }); - - describe('using arguments object', () => { - test('basic', () => { - expect.assertions(1); - function foo(_a: unknown, _b: unknown) { - // biome-ignore lint/suspicious/noConsoleLog: explicit test case - // biome-ignore lint/style/noArguments: explicit test case - console.log(arguments); // eslint-disable-line prefer-rest-params - } - expect(parameters(foo)).toBe(2); - }); - }); - - describe('edge cases in function declaration and expression', () => { - test('function declaration and expression', () => { - expect.assertions(1); - const foo = function foo(_a: unknown, _b: unknown) {}; - expect(parameters(foo)).toBe(2); - }); - - test('generator function declaration and expression', () => { - expect.assertions(1); - const foo = function* foo(_a: unknown, _b: unknown) { - yield null; - }; - expect(parameters(foo)).toBe(2); - }); - - test('async function declaration and expression', () => { - expect.assertions(1); - const foo = async function foo(_a: unknown, _b: unknown) { - await Promise.resolve(); - }; - expect(parameters(foo)).toBe(2); - }); - - test('async generator function declaration and expression', () => { - expect.assertions(1); - const foo = async function* foo(_a: unknown, _b: unknown) { - await Promise.resolve(); - yield null; - }; - expect(parameters(foo)).toBe(2); - }); - - test('function expression', () => { - expect.assertions(1); - // biome-ignore lint/complexity/useArrowFunction: explicit test case - const bar = function (_a: unknown, _b: unknown) {}; // eslint-disable-line func-names - expect(parameters(bar)).toBe(2); - }); - - test('generator function expression', () => { - expect.assertions(1); - // eslint-disable-next-line func-names - const bar = function* (_a: unknown, _b: unknown) { - yield null; - }; - expect(parameters(bar)).toBe(2); - }); - - test('async function expression', () => { - expect.assertions(1); - // biome-ignore lint/complexity/useArrowFunction: explicit test case - const bar = async function (_a: unknown, _b: unknown) /* eslint-disable-line func-names */ { - await Promise.resolve(); - }; - expect(parameters(bar)).toBe(2); - }); - - test('async generator function expression', () => { - expect.assertions(1); - const bar = async function* (_a: unknown, _b: unknown) /* eslint-disable-line func-names */ { - await Promise.resolve(); - yield null; - }; - expect(parameters(bar)).toBe(2); - }); - - test('arrow function expression', () => { - expect.assertions(1); - const bar = (_a: unknown, _b: unknown) => {}; - expect(parameters(bar)).toBe(2); - }); - - test('async arrow function expression', () => { - expect.assertions(1); - const bar = async (_a: unknown, _b: unknown) => { - await Promise.resolve(); - }; - expect(parameters(bar)).toBe(2); - }); - - test('function declaration', () => { - expect.assertions(1); - function baz(_a: unknown, _b: unknown) {} - expect(parameters(baz)).toBe(2); - }); - - test('generator function declaration', () => { - expect.assertions(1); - function* baz(_a: unknown, _b: unknown) { - yield null; - } - expect(parameters(baz)).toBe(2); - }); - - test('async function declaration', () => { - expect.assertions(1); - async function baz(_a: unknown, _b: unknown) { - await Promise.resolve(); - } - expect(parameters(baz)).toBe(2); - }); - - test('async generator function declaration', () => { - expect.assertions(1); - async function* baz(_a: unknown, _b: unknown) { - await Promise.resolve(); - yield null; - } - expect(parameters(baz)).toBe(2); - }); - }); - - /* eslint-disable @typescript-eslint/lines-between-class-members, @typescript-eslint/no-empty-function, @typescript-eslint/no-extraneous-class, @typescript-eslint/no-invalid-void-type, @typescript-eslint/no-useless-constructor, class-methods-use-this */ - describe('classes', () => { - test('basic', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - } - expect(parameters(Foo)).toBe(2); - }); - - test('no constructor parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor() {} - } - expect(parameters(Foo)).toBe(0); - }); - - test('extends', () => { - expect.assertions(3); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - } - class Bar extends Foo { - constructor(_a: unknown, _b: unknown, _c: unknown) { - super(_a, _b); - } - } - class Baz extends Bar { - constructor() { - super(null, null, null); - } - } - expect(parameters(Foo)).toBe(2); - expect(parameters(Bar)).toBe(3); - expect(parameters(Baz)).toBe(0); - }); - - test('anonymous', () => { - expect.assertions(1); - expect( - parameters( - class { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - }, - ), - ).toBe(2); - }); - - test('with no constructor function throw', () => { - expect.assertions(4); - class Foo {} - class Bar extends Foo {} - const error = new Error('Invalid function signature'); - expect(() => parameters(Foo)).toThrow(error); - expect(() => parameters(Bar)).toThrow(error); - expect(() => parameters(class {})).toThrow(error); - expect(() => parameters(class extends Foo {})).toThrow(error); - }); - - describe('with methods', () => { - test('case 1: constructor', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - method(this: void, _c: unknown, _d: unknown, _e: unknown) {} - } - expect(parameters(Foo)).toBe(2); - }); - - test('case 2: method parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - method(this: void, _c: unknown, _d: unknown, _e: unknown) {} - } - const instance = new Foo(1, 2); - expect(parameters(instance.method)).toBe(3); - }); - - test('case 3: method parameters no constructor', () => { - expect.assertions(1); - class Foo { - method(this: void, _a: unknown, _b: unknown, _c: unknown) {} - } - const instance = new Foo(); - expect(parameters(instance.method)).toBe(3); - }); - - test('case 4: generator method parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - // eslint-disable-next-line generator-star-spacing - *method(this: void, _c: unknown, _d: unknown, _e: unknown) { - yield null; - } - } - const instance = new Foo(1, 2); - expect(parameters(instance.method)).toBe(3); - }); - - test('case 5: async method parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - async method(this: void, _c: unknown, _d: unknown, _e: unknown) { - await Promise.resolve(); - } - } - const instance = new Foo(1, 2); - expect(parameters(instance.method)).toBe(3); - }); - - test('case 6: async generator method parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - // eslint-disable-next-line generator-star-spacing - async *method(this: void, _c: unknown, _d: unknown, _e: unknown) { - await Promise.resolve(); - yield null; - } - } - const instance = new Foo(1, 2); - expect(parameters(instance.method)).toBe(3); - }); - - test('case 7: anonymous method parameters', () => { - expect.assertions(1); - const instance = new (class { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - method(this: void, _c: unknown, _d: unknown, _e: unknown) {} - })(1, 2); - expect(parameters(instance.method)).toBe(3); - }); - - test('case 8: field parameters', () => { - expect.assertions(1); - class Foo { - method = (_a: unknown, _b: unknown, _c: unknown) => {}; - } - const instance = new Foo(); - expect(parameters(instance.method)).toBe(3); - }); - }); - - describe('with static methods', () => { - test('case 1: constructor', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - static method(this: void, _c: unknown, _d: unknown, _e: unknown) {} - } - expect(parameters(Foo)).toBe(2); - }); - - test('case 2: method parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - static method(this: void, _c: unknown, _d: unknown, _e: unknown) {} - } - expect(parameters(Foo.method)).toBe(3); - }); - - test('case 3: method parameters no constructor', () => { - expect.assertions(1); - // biome-ignore lint/complexity/noStaticOnlyClass: explicit test case - class Foo /* eslint-disable-line unicorn/no-static-only-class */ { - static method(this: void, _a: unknown, _b: unknown, _c: unknown) {} - } - expect(parameters(Foo.method)).toBe(3); - }); - - test('case 4: generator method parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - // eslint-disable-next-line generator-star-spacing - static *method(this: void, _c: unknown, _d: unknown, _e: unknown) { - yield null; - } - } - expect(parameters(Foo.method)).toBe(3); - }); - - test('case 5: async method parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - static async method(this: void, _c: unknown, _d: unknown, _e: unknown) { - await Promise.resolve(); - } - } - expect(parameters(Foo.method)).toBe(3); - }); - - test('case 6: async generator method parameters', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - // eslint-disable-next-line generator-star-spacing - static async *method(this: void, _c: unknown, _d: unknown, _e: unknown) { - await Promise.resolve(); - yield null; - } - } - expect(parameters(Foo.method)).toBe(3); - }); - - test('case 7: anonymous method parameters', () => { - expect.assertions(1); - expect( - parameters( - class { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - static method(this: void, _c: unknown, _d: unknown, _e: unknown) {} - }.method, - ), - ).toBe(3); - }); - - test('case 8: field parameters', () => { - expect.assertions(1); - // biome-ignore lint/complexity/noStaticOnlyClass: explicit test case - class Foo /* eslint-disable-line unicorn/no-static-only-class */ { - static method = (_a: unknown, _b: unknown, _c: unknown) => {}; - } - expect(parameters(Foo.method)).toBe(3); - }); - }); - - describe('with getters and setters', () => { - test('case 1: constructor', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - get prop(): null { - return null; - } - set prop(_c: unknown) {} - } - expect(parameters(Foo)).toBe(2); - }); - - test('case 2: getter/setter throws', () => { - expect.assertions(1); - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - get prop(): null { - return null; - } - set prop(_c: unknown) {} - } - const instance = new Foo(1, 2); - expect(() => parameters(instance.prop)).toThrow(new TypeError('Expected a function')); - }); - }); - - describe('with computed property names', () => { - test('case 1: constructor', () => { - expect.assertions(1); - const prop = 'method'; - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - [prop](this: void, _c: unknown, _d: unknown, _e: unknown) {} - } - expect(parameters(Foo)).toBe(2); - }); - - test('case 2: method parameters', () => { - expect.assertions(1); - const prop = 'method'; - class Foo { - // biome-ignore lint/complexity/noUselessConstructor: simple test case - constructor(_a: unknown, _b: unknown) {} - [prop](this: void, _c: unknown, _d: unknown, _e: unknown) {} - } - const instance = new Foo(1, 2); - expect(parameters(instance[prop])).toBe(3); - }); - }); - }); - /* eslint-enable @typescript-eslint/lines-between-class-members, @typescript-eslint/no-empty-function, @typescript-eslint/no-extraneous-class, @typescript-eslint/no-invalid-void-type, @typescript-eslint/no-useless-constructor, class-methods-use-this */ - - describe('native functions', () => { - /* eslint-disable @typescript-eslint/unbound-method */ - const builtins: [text: string, func: (...args: never[]) => unknown, length: number][] = [ - ['Function', Function, 1], - ['Object', Object, 1], - ['Array', Array, 1], - ['String', String, 1], - ['Number', Number, 1], - ['Boolean', Boolean, 1], - ['Symbol', Symbol, 0], - ['BigInt', BigInt, 1], - // @ts-expect-error - Buffer is callable (obsolete and deprecated Node.js API) - ['Buffer', Buffer, 3], - // @ts-expect-error - explicit test case - ['Function.prototype', Function.prototype, 0], - ['Array.prototype.splice', Array.prototype.splice, 2], - ['Array.prototype.reduce', Array.prototype.reduce, 1], - ['Array.prototype.reduceRight', Array.prototype.reduceRight, 1], - ['Function.prototype.apply', Function.prototype.apply, 2], - ['Function.prototype.call', Function.prototype.call, 1], - ['String.prototype.replace', String.prototype.replace, 2], - ['String.prototype.split', String.prototype.split, 2], - ['String.prototype.match', String.prototype.match, 1], - ['RegExp.prototype.exec', RegExp.prototype.exec, 1], - ['Number.parseInt', Number.parseInt, 2], - ['Symbol.for', Symbol.for, 1], - ['JSON.parse', JSON.parse, 2], - ['JSON.stringify', JSON.stringify, 3], - ['Math.max', Math.max, 2], - ['Math.min', Math.min, 2], - ['Date.now', Date.now, 0], - ['Intl.NumberFormat', Intl.NumberFormat, 0], - ['Intl.DateTimeFormat', Intl.DateTimeFormat, 0], - ['setTimeout', setTimeout, 1], - ['clearTimeout', clearTimeout, 1], - ['setInterval', setInterval, 1], - ['clearInterval', clearInterval, 1], - ['setImmediate', setImmediate, 1], - ['clearImmediate', clearImmediate, 1], - ['fetch', fetch, 2], - ]; - /* eslint-enable @typescript-eslint/unbound-method */ - - test.each(builtins)('case %#: %s', (_, func, length) => { - expect.assertions(3); - const spy = spyOn(console, 'warn').mockImplementation(() => {}); - expect(parameters(func)).toBe(length); - expect(spy).toBeCalledTimes(1); - expect(spy).toHaveBeenCalledWith( - 'Optional parameters cannot be determined for native functions', - ); - spy.mockRestore(); - }); - }); - - describe('non-functions', function closure(this: undefined) { - const notFunctions: [text: string, value: unknown][] = [ - ['null', null], - ['undefined', undefined], - ['true', true], - ['false', false], - ['-1', -1], - ['0', 0], - ['1', 1], - ['Number.MAX_VALUE', Number.MAX_VALUE], - ['Number.POSITIVE_INFINITY', Number.POSITIVE_INFINITY], - ['Number.NEGATIVE_INFINITY', Number.NEGATIVE_INFINITY], - ['Number.NaN', Number.NaN], - ["Symbol('sym')", Symbol('sym')], - ['BigInt(1234)', BigInt(1234)], - ['[]', []], - ['{}', {}], - ['', ''], - ['new Int8Array()', new Int8Array()], - ['new Uint8Array()', new Uint8Array()], - ['new Uint8ClampedArray()', new Uint8ClampedArray()], - ['new Int16Array()', new Int16Array()], - ['new Uint16Array()', new Uint16Array()], - ['new Int32Array()', new Int32Array()], - ['new Uint32Array()', new Uint32Array()], - ['new Float32Array()', new Float32Array()], - ['new Float64Array()', new Float64Array()], - ['new BigInt64Array()', new BigInt64Array()], - ['new BigUint64Array()', new BigUint64Array()], - ['new Map()', new Map()], - ['new Set()', new Set()], - ['new WeakMap()', new WeakMap()], - ['new WeakSet()', new WeakSet()], - ['new Promise(() => {})', new Promise(() => {})], - ['new Date()', new Date()], - ['/(?:)/', /(?:)/], - // biome-ignore lint/nursery/useErrorMessage: simple test case - ['new Error()', new Error()], // eslint-disable-line unicorn/error-message - ['Math', Math], - ['JSON', JSON], - ['Intl', Intl], - ['Object.prototype', Object.prototype], - ['Array.prototype', Array.prototype], - ['String.prototype', String.prototype], - ['Number.prototype', Number.prototype], - ['Boolean.prototype', Boolean.prototype], - ['Symbol.prototype', Symbol.prototype], - ['BigInt.prototype', BigInt.prototype], - ['console', console], - ['window', window], - ['document', document], - ['chrome', chrome], - ['process', process], - ['global', global], - ['globalThis', globalThis], - // eslint-disable-next-line no-restricted-globals - ['self', self], - ['this', this], - // biome-ignore lint/style/noArguments: explicit test case - ['arguments', arguments], // eslint-disable-line prefer-rest-params - ['new.target', new.target], - - // XXX: Although these are built-in classes, they have callable - // constructors which make them functions when accessed directly. - // ['Function', Function], - // ['Object', Object], - // ['Array', Array], - // ['String', String], - // ['Number', Number], - // ['Boolean', Boolean], - // ['Symbol', Symbol], - // ['BigInt', BigInt], - // ['Buffer', Buffer], - // ['Function.prototype', Function.prototype], - ] as const; - - test.each(notFunctions)('throws for %s', (_, value) => { - expect.assertions(1); - expect(() => parameters(value)).toThrow(new TypeError('Expected a function')); - }); - }); - - describe('built-in functions', () => { - const builtIns: [text: string, value: unknown, length: number][] = [ - // biome-ignore lint/security/noGlobalEval: explicit test case - ['eval', eval, 1], // eslint-disable-line no-eval - ['fetch', fetch, 2], - ['setTimeout', setTimeout, 1], - ['clearTimeout', clearTimeout, 1], - ['setInterval', setInterval, 1], - ['clearInterval', clearInterval, 1], - ['setImmediate', setImmediate, 1], - ['clearImmediate', clearImmediate, 1], - // eslint-disable-next-line unicorn/prefer-module - ['require', require, 1], - ]; - - test.each(builtIns)('has expected count for %s', (_, value, length) => { - expect.assertions(1); - expect(parameters(value)).toBe(length); - }); - }); -}); diff --git a/test/unit/test-utils.test.ts b/test/unit/test-utils.test.ts deleted file mode 100644 index 3352344ff..000000000 --- a/test/unit/test-utils.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { afterEach, describe, expect, spyOn, test } from 'bun:test'; -import { Test } from '../TestComponent'; -import * as utilsExports from './utils'; -import { cleanup, performanceSpy, render } from './utils'; - -describe('exports', () => { - const exports = ['cleanup', 'performanceSpy', 'render']; - - test.each(exports)('has "%s" named export', (exportName) => { - expect.assertions(1); - expect(utilsExports).toHaveProperty(exportName); - }); - - test('does not have a default export', () => { - expect.assertions(1); - expect(utilsExports).not.toHaveProperty('default'); - }); - - test('does not export anything else', () => { - expect.assertions(1); - expect(Object.keys(utilsExports)).toHaveLength(exports.length); - }); -}); - -describe('render ', () => { - test('is a function', () => { - expect.assertions(2); - expect(render).toBeFunction(); - expect(render).not.toBeClass(); - }); - - test('expects 1 parameter', () => { - expect.assertions(1); - expect(render).toHaveParameters(1, 0); - }); -}); - -describe('render', () => { - afterEach(cleanup); - - test('returns a container element', () => { - expect.assertions(2); - const rendered = render(document.createElement('div')); - expect(rendered).toHaveProperty('container'); - expect(rendered.container).toBeInstanceOf(window.Element); - }); - - test('mounts supplied element in container', () => { - expect.assertions(1); - const el = document.createElement('span'); - const rendered = render(el); - expect(rendered.container.firstChild).toBe(el); - }); - - test('mounts container div to document body', () => { - expect.assertions(3); - expect(document.body.firstChild).toBeNull(); - const rendered = render(document.createElement('div')); - expect(document.body.firstChild).toBe(rendered.container); - expect(document.body.firstChild).toBeInstanceOf(window.HTMLDivElement); - }); - - test('mounts containers when other DOM elements exist on document body', () => { - expect.assertions(2); - document.body.append(document.createElement('span')); - document.body.append(document.createElement('span')); - render(document.createElement('a')); - render(document.createElement('a')); - document.body.append(document.createElement('span')); - expect(document.body.childNodes).toHaveLength(5); - expect(document.body.innerHTML).toBe( - '
', - ); - document.body.textContent = ''; - }); - - test('renders Test component correctly', () => { - expect.assertions(1); - const rendered = render(Test({ text: 'abc' })); - expect(rendered.container.innerHTML).toBe('
abc
'); - }); - - describe('unmount method', () => { - test('is a function', () => { - expect.assertions(3); - const rendered = render(document.createElement('div')); - expect(rendered).toHaveProperty('unmount'); - expect(rendered.unmount).toBeFunction(); - expect(rendered.unmount).not.toBeClass(); - }); - - test('expects no parameters', () => { - expect.assertions(1); - const rendered = render(document.createElement('div')); - expect(rendered.unmount).toHaveParameters(0, 0); - }); - - test('removes supplied element from container', () => { - expect.assertions(3); - const rendered = render(document.createElement('div')); - expect(rendered.container.firstChild).toBeTruthy(); - rendered.unmount(); - expect(rendered.container).toBeTruthy(); - expect(rendered.container.firstChild).toBeNull(); - }); - }); - - describe('debug method', () => { - test('is a function', () => { - expect.assertions(3); - const rendered = render(document.createElement('div')); - expect(rendered).toHaveProperty('debug'); - expect(rendered.debug).toBeFunction(); - expect(rendered.debug).not.toBeClass(); - }); - - test('expects 1 optional parameter', () => { - expect.assertions(1); - const rendered = render(document.createElement('div')); - expect(rendered.debug).toHaveParameters(0, 1); - }); - - test('prints to $console', () => { - expect.assertions(1); - const spy = spyOn($console, 'log').mockImplementation(() => {}); - const rendered = render(document.createElement('div')); - rendered.debug(); - expect(spy).toHaveBeenCalledTimes(1); - // TODO: Uncomment once biome has a HTML parser. - // expect(spy).toHaveBeenCalledWith('DEBUG:\n
\n'); - spy.mockRestore(); - }); - - test('does not print to console, only $console', () => { - expect.assertions(2); - const spy = spyOn(console, 'log').mockImplementation(() => {}); - const spy2 = spyOn($console, 'log').mockImplementation(() => {}); - const rendered = render(document.createElement('div')); - rendered.debug(); - expect(spy).not.toHaveBeenCalled(); - expect(spy2).toHaveBeenCalledTimes(1); - spy.mockRestore(); - spy2.mockRestore(); - }); - - // TODO: Don't skip once biome has a HTML parser. - test.skip('prints prettified container DOM to console', () => { - expect.assertions(2); - const spy = spyOn($console, 'log').mockImplementation(() => {}); - const main = document.createElement('main'); - main.append( - document.createElement('div'), - document.createElement('div'), - document.createElement('div'), - ); - const rendered = render(main); - rendered.debug(); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenCalledWith( - 'DEBUG:\n
\n
\n
\n
\n
\n', - ); - spy.mockRestore(); - }); - }); -}); - -describe('cleanup', () => { - test('is a function', () => { - expect.assertions(2); - expect(cleanup).toBeFunction(); - expect(cleanup).not.toBeClass(); - }); - - test('expects no parameters', () => { - expect.assertions(1); - expect(cleanup).toHaveParameters(0, 0); - }); - - test('throws when there are no rendered components', () => { - expect.assertions(1); - expect(() => { - cleanup(); - }).toThrow(); - }); - - test('removes mounted container from document body', () => { - expect.assertions(2); - render(document.createElement('div')); - expect(document.body.firstChild).toBeTruthy(); - cleanup(); - expect(document.body.firstChild).toBeNull(); - }); - - test('removes multiple mounted containers from document body', () => { - expect.assertions(2); - render(document.createElement('div')); - render(document.createElement('div')); - render(document.createElement('div')); - expect(document.body.childNodes).toHaveLength(3); - cleanup(); - expect(document.body.childNodes).toHaveLength(0); - }); - - test('only removes mounted containers and not other DOM nodes', () => { - expect.assertions(5); - document.body.append(document.createElement('span')); - document.body.append(document.createElement('span')); - render(document.createElement('a')); - render(document.createElement('a')); - document.body.append(document.createElement('span')); - expect(document.body.childNodes).toHaveLength(5); - cleanup(); - expect(document.body.childNodes).toHaveLength(3); - for (const node of document.body.childNodes) { - expect(node).toBeInstanceOf(window.HTMLSpanElement); - } - document.body.textContent = ''; - }); -}); - -describe('performanceSpy', () => { - test('is a function', () => { - expect.assertions(2); - expect(performanceSpy).toBeFunction(); - expect(performanceSpy).not.toBeClass(); - }); - - test('expects no parameters', () => { - expect.assertions(1); - expect(performanceSpy).toHaveParameters(0, 0); - }); - - test('returns a function', () => { - expect.hasAssertions(); // variable number of assertions - const check = performanceSpy(); - expect(check).toBeFunction(); - expect(check).not.toBeClass(); - check(); - }); - - test('returned function expects no parameters', () => { - expect.hasAssertions(); // variable number of assertions - const check = performanceSpy(); - expect(check).toHaveParameters(0, 0); - check(); - }); - - test('passes when no performance methods are called', () => { - expect.hasAssertions(); // variable number of assertions - const check = performanceSpy(); - check(); - }); - - // TODO: Don't skip this once test.failing() is supported in bun. We need to - // check that the expect() inside the performanceSpy() fails (meaning this - // test should then be a pass). - // ↳ https://jestjs.io/docs/api#testfailingname-fn-timeout - test.skip('fails when performance methods are called', () => { - expect.hasAssertions(); // variable number of assertions - const check = performanceSpy(); - performance.mark('a'); - performance.measure('a', 'a'); - check(); - }); -}); diff --git a/test/unit/theme.test.ts b/test/unit/theme.test.ts index 4fd3bdc53..cfa85b793 100644 --- a/test/unit/theme.test.ts +++ b/test/unit/theme.test.ts @@ -1,9 +1,6 @@ /* eslint-disable consistent-return */ import { afterEach, describe, expect, test } from 'bun:test'; -import themes from '../../dist/themes.json'; -import type { UserStorageData } from '../../src/types'; -import { reset } from '../setup'; import { DECLARATION, type Element, @@ -13,7 +10,10 @@ import { isHexColor, isLightOrDark, walk, -} from './css-engine'; +} from '@maxmilton/test-utils/css'; +import themes from '../../dist/themes.json'; +import type { UserStorageData } from '../../src/types'; +import { reset } from '../setup'; const themeNames = [ 'auto', diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index 7d37e53bf..24adfe049 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -1,5 +1,5 @@ import { afterAll, beforeAll, describe, expect, mock, spyOn, test } from 'bun:test'; -import { target } from 'happy-dom/lib/PropertySymbol.js'; // eslint-disable-line import/extensions +import { target } from 'happy-dom/lib/PropertySymbol.js'; import { DEFAULT_SECTION_ORDER, handleClick } from '../../src/utils'; describe('DEFAULT_SECTION_ORDER', () => { diff --git a/test/unit/utils.ts b/test/unit/utils.ts deleted file mode 100644 index 215d2e144..000000000 --- a/test/unit/utils.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint "@typescript-eslint/no-invalid-void-type": "warn" */ - -import { type Mock, expect, spyOn } from 'bun:test'; - -export interface RenderResult { - /** A wrapper DIV which contains your mounted component. */ - container: HTMLDivElement; - /** - * A helper to print the HTML structure of the mounted container. The HTML is - * prettified and may not accurately represent your actual HTML. It's intended - * for debugging tests only and should not be used in any assertions. - * - * @param element - An element to inspect. Default is the mounted container. - */ - debug(this: void, element?: Element): void; - unmount(this: void): void; -} - -const mountedContainers = new Set(); - -export function render(component: Node): RenderResult { - const container = document.createElement('div'); - - container.appendChild(component); - document.body.appendChild(container); - - mountedContainers.add(container); - - return { - container, - debug(el = container) { - // const { format } = await import('prettier'); - // const html = await format(el.innerHTML, { parser: 'html' }); - // $console.log(`DEBUG:\n${html}`); - - // FIXME: Replace with biome once it has a HTML parser - $console.log(`DEBUG:\n${el.innerHTML}`); - }, - unmount() { - // eslint-disable-next-line unicorn/prefer-dom-node-remove - container.removeChild(component); - }, - }; -} - -export function cleanup(): void { - if (mountedContainers.size === 0) { - throw new Error('No components mounted, did you forget to call render()?'); - } - - for (const container of mountedContainers) { - if (container.parentNode === document.body) { - container.remove(); - } - - mountedContainers.delete(container); - } -} - -// TODO: Use this implementation if happy-dom removes internal performance.now calls. -// const methods = Object.getOwnPropertyNames(performance) as (keyof Performance)[]; -// -// export function performanceSpy(): () => void { -// const spies: Mock<() => void>[] = []; -// -// for (const method of methods) { -// spies.push(spyOn(performance, method)); -// } -// -// return /** check */ () => { -// for (const spy of spies) { -// expect(spy).not.toHaveBeenCalled(); -// spy.mockRestore(); -// } -// }; -// } - -const originalNow = performance.now.bind(performance); -const methods = Object.getOwnPropertyNames(performance) as (keyof Performance)[]; - -export function performanceSpy(): () => void { - const spies: Mock<() => void>[] = []; - let happydomInternalNowCalls = 0; - - function now() { - // biome-ignore lint/nursery/useErrorMessage: only used to get stack - const callerLocation = new Error().stack!.split('\n')[3]; // eslint-disable-line unicorn/error-message - if (callerLocation.includes('/node_modules/happy-dom/lib/')) { - happydomInternalNowCalls++; - } - return originalNow(); - } - - for (const method of methods) { - spies.push( - method === 'now' - ? spyOn(performance, method).mockImplementation(now) - : spyOn(performance, method), - ); - } - - return /** check */ () => { - for (const spy of spies) { - if (spy.getMockName() === 'now') { - // HACK: Workaround for happy-dom calling performance.now internally. - // biome-ignore lint/nursery/noMisplacedAssertion: only used within tests - expect(spy).toHaveBeenCalledTimes(happydomInternalNowCalls); - } else { - // biome-ignore lint/nursery/noMisplacedAssertion: only used within tests - expect(spy).not.toHaveBeenCalled(); - } - spy.mockRestore(); - } - }; -} diff --git a/tsconfig.json b/tsconfig.json index f3bea7ca9..449389528 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": false, // covered by eslint "noImplicitOverride": true, + "noUncheckedSideEffectImports": true, "noUnusedLocals": false, // covered by eslint "noUnusedParameters": false, // covered by eslint "verbatimModuleSyntax": true diff --git a/tsconfig.node.json b/tsconfig.node.json index a57b1a9a9..8cb26d89d 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -6,6 +6,7 @@ "allowSyntheticDefaultImports": true }, "include": [ + "**/*.cjs", "package.json" // imported in manifest.config.ts ] }