From efd74e5c45c6182546e20d8a1241f9ed168c762c Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 17:15:49 +0900 Subject: [PATCH 1/9] chore: Add new TypeScript config option --- tsconfig.json | 1 + tsconfig.node.json | 1 + 2 files changed, 2 insertions(+) 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 ] } From e38d67da5355eb8940b40d00f1fd0812b43a690b Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 17:17:18 +0900 Subject: [PATCH 2/9] fix: Remove lightningcss version override and junk CSS injected in themes --- build.ts | 5 +++-- manifest.config.ts | 2 +- package.json | 5 +---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/build.ts b/build.ts index df19a4f63..4d101c94c 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 Uint8Array(Buffer.from(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) { diff --git a/manifest.config.ts b/manifest.config.ts index 0283a5c91..574adaa13 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', diff --git a/package.json b/package.json index f2ee0b468..bac547853 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,6 @@ "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" }, @@ -46,7 +43,7 @@ "eslint-plugin-import": "2.31.0", "eslint-plugin-unicorn": "55.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", From f0d873abc2a689a668535a6c89e8508af2326302 Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 17:18:00 +0900 Subject: [PATCH 3/9] chore: Use new external tooling configs + update dependencies --- bun.lockb | Bin 171168 -> 114152 bytes package.json | 33 ++++++++++++++------------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/bun.lockb b/bun.lockb index cdc586c5495aa53f2dc6cee4b934c2ada5c9c950..ecf5e7edd2922bd6ea7b31468a33b4f73b596153 100755 GIT binary patch delta 29690 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 literal 171168 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 DxkP3R diff --git a/package.json b/package.json index bac547853..89f3743bf 100644 --- a/package.json +++ b/package.json @@ -25,29 +25,24 @@ "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.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" } } From 8d75388bfdc7fa4f3fcd09fe479705570fd175cb Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 17:18:31 +0900 Subject: [PATCH 4/9] chore: Rework biome config --- biome.jsonc | 105 +++++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 51 deletions(-) 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"] } } ] From df9c2c3cf49f546d68b71becbc37280414514d93 Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 17:18:47 +0900 Subject: [PATCH 5/9] chore: Rework eslint config --- eslint.config.mjs | 81 +++++++++++------------------------------------ 1 file changed, 18 insertions(+), 63 deletions(-) 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/**'], }, ); From 09d91f203168a5a2ed2dc8b837958ffdee247339 Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 17:20:07 +0900 Subject: [PATCH 6/9] chore: Use new `@maxmilton/test-utils` package in tests --- test/TestComponent.ts | 28 - test/setup.ts | 179 +--- test/unit/BookmarkBar.test.ts | 2 +- test/unit/BookmarkNode.test.ts | 2 +- test/unit/Link.test.ts | 2 +- test/unit/Menu.test.ts | 2 +- test/unit/Search.test.ts | 2 +- test/unit/css-engine.ts | 165 ---- test/unit/newtab.test.ts | 4 +- test/unit/settings.test.ts | 4 +- test/unit/test-css-engine.test.ts | 367 ------- test/unit/test-setup.test.ts | 1494 ----------------------------- test/unit/test-utils.test.ts | 265 ----- test/unit/theme.test.ts | 8 +- test/unit/utils.test.ts | 2 +- test/unit/utils.ts | 115 --- 16 files changed, 19 insertions(+), 2622 deletions(-) delete mode 100644 test/TestComponent.ts delete mode 100644 test/unit/css-engine.ts delete mode 100644 test/unit/test-css-engine.test.ts delete mode 100644 test/unit/test-setup.test.ts delete mode 100644 test/unit/test-utils.test.ts delete mode 100644 test/unit/utils.ts 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/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/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(); - } - }; -} From 38f9ba96c4653a3dd7047c281bfdb3cf6cc715c8 Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 17:20:27 +0900 Subject: [PATCH 7/9] chore: Fix sourcemaps in dev builds --- build.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.ts b/build.ts index 4d101c94c..fbb0ae09c 100644 --- a/build.ts +++ b/build.ts @@ -137,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); @@ -149,6 +149,7 @@ const out2 = await Bun.build({ outdir: 'dist', target: 'browser', minify: !dev, + sourcemap: dev ? 'linked' : 'none', }); console.timeEnd('build2'); console.log(out2); From 059fb2718b1fcf0d3c9162b3f982916fbf7f4911 Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 17:21:25 +0900 Subject: [PATCH 8/9] chore: Fix or ignore new lint errors --- manifest.config.ts | 1 + src/components/BookmarkBar.ts | 1 + src/components/BookmarkNode.ts | 3 ++- src/components/Search.ts | 6 ++++++ src/settings.ts | 2 +- src/sw.ts | 2 +- test/e2e/screenshot.css | 2 +- test/unit/index.test.ts | 1 - 8 files changed, 13 insertions(+), 5 deletions(-) diff --git a/manifest.config.ts b/manifest.config.ts index 574adaa13..893ee0788 100644 --- a/manifest.config.ts +++ b/manifest.config.ts @@ -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/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/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/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}`); From 33b8a9ab865703124c93a3584e6de4a7d7f35fb0 Mon Sep 17 00:00:00 2001 From: Max Milton Date: Sun, 27 Oct 2024 18:05:25 +0900 Subject: [PATCH 9/9] chore: Optimise Uint8Array creation in build --- build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.ts b/build.ts index fbb0ae09c..4fb63ed30 100644 --- a/build.ts +++ b/build.ts @@ -34,7 +34,7 @@ function compileCSS(src: string, from: string) { // TODO: Migrate to bun CSS handling (which is based on lightningcss). const minified = lightningcss.transform({ filename: from, - code: new Uint8Array(Buffer.from(compiled.css)), + code: new TextEncoder().encode(compiled.css), minify: !dev, // eslint-disable-next-line no-bitwise targets: { chrome: 123 << 16 }, // matches manifest minimum_chrome_version