From b0e721ea728ea8ae085bbec369f0422ba890f9c5 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:26:19 +0800 Subject: [PATCH] feat(eslint): ban dynamic imports in test hooks, and convert the last 33 sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prose rule added in #3015 did not hold on its own, and there is hard evidence for that: sweeping for `beforeAll(async () => { await import(…) })` found 37 files, and EVERY one already carried a raised timeout, on an escalating ladder — 15s -> 30s -> 60s. One even left the escalation as advice (`// Increase timeout to 30 seconds for heavy renderer imports`), and plugin-kanban blew its raised 15s anyway at 15021ms (#3010). A convention that gets re-broken 37 times is a lint rule. Adds `object-ui/no-dynamic-import-in-test-hook` (error, scoped to test files): a module loaded inside `beforeAll`/`beforeEach` bills its cold Vite transform to `hookTimeout`, so the test passes or fails on machine load rather than on the code it covers. Imported at module scope the same cost lands in the import phase, which no test or hook timeout applies to. Kept deliberately narrow, with both exemptions pinned in the RuleTester because both are legitimate and both are real code here: - a lazy FACTORY — `registerLazy('x', () => import('./x'))` — the hook installs a loader, it never runs the import. The hook frame resets inside a nested function, so this never reports. (Without this the rule would flag the console's own plugin registration and be unusable.) - a deliberate re-import against per-run module state (`vi.resetModules`, `doMock`, `doUnmock`, `stubEnv`, `stubGlobal`) — there the re-import is the point, and hoisting it would break the test rather than speed it up. No autofixer on purpose: the mechanical case is 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. Because an `error` ratchet has to lint clean today (the convention the other three rules follow), this also converts the 33 remaining sites — 30 in packages/components, 3 in plugin-dashboard. 27 were the canonical pure warm-up. The other 6 also register overrides that must run AFTER the barrel, so only the import moved out; those keep a (now sync, now untimed) hook, and the ordering still holds because static imports are evaluated before any hook. The side-effect import goes after the last existing import — the faithful equivalent of a hook that ran once every static import had been evaluated. Verified: - rule: 19 RuleTester cases (12 valid / 7 invalid); a fresh violation fails lint as an error with the exact fix in the message. - repo sweep: 33 violations -> 0. The only remaining error-level lint findings are 3 pre-existing ones under `e2e/`, which CI's per-package `turbo run lint` does not cover; untouched here. - tests: the 33 converted files pass (209 tests), and the full suite is 100% green — 733 files passed / 1 skipped, 8552 tests passed / 24 skipped. - the suite's cumulative timed portion dropped 243s -> 127s. Wall clock is unchanged (~229s): the work did not disappear, it moved out of timed windows into the import phase, which is the point. AGENTS.md §9 now points at the rule instead of only describing the convention, including both exemptions so the next agent does not fight the linter. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 2 +- eslint-rules/index.js | 2 + .../no-dynamic-import-in-test-hook.js | 143 ++++++++++++++++++ .../no-dynamic-import-in-test-hook.test.js | 94 ++++++++++++ eslint.config.js | 14 ++ .../src/__tests__/basic-renderers.test.tsx | 11 +- .../src/__tests__/data-list.test.tsx | 10 +- .../__tests__/data-table-inline-edit.test.tsx | 10 +- .../data-table-manual-pagination.test.tsx | 10 +- .../__tests__/data-table-row-click.test.tsx | 10 +- .../data-table-selection-mode.test.tsx | 10 +- .../src/__tests__/form-renderers.test.tsx | 11 +- .../form-write-error-message.test.tsx | 10 +- .../src/__tests__/html-anchor-links.test.tsx | 10 +- .../__tests__/html-page-lazy-blocks.test.tsx | 9 +- .../__tests__/page-tabs-visibility.test.tsx | 10 +- .../src/__tests__/page-variables.test.tsx | 10 +- .../palette-placeholder-blocks.test.tsx | 11 +- .../src/__tests__/react-page-adapter.test.tsx | 11 +- .../src/__tests__/react-page-scope.test.tsx | 12 +- .../src/__tests__/react-page-state.test.tsx | 9 +- .../src/__tests__/text-input.test.tsx | 10 +- .../toaster-position-spec-parity.test.tsx | 10 +- .../__tests__/form-cascading-select.test.tsx | 10 +- .../__tests__/form-date-picker-click.test.tsx | 10 +- ...-defaults-change-keeps-user-input.test.tsx | 10 +- .../form-defaults-reset-window.test.tsx | 10 +- .../__tests__/form-dependent-values.test.tsx | 9 +- .../form/__tests__/form-field-panes.test.tsx | 10 +- .../form/__tests__/form-field-tabs.test.tsx | 10 +- .../__tests__/form-invalid-scroll.test.tsx | 10 +- .../__tests__/form-legacy-condition.test.tsx | 10 +- .../form-select-value-survives-rules.test.tsx | 10 +- .../form-server-field-errors.test.tsx | 10 +- .../form/__tests__/form-visibleon.test.tsx | 10 +- .../__tests__/DrillDownDrawer.chain.test.tsx | 11 +- .../DrillDownDrawer.escapeHatch.test.tsx | 10 +- .../__tests__/ObjectDataTable.drill.test.tsx | 11 +- 38 files changed, 428 insertions(+), 162 deletions(-) create mode 100644 eslint-rules/no-dynamic-import-in-test-hook.js create mode 100644 eslint-rules/no-dynamic-import-in-test-hook.test.js 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 },