diff --git a/AGENTS.md b/AGENTS.md index 05a744de28..eac9037cc1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,7 +154,7 @@ export const SchemaRenderer = ({ schema }: { schema: UIComponent }) => { 单跑稳定绿、全量并行下偶发红的测试,**根因几乎总是同一个**:一段**无界的模块加载被计入了一个有界的窗口**。满并行下 Vite 的 transform 管线是饱和的(单 `dom-heavy` 项目就 ~60s transform),实测一次首包 `import()` 可达 **976ms** —— 已吃掉 RTL `findBy`/`waitFor` 默认 **1000ms** 预算的 97.6%。于是断言在和模块加载器抢时间,红绿取决于机器负载而不是被测代码。 - **断言的内容落在 `React.lazy` / 动态 `import()` 边界之后 → 在测试文件的模块作用域直接 `import` 该模块**(`import '@object-ui/plugin-charts';` + 一行注释说明原因)。成本进入 import 阶段,**不受任何 test/hook 超时约束**。specifier 必须与被测组件里的**完全一致** —— ESM 按解析后的 specifier 缓存,这样组件自己的 `React.lazy` 工厂才会立刻 resolve。 -- **不要用 `beforeAll` 预热**:它受 `hookTimeout`(**10s**)约束,比它取代的 `testTimeout`(**15s**)**更窄**,那只是把问题挪个窝。 +- **不要用 `beforeAll` 预热**:它受 `hookTimeout`(**10s**)约束,比它取代的 `testTimeout`(**15s**)**更窄**,那只是把问题挪个窝。**这一条现在由 lint 机械强制** —— `object-ui/no-dynamic-import-in-test-hook`(error)禁止在 `beforeAll`/`beforeEach` 体内 `await import(…)`。两种写法**不会**被它拦(都是正当的,已在规则的 RuleTester 里钉住):传给注册器的**惰性工厂**(`registerLazy('x', () => import('./x'))` —— hook 只是登记 loader,并不执行导入);以及同一 hook 里调了 `vi.resetModules()`/`doMock`/`stubEnv`/`stubGlobal` 的**故意重导入**(它必须读取只在 hook 时存在的状态,提到模块作用域反而会改坏测试)。 - **禁止**用「调高超时」或「把文件塞进 `vitest.config.mts` 的 `heavyDomTests`」来修 flaky —— 两者都只是把竞态藏起来。`heavyDomTests` 只用于 registry「`` not registered」这类失败。 - 失败现场会直接指认根因:dump 里若仍是 Suspense fallback(如 `Loading report renderer…`),就是本条;**hook 超时表现为「失败的*文件* + 0 个失败*测试*」**(其余全部 skipped),别误读成断言失败。 - 顺手体检:别把 `keysOf(x)` 这类整体计算写在 `.filter()` 谓词里(每个元素重算一遍)。`all-locales-key-parity` 曾因此 7.51s,提升出谓词后 **25ms**。 diff --git a/eslint-rules/index.js b/eslint-rules/index.js index e34e256be8..8a5299c24d 100644 --- a/eslint-rules/index.js +++ b/eslint-rules/index.js @@ -4,11 +4,13 @@ import noSyntheticEventTrigger from './no-synthetic-event-trigger.js'; import noInlineSpecConfig from './no-inline-spec-config.js'; import noTryCatchAroundHook from './no-try-catch-around-hook.js'; +import noDynamicImportInTestHook from './no-dynamic-import-in-test-hook.js'; export default { rules: { 'no-synthetic-event-trigger': noSyntheticEventTrigger, 'no-inline-spec-config': noInlineSpecConfig, 'no-try-catch-around-hook': noTryCatchAroundHook, + 'no-dynamic-import-in-test-hook': noDynamicImportInTestHook, }, }; diff --git a/eslint-rules/no-dynamic-import-in-test-hook.js b/eslint-rules/no-dynamic-import-in-test-hook.js new file mode 100644 index 0000000000..b7288e68bc --- /dev/null +++ b/eslint-rules/no-dynamic-import-in-test-hook.js @@ -0,0 +1,143 @@ +/** + * ObjectUI ESLint rule: no-dynamic-import-in-test-hook + * + * Bans `await import(…)` in a `beforeAll`/`beforeEach` body. Import the module + * at module scope instead. + * + * A dynamic import inside a hook bills the module's cold Vite transform to + * `hookTimeout`. That transform is unbounded work — under full-suite + * parallelism the transform pipeline is saturated (the `dom-heavy` project + * alone spends ~60s in transform) — so the hook's budget is really a bet on + * machine load, and the test's reliability stops depending on the code it + * covers. Imported at module scope the same cost lands in the file's import + * phase, which no test or hook timeout applies to. + * + * This exists because raising the timeout is the intuitive "fix" and it does + * not work. objectui#3010 found five load-sensitive flaky tests of this shape; + * a sweep then found 36 more files, EVERY one already carrying a raised + * timeout, on an escalating ladder — 15s → 30s → 60s. One of them even left + * the escalation as advice (`// Increase timeout to 30 seconds for heavy + * renderer imports`), and `plugin-kanban` blew its raised 15s budget anyway at + * 15021ms. #3021 converted the four riskiest. The prose rule in AGENTS.md §9 + * did not hold on its own, which is why this is mechanical. + * + * Scope, deliberately narrow to stay false-positive free: + * - Only `beforeAll` / `beforeEach` as BARE identifiers. A `foo.beforeAll()` + * member call is somebody else's API. + * - The hook frame RESETS inside a nested function, so a lazy FACTORY — + * `registerLazy('x', () => import('./x'))` — is fine: the hook registers + * the loader, it does not run the import. + * - A hook that also establishes per-run module state (`vi.resetModules`, + * `vi.doMock`, `vi.doUnmock`, `vi.stubEnv`, `vi.stubGlobal`) is EXEMPT. The + * re-import is the point there — it has to observe state that only exists + * at hook time, so hoisting it would break the test rather than speed it up. + * + * No autofixer on purpose. The mechanical cases are a one-line move, but the + * general case captures the module into a variable the tests read, and hoisting + * that means introducing top-level await and reordering side effects — not a + * rewrite a fixer should make unattended. + * + * @type {import('eslint').Rule.RuleModule} + */ + +/** Vitest lifecycle hooks whose body is billed to `hookTimeout`. */ +const HOOKS = new Set(['beforeAll', 'beforeEach']); + +/** + * `vi.*` calls that establish per-run module state. A dynamic import in the + * same hook is deliberately re-reading that state, so it cannot be hoisted. + */ +const MODULE_STATE_HELPERS = new Set([ + 'resetModules', + 'doMock', + 'doUnmock', + 'stubEnv', + 'stubGlobal', +]); + +/** True for `beforeAll(fn)` / `beforeEach(fn, timeout)` — bare identifier only. */ +function isHookCall(node) { + return ( + node?.type === 'CallExpression' && + node.callee.type === 'Identifier' && + HOOKS.has(node.callee.name) + ); +} + +/** True for `vi.resetModules()` / `vitest.doMock(…)` and friends. */ +function isModuleStateHelper(node) { + const callee = node.callee; + return ( + callee.type === 'MemberExpression' && + callee.object.type === 'Identifier' && + (callee.object.name === 'vi' || callee.object.name === 'vitest') && + callee.property.type === 'Identifier' && + MODULE_STATE_HELPERS.has(callee.property.name) + ); +} + +export default { + meta: { + type: 'problem', + docs: { + description: + 'Disallow `await import(…)` inside a beforeAll/beforeEach body — it bills an unbounded module transform to hookTimeout, making the test load-sensitive (objectui#3010/#3021).', + recommended: true, + }, + schema: [], + messages: { + banned: + "Do not load a module inside '{{hook}}' — the cold transform is billed to hookTimeout, so the test passes or fails on machine load. Import it at module scope instead (`import '{{source}}';`), which moves the cost to the import phase where no test or hook timeout applies. Raising the timeout is not the fix: every one of the 36 files found this way already had a raised timeout, escalating 15s -> 30s -> 60s (objectui#3010/#3021, AGENTS.md section 9). If the import must re-read per-run state, keep it and call vi.resetModules() in the same hook.", + }, + }, + create(context) { + // One frame per enclosing function. `hook` is the hook name when this + // function is *directly* the hook's callback, else undefined — so the frame + // resets inside a nested function and a lazy factory never reports. + const frames = [{ hook: undefined, candidates: [], exempt: false }]; + const top = () => frames[frames.length - 1]; + + return { + ':function'(node) { + const parent = node.parent; + const hook = isHookCall(parent) && parent.arguments.includes(node) + ? parent.callee.name + : undefined; + frames.push({ hook, candidates: [], exempt: false }); + }, + + ':function:exit'() { + const frame = frames.pop(); + if (!frame.hook || frame.exempt) return; + for (const { node, source } of frame.candidates) { + context.report({ + node, + messageId: 'banned', + data: { hook: frame.hook, source }, + }); + } + }, + + // `import('…')` — parsers spell this `ImportExpression`. + ImportExpression(node) { + const frame = top(); + if (!frame.hook) return; + frame.candidates.push({ + node, + source: node.source?.type === 'Literal' ? String(node.source.value) : 'the-module', + }); + }, + + CallExpression(node) { + // Mark the whole hook exempt, wherever in its body the helper sits. + if (!isModuleStateHelper(node)) return; + for (let i = frames.length - 1; i >= 0; i -= 1) { + if (frames[i].hook) { + frames[i].exempt = true; + return; + } + } + }, + }; + }, +}; diff --git a/eslint-rules/no-dynamic-import-in-test-hook.test.js b/eslint-rules/no-dynamic-import-in-test-hook.test.js new file mode 100644 index 0000000000..7ad19759dc --- /dev/null +++ b/eslint-rules/no-dynamic-import-in-test-hook.test.js @@ -0,0 +1,94 @@ +/** + * Pins `no-dynamic-import-in-test-hook` against the shapes that must NOT report, + * every one of them real code in this repo: + * + * - `registerLazy('x', () => import('./x'))` inside a hook — the hook installs + * a loader, it never runs the import. This is how the console registers its + * plugin layer, so a rule that flagged it would be unusable. + * - `vi.resetModules(); mod = await import('./x')` — the re-import is the whole + * point; hoisting it would break the test rather than speed it up. + * - a dynamic import at module scope, or in a test body — only hook bodies + * carry `hookTimeout`. + * + * The invalid cases are the exact shapes objectui#3010/#3021 converted, + * including the `}, 15000)` form that raising the timeout failed to fix. + */ +import { describe, it, afterAll } from 'vitest'; +import { RuleTester } from 'eslint'; +import rule from './no-dynamic-import-in-test-hook.js'; + +RuleTester.afterAll = afterAll; +RuleTester.it = it; +RuleTester.describe = describe; + +const ruleTester = new RuleTester(); + +ruleTester.run('no-dynamic-import-in-test-hook', rule, { + valid: [ + // The correct shape: module scope, no hook, no raised timeout. + `import './index'; + describe('Plugin', () => { it('registers', () => { expect(1).toBe(1); }); });`, + // A lazy FACTORY passed to a registrar: the import is never called here. + `beforeAll(() => { registerLazy('object-chart', () => import('@object-ui/plugin-charts')); });`, + // Same, one nesting level deeper. + `beforeAll(() => { tags.forEach((t) => registerLazy(t, () => import('./' + t))); });`, + // Deliberate re-import against per-run module state — cannot be hoisted. + `beforeEach(async () => { vi.resetModules(); mod = await import('./thing'); });`, + `beforeAll(async () => { vi.doMock('./dep', () => ({})); mod = await import('./thing'); });`, + `beforeEach(async () => { vi.stubEnv('TZ', 'UTC'); mod = await import('./clock'); });`, + // The helper may sit after the import and still exempt the hook. + `beforeEach(async () => { mod = await import('./thing'); vi.resetModules(); });`, + // Module scope is exactly what the rule wants. + `const mod = await import('./thing');`, + // A test body is not a hook body. + `it('loads on demand', async () => { const m = await import('./thing'); expect(m).toBeTruthy(); });`, + // Other hooks are not billed the warm-up cost this rule is about. + `afterAll(async () => { await import('./teardown'); });`, + // A member call that merely shares the name is somebody else's API. + `suite.beforeAll(async () => { await import('./thing'); });`, + // A hook with no dynamic import at all. + `beforeAll(() => { ComponentRegistry.reset(); });`, + ], + invalid: [ + // The shape #3021 converted, raised timeout and all. + { + code: `describe('Plugin Markdown', () => { + beforeAll(async () => { await import('./index'); }, 15000); + });`, + errors: [{ messageId: 'banned' }], + }, + // The 30s `packages/components` cluster. + { + code: `beforeAll(async () => { await import('../../../renderers'); }, 30000);`, + errors: [{ messageId: 'banned' }], + }, + // Capturing into a variable is still the same cost. + { + code: `let mod; beforeAll(async () => { mod = await import('./index'); });`, + errors: [{ messageId: 'banned' }], + }, + // `beforeEach` pays it once per test, which is worse. + { + code: `beforeEach(async () => { await import('./index'); });`, + errors: [{ messageId: 'banned' }], + }, + // Two imports in one hook report twice — each is a separate move. + { + code: `beforeAll(async () => { + await import('@object-ui/components'); + await import('../register-plugins'); + }, 60000);`, + errors: [{ messageId: 'banned' }, { messageId: 'banned' }], + }, + // Concurrent loads are still loads. + { + code: `beforeAll(async () => { await Promise.all([import('./a'), import('./b')]); });`, + errors: [{ messageId: 'banned' }, { messageId: 'banned' }], + }, + // An unrelated `vi.*` call must not buy an exemption. + { + code: `beforeAll(async () => { vi.useFakeTimers(); await import('./index'); });`, + errors: [{ messageId: 'banned' }], + }, + ], +}); diff --git a/eslint.config.js b/eslint.config.js index 0dd58e38ec..f955ebeab9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -64,6 +64,20 @@ export default tseslint.config({ // this lints clean today. 'object-ui/no-try-catch-around-hook': 'error', }, +}, { + // objectui#3010/#3021 ratchet — a module loaded inside beforeAll/beforeEach + // bills its cold Vite transform to `hookTimeout`, so the test passes or fails + // on machine load. Raising the timeout is the intuitive fix and it does not + // work: all 37 files found this way already had a raised timeout, escalating + // 15s -> 30s -> 60s, and plugin-kanban blew its raised 15s anyway at 15021ms. + // Scoped to test files — a dynamic import in app code is normal code + // splitting. Error so a new one fails CI; every existing site was converted + // first, so this lints clean today. + files: ['**/*.test.{ts,tsx}', '**/__tests__/**/*.{ts,tsx}'], + plugins: { 'object-ui': objectUi }, + rules: { + 'object-ui/no-dynamic-import-in-test-hook': 'error', + }, }, { // Type-discipline ratchet, scoped to the canonical view-schema file: a // spec-backed view-config field must reference its @objectstack/spec type, diff --git a/packages/components/src/__tests__/basic-renderers.test.tsx b/packages/components/src/__tests__/basic-renderers.test.tsx index 512b3d3eff..4be701cc7e 100644 --- a/packages/components/src/__tests__/basic-renderers.test.tsx +++ b/packages/components/src/__tests__/basic-renderers.test.tsx @@ -6,18 +6,17 @@ * LICENSE file in the root directory of this source tree. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { renderComponent, validateComponentRegistration, getAllDisplayIssues, checkDOMStructure, } from './test-utils'; - -// Import renderers to ensure registration -beforeAll(async () => { - await import('../renderers'); -}, 30000); // Increase timeout to 30 seconds for heavy renderer imports +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; /** * Comprehensive tests for basic renderer components diff --git a/packages/components/src/__tests__/data-list.test.tsx b/packages/components/src/__tests__/data-list.test.tsx index 62ba5d655f..c45bf2d963 100644 --- a/packages/components/src/__tests__/data-list.test.tsx +++ b/packages/components/src/__tests__/data-list.test.tsx @@ -6,14 +6,14 @@ * LICENSE file in the root directory of this source tree. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; function renderType(schema: any) { const Component = ComponentRegistry.get(schema.type); diff --git a/packages/components/src/__tests__/data-table-inline-edit.test.tsx b/packages/components/src/__tests__/data-table-inline-edit.test.tsx index 0d30548968..6914f11686 100644 --- a/packages/components/src/__tests__/data-table-inline-edit.test.tsx +++ b/packages/components/src/__tests__/data-table-inline-edit.test.tsx @@ -18,15 +18,15 @@ * date columns could only be typed by hand. A `type: 'date'` column must * render a native date picker (). */ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; import { renderComponent } from './test-utils'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; const editableSchema = { type: 'data-table' as const, diff --git a/packages/components/src/__tests__/data-table-manual-pagination.test.tsx b/packages/components/src/__tests__/data-table-manual-pagination.test.tsx index 6d7650f39e..987abd77e0 100644 --- a/packages/components/src/__tests__/data-table-manual-pagination.test.tsx +++ b/packages/components/src/__tests__/data-table-manual-pagination.test.tsx @@ -15,14 +15,14 @@ * navigation is reported via `onPageChange` instead of mutating internal state. * This is what lets a grid reach records beyond the first batch. */ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { renderComponent } from './test-utils'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; describe('data-table — manual (server-side) pagination', () => { // 5 rows that represent page 2 of a 3125-row result set, page size 50. diff --git a/packages/components/src/__tests__/data-table-row-click.test.tsx b/packages/components/src/__tests__/data-table-row-click.test.tsx index 855bfb0fa2..85b603b582 100644 --- a/packages/components/src/__tests__/data-table-row-click.test.tsx +++ b/packages/components/src/__tests__/data-table-row-click.test.tsx @@ -16,15 +16,15 @@ * page because the click event bubbled up through React's synthetic tree to * the onClick handler which calls onRowClick. */ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; import { renderComponent } from './test-utils'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; describe('data-table — row click heuristic ignores overlay items', () => { const baseSchema = { diff --git a/packages/components/src/__tests__/data-table-selection-mode.test.tsx b/packages/components/src/__tests__/data-table-selection-mode.test.tsx index 4e01e8493c..9df47a33c6 100644 --- a/packages/components/src/__tests__/data-table-selection-mode.test.tsx +++ b/packages/components/src/__tests__/data-table-selection-mode.test.tsx @@ -21,16 +21,16 @@ * - `single` offers no select-all and replaces the selection on each pick; * - `multiple` (and legacy `true`) keeps the accumulate + select-all UX. */ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { SelectionConfigSchema } from '@objectstack/spec/ui'; import { renderComponent } from './test-utils'; import { SUPPORTED_SELECTION_MODES } from '../renderers/complex/data-table'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; /** The spec's selection vocabulary, read through the `.default()` wrapper. */ function specSelectionModes(): string[] { diff --git a/packages/components/src/__tests__/form-renderers.test.tsx b/packages/components/src/__tests__/form-renderers.test.tsx index 30f6391047..50f6300b32 100644 --- a/packages/components/src/__tests__/form-renderers.test.tsx +++ b/packages/components/src/__tests__/form-renderers.test.tsx @@ -6,18 +6,17 @@ * LICENSE file in the root directory of this source tree. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { screen } from '@testing-library/react'; import { renderComponent, validateComponentRegistration, getAllDisplayIssues, } from './test-utils'; - -// Import renderers to ensure registration -beforeAll(async () => { - await import('../renderers'); -}, 30000); // Increase timeout to 30 seconds for heavy renderer imports +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; /** * Comprehensive tests for form renderer components diff --git a/packages/components/src/__tests__/form-write-error-message.test.tsx b/packages/components/src/__tests__/form-write-error-message.test.tsx index 53449eb6f2..a92ab0644f 100644 --- a/packages/components/src/__tests__/form-write-error-message.test.tsx +++ b/packages/components/src/__tests__/form-write-error-message.test.tsx @@ -16,15 +16,15 @@ * pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and * the record id to whoever hit it. */ -import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; const SCHEMA = { type: 'form', diff --git a/packages/components/src/__tests__/html-anchor-links.test.tsx b/packages/components/src/__tests__/html-anchor-links.test.tsx index 29192ee271..959075c6f6 100644 --- a/packages/components/src/__tests__/html-anchor-links.test.tsx +++ b/packages/components/src/__tests__/html-anchor-links.test.tsx @@ -16,15 +16,15 @@ * external / fragment / new-tab / modified-click navigation to the browser. */ -import { describe, it, expect, beforeAll, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, fireEvent, waitFor } from '@testing-library/react'; import { SchemaRenderer, ActionProvider } from '@object-ui/react'; import type { NavigationHandler } from '@object-ui/core'; import type { SchemaNode } from '@object-ui/types'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; function renderAnchor( schema: Record, diff --git a/packages/components/src/__tests__/html-page-lazy-blocks.test.tsx b/packages/components/src/__tests__/html-page-lazy-blocks.test.tsx index d6a6d365f5..b8d31f25d9 100644 --- a/packages/components/src/__tests__/html-page-lazy-blocks.test.tsx +++ b/packages/components/src/__tests__/html-page-lazy-blocks.test.tsx @@ -55,10 +55,11 @@ function renderHtmlPage(source: string) { ); } -beforeAll(async () => { - // Registers PageRenderer for `type:'home'`, which dispatches kind:'html'. - await import('../renderers'); -}, 30000); +// Registers PageRenderer for `type:'home'`, which dispatches kind:'html'. At +// module scope, NOT inside a `beforeAll` — there the cold transform is billed to +// `hookTimeout`, which is why this carried a raised timeout. See +// object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; afterEach(() => { ComponentRegistry.unregister('object-kanban', 'plugin-kanban'); diff --git a/packages/components/src/__tests__/page-tabs-visibility.test.tsx b/packages/components/src/__tests__/page-tabs-visibility.test.tsx index 26b257a689..5953007d76 100644 --- a/packages/components/src/__tests__/page-tabs-visibility.test.tsx +++ b/packages/components/src/__tests__/page-tabs-visibility.test.tsx @@ -20,14 +20,14 @@ * NOT honored on this new surface. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render, act, fireEvent } from '@testing-library/react'; import React from 'react'; import { SchemaRenderer, PageVariablesProvider, usePageVariables } from '@object-ui/react'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; /** Writer that flips a page variable on click — stands in for an interactive element. */ function Writer({ name, value }: { name: string; value: any }) { diff --git a/packages/components/src/__tests__/page-variables.test.tsx b/packages/components/src/__tests__/page-variables.test.tsx index 81350c0930..3baa90e3a3 100644 --- a/packages/components/src/__tests__/page-variables.test.tsx +++ b/packages/components/src/__tests__/page-variables.test.tsx @@ -13,7 +13,7 @@ * 3. element:record_picker reads its bound variable and queries its object. */ -import { describe, it, expect, beforeAll, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, renderHook, act, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; @@ -24,10 +24,10 @@ import { usePageVariableBinding, AdapterCtx, } from '@object-ui/react'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; // --------------------------------------------------------------------------- // usePageVariableBinding — resolve writer binding from a component id diff --git a/packages/components/src/__tests__/palette-placeholder-blocks.test.tsx b/packages/components/src/__tests__/palette-placeholder-blocks.test.tsx index 250586007a..00f8832f95 100644 --- a/packages/components/src/__tests__/palette-placeholder-blocks.test.tsx +++ b/packages/components/src/__tests__/palette-placeholder-blocks.test.tsx @@ -25,11 +25,12 @@ import '@testing-library/jest-dom'; import { ComponentRegistry } from '@object-ui/core'; import { PALETTE_PLACEHOLDER_BLOCKS } from '../renderers/placeholders'; -beforeAll(async () => { - // Import the barrel ONLY — never `registerPlaceholders()`, which is what - // this test exists to prove is not required for these blocks. - await import('../renderers'); -}, 30000); +// Import the barrel ONLY — never `registerPlaceholders()`, which is what this +// test exists to prove is not required for these blocks. At module scope, NOT +// inside a `beforeAll` — there the cold transform is billed to `hookTimeout`, +// which is why this carried a raised timeout. See +// object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; describe('palette-offered blocks render without the optional bootstrap', () => { it('declares a non-empty eager set', () => { diff --git a/packages/components/src/__tests__/react-page-adapter.test.tsx b/packages/components/src/__tests__/react-page-adapter.test.tsx index f2d7efc583..408910559a 100644 --- a/packages/components/src/__tests__/react-page-adapter.test.tsx +++ b/packages/components/src/__tests__/react-page-adapter.test.tsx @@ -31,6 +31,7 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer, AdapterCtx } from '@object-ui/react'; +import '../renderers'; /** The dataSource each render of the stand-in block was handed. */ const seen: unknown[] = []; @@ -50,13 +51,17 @@ function Page() { const SCHEMA = { type: 'home', kind: 'react', name: 'adapter_page', source: SOURCE }; -beforeAll(async () => { - await import('../renderers'); +// The barrel import moved to module scope (see the `import '../renderers'` +// above): inside the hook its cold transform was billed to `hookTimeout`, which +// is why this carried a raised timeout. The override below still has to run +// AFTER the barrel, and it does — static imports are evaluated before any hook. +// See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +beforeAll(() => { ComponentRegistry.register('list-view', (props: any) => { seen.push(props.schema?.dataSource); return
; }); -}, 30000); +}); afterAll(() => { ComponentRegistry.unregister('list-view'); diff --git a/packages/components/src/__tests__/react-page-scope.test.tsx b/packages/components/src/__tests__/react-page-scope.test.tsx index 5f31d77510..05e4200aaa 100644 --- a/packages/components/src/__tests__/react-page-scope.test.tsx +++ b/packages/components/src/__tests__/react-page-scope.test.tsx @@ -36,6 +36,8 @@ import { render, waitFor } from '@testing-library/react'; import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer, AdapterCtx } from '@object-ui/react'; +// Registers PageRenderer for `type:'home'`, which dispatches kind:'react'. +import '../renderers'; /** Props the stand-in blocks last received, for flat-prop assertions. */ const captured: { listView?: any; objectForm?: any; kanban?: any } = {}; @@ -50,10 +52,12 @@ function renderReactPage(source: string) { ); } -beforeAll(async () => { - // Registers PageRenderer for `type:'home'`, which dispatches kind:'react'. - await import('../renderers'); - +// The barrel import moved to module scope (see `import '../renderers'` above): +// inside the hook its cold transform was billed to `hookTimeout`, which is why +// this carried a raised timeout. The stand-ins below still have to run AFTER the +// barrel, and they do — static imports are evaluated before any hook. See +// object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +beforeAll(() => { // Stand-ins under the REAL curated tags. `getPublicConfigs()` walks // PUBLIC_BLOCKS and resolves each tag, so registering the bare tag is enough // to make it public — no `tier` flag needed. Neither tag is registered by diff --git a/packages/components/src/__tests__/react-page-state.test.tsx b/packages/components/src/__tests__/react-page-state.test.tsx index f1fa71cd02..cf6dabbb06 100644 --- a/packages/components/src/__tests__/react-page-state.test.tsx +++ b/packages/components/src/__tests__/react-page-state.test.tsx @@ -48,10 +48,11 @@ function Page() { /** Module-scope so the schema identity is stable across host re-renders. */ const PLAIN_SCHEMA = { type: 'home', kind: 'react', name: 'test_page', source: counterSource() }; -beforeAll(async () => { - // Registers PageRenderer for `type:'home'`, which dispatches kind:'react'. - await import('../renderers'); -}, 30000); +// Registers PageRenderer for `type:'home'`, which dispatches kind:'react'. At +// module scope, NOT inside a `beforeAll` — there the cold transform is billed to +// `hookTimeout`, which is why this carried a raised timeout. See +// object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; afterEach(() => { ComponentRegistry.unregister('object-kanban', 'plugin-kanban'); diff --git a/packages/components/src/__tests__/text-input.test.tsx b/packages/components/src/__tests__/text-input.test.tsx index 830bb9cc1c..c3f8a4902d 100644 --- a/packages/components/src/__tests__/text-input.test.tsx +++ b/packages/components/src/__tests__/text-input.test.tsx @@ -13,15 +13,15 @@ * 5. is safe to drop outside a Page (uncontrolled, never throws). */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render, fireEvent } from '@testing-library/react'; import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { SchemaRenderer, PageVariablesProvider, usePageVariables } from '@object-ui/react'; - -beforeAll(async () => { - await import('../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; // Reads a page variable back out so a test can assert what the input wrote. function VarProbe({ name }: { name: string }) { diff --git a/packages/components/src/__tests__/toaster-position-spec-parity.test.tsx b/packages/components/src/__tests__/toaster-position-spec-parity.test.tsx index b354b2a0f3..f15778a933 100644 --- a/packages/components/src/__tests__/toaster-position-spec-parity.test.tsx +++ b/packages/components/src/__tests__/toaster-position-spec-parity.test.tsx @@ -13,12 +13,16 @@ * `inputs: []` — all six `NotificationPositionSchema` values (and `limit`) * validated and were discarded. */ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import React from 'react'; import '@testing-library/jest-dom'; import { NotificationPositionSchema } from '@objectstack/spec/ui'; import { renderComponent } from './test-utils'; import { TOASTER_POSITIONS } from '../renderers/feedback/toaster'; +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../renderers'; // Sonner defers mounting its DOM container, so the behavior tests assert on // the props our registration hands it rather than sonner internals. @@ -34,10 +38,6 @@ vi.mock('../ui/sonner', () => ({ }), })); -beforeAll(async () => { - await import('../renderers'); -}, 30000); - describe('toaster covers the spec notification-position vocabulary', () => { const rawOptions = (NotificationPositionSchema as unknown as { options?: readonly string[] }).options; const specNames: string[] = Array.isArray(rawOptions) ? [...rawOptions] : []; diff --git a/packages/components/src/renderers/form/__tests__/form-cascading-select.test.tsx b/packages/components/src/renderers/form/__tests__/form-cascading-select.test.tsx index 1f35590f42..d815b28b9a 100644 --- a/packages/components/src/renderers/form/__tests__/form-cascading-select.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-cascading-select.test.tsx @@ -16,13 +16,13 @@ * field through a plain text `input` (Radix Select can't be driven by synthetic * DOM events) and assert the gate transition on the dependent select. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; function renderForm(fields: any[]) { const Form = ComponentRegistry.get('form')!; diff --git a/packages/components/src/renderers/form/__tests__/form-date-picker-click.test.tsx b/packages/components/src/renderers/form/__tests__/form-date-picker-click.test.tsx index 6ff442d037..bccd7dd3fe 100644 --- a/packages/components/src/renderers/form/__tests__/form-date-picker-click.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-date-picker-click.test.tsx @@ -15,13 +15,13 @@ * picker — matching how the other field widgets behave. Plain inputs must not * trigger a picker. */ -import { describe, it, expect, beforeAll, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; function renderForm(fields: any[]) { const Form = ComponentRegistry.get('form')!; diff --git a/packages/components/src/renderers/form/__tests__/form-defaults-change-keeps-user-input.test.tsx b/packages/components/src/renderers/form/__tests__/form-defaults-change-keeps-user-input.test.tsx index 4a52b08d10..a0c28bbe69 100644 --- a/packages/components/src/renderers/form/__tests__/form-defaults-change-keeps-user-input.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-defaults-change-keeps-user-input.test.tsx @@ -30,13 +30,13 @@ * has NEVER carried (absent from both the outgoing and the incoming defaults) * and that the user actually changed survives the reset. */ -import { describe, it, expect, beforeAll, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, fireEvent, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; const fields = [ { name: 'name', label: 'Name', type: 'input' }, diff --git a/packages/components/src/renderers/form/__tests__/form-defaults-reset-window.test.tsx b/packages/components/src/renderers/form/__tests__/form-defaults-reset-window.test.tsx index 2a5ca6cfc2..7c276e0c9c 100644 --- a/packages/components/src/renderers/form/__tests__/form-defaults-reset-window.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-defaults-reset-window.test.tsx @@ -32,14 +32,14 @@ * With the reset in a layout effect it has already run by then and the typed * value survives; revert it to `React.useEffect` and this test fails every run. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render, waitFor, fireEvent, act } from '@testing-library/react'; import React from 'react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; const FIELDS = [ { name: 'name', label: 'Name', type: 'input' }, diff --git a/packages/components/src/renderers/form/__tests__/form-dependent-values.test.tsx b/packages/components/src/renderers/form/__tests__/form-dependent-values.test.tsx index 0346107ad3..85750fe68c 100644 --- a/packages/components/src/renderers/form/__tests__/form-dependent-values.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-dependent-values.test.tsx @@ -20,6 +20,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; +import '../../../renderers'; // Probe widget standing in for a lookup field: surfaces the received // `dependentValues` so the test can assert on the injection. @@ -27,8 +28,12 @@ function DependentValuesProbe(props: any) { return
{JSON.stringify(props.dependentValues ?? null)}
; } -beforeAll(async () => { - await import('../../../renderers'); +// The barrel import moved to module scope (see `import '../../../renderers'` +// above): inside the hook its cold transform was billed to `hookTimeout`, which +// is why this carried a raised timeout. The overrides below still have to run +// AFTER the barrel, and they do — static imports are evaluated before any hook. +// See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +beforeAll(() => { // Override the protocol placeholder for `field:lookup` with the probe // (the fields package owns the real widget; components tests never load it). ComponentRegistry.register('field:lookup', DependentValuesProbe, { namespace: 'test' }); diff --git a/packages/components/src/renderers/form/__tests__/form-field-panes.test.tsx b/packages/components/src/renderers/form/__tests__/form-field-panes.test.tsx index 61bcff558a..133c48baa6 100644 --- a/packages/components/src/renderers/form/__tests__/form-field-panes.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-field-panes.test.tsx @@ -25,13 +25,13 @@ * comes out an even `flex-grow: 50`. They are verified in a real browser. */ -import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; beforeEach(() => { if (!(Element.prototype as any).scrollIntoView) { diff --git a/packages/components/src/renderers/form/__tests__/form-field-tabs.test.tsx b/packages/components/src/renderers/form/__tests__/form-field-tabs.test.tsx index bab137222a..50ec3f8c02 100644 --- a/packages/components/src/renderers/form/__tests__/form-field-tabs.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-field-tabs.test.tsx @@ -19,14 +19,14 @@ * - a field no tab claims is still rendered (never silently dropped). */ -import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { toast } from '../../../ui/sonner'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; beforeEach(() => { vi.spyOn(toast, 'error').mockImplementation(() => 'id' as any); diff --git a/packages/components/src/renderers/form/__tests__/form-invalid-scroll.test.tsx b/packages/components/src/renderers/form/__tests__/form-invalid-scroll.test.tsx index 6dff3aa1a3..a2a389187f 100644 --- a/packages/components/src/renderers/form/__tests__/form-invalid-scroll.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-invalid-scroll.test.tsx @@ -14,14 +14,14 @@ * master-detail), so on a long form the user clicked 创建 and saw nothing. */ -import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { toast } from '../../../ui/sonner'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; let toastErrorSpy: ReturnType; diff --git a/packages/components/src/renderers/form/__tests__/form-legacy-condition.test.tsx b/packages/components/src/renderers/form/__tests__/form-legacy-condition.test.tsx index 2b7a57f184..9866539a0a 100644 --- a/packages/components/src/renderers/form/__tests__/form-legacy-condition.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-legacy-condition.test.tsx @@ -15,13 +15,13 @@ * / `visibleOn`. This locks the translated semantics and reactivity. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; function renderForm(fields: any[]) { const Form = ComponentRegistry.get('form')!; diff --git a/packages/components/src/renderers/form/__tests__/form-select-value-survives-rules.test.tsx b/packages/components/src/renderers/form/__tests__/form-select-value-survives-rules.test.tsx index e5294836c4..a2b1aef1d5 100644 --- a/packages/components/src/renderers/form/__tests__/form-select-value-survives-rules.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-select-value-survives-rules.test.tsx @@ -22,13 +22,13 @@ * value except the user. A value the option list does not (yet) offer is left * alone rather than blanked. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; const CATEGORY_OPTIONS = [ { label: '00 Production', value: 'production' }, diff --git a/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx b/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx index 80a84e911b..9b8a93cf04 100644 --- a/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-server-field-errors.test.tsx @@ -20,14 +20,14 @@ * these two paths now share one implementation, so they must behave alike. */ -import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; import { toast } from '../../../ui/sonner'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; let toastErrorSpy: ReturnType; diff --git a/packages/components/src/renderers/form/__tests__/form-visibleon.test.tsx b/packages/components/src/renderers/form/__tests__/form-visibleon.test.tsx index bfbf01e554..391a34e737 100644 --- a/packages/components/src/renderers/form/__tests__/form-visibleon.test.tsx +++ b/packages/components/src/renderers/form/__tests__/form-visibleon.test.tsx @@ -17,13 +17,13 @@ * field config and dropped, so conditional fields always rendered. */ -import { describe, it, expect, beforeAll } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { ComponentRegistry } from '@object-ui/core'; - -beforeAll(async () => { - await import('../../../renderers'); -}, 30000); +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '../../../renderers'; function renderForm(fields: any[]) { const Form = ComponentRegistry.get('form')!; diff --git a/packages/plugin-dashboard/src/__tests__/DrillDownDrawer.chain.test.tsx b/packages/plugin-dashboard/src/__tests__/DrillDownDrawer.chain.test.tsx index 9ee8c5d366..cd276e1274 100644 --- a/packages/plugin-dashboard/src/__tests__/DrillDownDrawer.chain.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DrillDownDrawer.chain.test.tsx @@ -6,17 +6,16 @@ * record list it renders (for pivot / dataset / chart drill-through) is itself * drill-to-record, so clicking a row opens that record. */ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; -// Register the base data-table renderer (transitively via @object-ui/components). -beforeAll(async () => { - await import('@object-ui/components'); -}, 30000); - import { DrillDownDrawer } from '../DrillDownDrawer'; +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '@object-ui/components'; const records = [ { id: '1', name: 'Acme Renewal', amount: 1500 }, diff --git a/packages/plugin-dashboard/src/__tests__/DrillDownDrawer.escapeHatch.test.tsx b/packages/plugin-dashboard/src/__tests__/DrillDownDrawer.escapeHatch.test.tsx index 3d52c63111..b2e05a8a1d 100644 --- a/packages/plugin-dashboard/src/__tests__/DrillDownDrawer.escapeHatch.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/DrillDownDrawer.escapeHatch.test.tsx @@ -7,17 +7,17 @@ * provides a `DrillNavigationContext.openRecordList` handler, and stays a * self-contained peek when it doesn't. */ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; import { DrillNavigationProvider } from '@object-ui/react'; -beforeAll(async () => { - await import('@object-ui/components'); -}, 30000); - import { DrillDownDrawer } from '../DrillDownDrawer'; +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '@object-ui/components'; function renderDrawer( props: Record = {}, diff --git a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.drill.test.tsx b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.drill.test.tsx index b9693c31b0..96051ff889 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.drill.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.drill.test.tsx @@ -11,17 +11,16 @@ * `@object-ui/components`) so they exercise the actual row-click path, not a * mock. */ -import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent, within } from '@testing-library/react'; import '@testing-library/jest-dom'; import React from 'react'; -// Ensure the base data-table renderer is registered before any test renders. -beforeAll(async () => { - await import('@object-ui/components'); -}, 30000); - import { ObjectDataTable } from '../ObjectDataTable'; +// Registers the renderers at module scope, NOT inside a `beforeAll` — there the +// cold transform is billed to `hookTimeout`, which is why this carried a raised +// timeout. See object-ui/no-dynamic-import-in-test-hook (objectui#3010/#3021). +import '@object-ui/components'; const records = [ { id: '1', name: 'Acme Renewal', amount: 1500 },