Files
lobehub__lobehub/tests/setup.ts
Innei 3280844f5d ⚡️ perf(test): cut root suite from 397s to 287s, dedupe logger mocks, fix blind stream tests (#18804)
* ⚡️ perf(test): stop loading antd in the global vitest setup

setupFiles re-evaluate per test file, so `import { theme } from 'antd'` —
present only to set `theme.defaultConfig.hashed = false` — cost every one of
the 1836 root-suite files ~596ms of module evaluation.

Nothing asserts on unhashed antd class names, so the flag can go with it.

Full root suite: 396.95s -> 283.88s wall, setup 2027s -> 256s, no test changes.

Prebundling antd via `deps.optimizer` was measured as an alternative and
rejected: a prebundled module resolves to the optimizer chunk while
`vi.mock('<bare-specifier>')` registers on the bare id, so the mock silently
stops applying (23 suites started reading the real implementation).

Claude-Session: https://claude.ai/code/session_01K9fwqjBqMYMsYDddo9b4JN

* ✅ test(desktop,cli): lift per-file logger mocks into the setup files

52 desktop suites and 38 CLI suites each carried an identical
`vi.mock('…/utils/logger')` whose only job was silencing output — the desktop
logger also pulls electron-log and `@/env` in at module scope.

Register one stub per app instead:
- desktop `__mocks__/setup.ts` stubs `createLogger`
- new `apps/cli/tests/setup.ts` spreads the real module and replaces every `log`
  method with a spy, so suites that assert on log calls keep working

Suites needing different behavior still override with their own `vi.mock`;
`logger.test.ts` in each app opts out via `vi.unmock`.

desktop 122/122 pass. CLI unit failures go 9 -> 8 (all pre-existing;
`src/settings/index.test.ts` now passes).

Claude-Session: https://claude.ai/code/session_01K9fwqjBqMYMsYDddo9b4JN

* 🐛 fix(test): make model-runtime stream tests actually assert

Three suites drove a stream end to end and then asserted only that something
existed, so they stayed green against a broken implementation.

- zhipu `handleStream`: 8 cases built tool_call chunks, read the whole stream
  into `chunks`, then asserted `expect(chunks).toBeDefined()`. Driving
  `instance.chat()` with a mocked `create` yields `choices: []`, so the
  transform under test never ran. Rewritten to call
  `params.chatCompletion.handleStream` directly and assert the emitted SSE
  `event: tool_calls` indices — 365 -> 147 lines, and the
  all-placeholders branch is now covered.
- google `should handle text messages correctly`: the mock enqueued a bare
  string, which actually throws `Cannot use 'in' operator to search for
  'promptFeedback'` once the body is read. Now enqueues a real candidates
  chunk and asserts the SSE text payload.
- azureOpenai `should handle o1 series models without streaming` asserted only
  that `transformResponseToStream` ran, never that o1 forces `stream: false`.

Each was mutation-verified: deleting the negative-index fix, the placeholder
filter, the google thought filter, or the o1 stream override now fails these
tests and previously did not.

186 files / 4650 tests pass.

Claude-Session: https://claude.ai/code/session_01K9fwqjBqMYMsYDddo9b4JN
2026-08-29 17:39:09 +08:00

283 lines
8.0 KiB
TypeScript

import '@testing-library/jest-dom';
// mock indexedDB to test with dexie
// refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
import 'fake-indexeddb/auto';
import i18n from 'i18next';
import { enableMapSet, enablePatches } from 'immer';
import type { ButtonHTMLAttributes, ComponentType, ElementType, ReactNode } from 'react';
import React from 'react';
import { beforeEach, vi } from 'vitest';
import chat from '@/locales/default/chat';
import common from '@/locales/default/common';
import discover from '@/locales/default/discover';
import home from '@/locales/default/home';
import oauth from '@/locales/default/oauth';
class TestMemoryStorage implements Storage {
private readonly store = new Map<string, string>();
get length() {
return this.store.size;
}
clear() {
this.store.clear();
}
getItem(key: string) {
return this.store.get(key) ?? null;
}
key(index: number) {
return Array.from(this.store.keys())[index] ?? null;
}
removeItem(key: string) {
this.store.delete(key);
}
setItem(key: string, value: string) {
this.store.set(key, String(value));
}
}
const installTestStorage = () => {
const localStorage = new TestMemoryStorage();
const sessionStorage = new TestMemoryStorage();
Object.defineProperties(globalThis, {
Storage: { configurable: true, value: TestMemoryStorage, writable: true },
localStorage: { configurable: true, value: localStorage, writable: true },
sessionStorage: { configurable: true, value: sessionStorage, writable: true },
});
if (typeof globalThis.window !== 'undefined') {
Object.defineProperties(window, {
localStorage: { configurable: true, value: localStorage, writable: true },
sessionStorage: { configurable: true, value: sessionStorage, writable: true },
});
}
};
// Enable Immer MapSet plugin so store code using Map/Set in produce() works in tests
enablePatches();
enableMapSet();
// Global mock for @lobehub/analytics/react to avoid AnalyticsProvider dependency
// This prevents tests from failing when components use useAnalytics hook
vi.mock('@lobehub/analytics/react', () => ({
useAnalytics: () => ({
analytics: {
track: vi.fn(),
},
}),
}));
// Global mock for @/auth to avoid better-auth validator module issue in tests
// The validator package has ESM resolution issues in Vitest environment
vi.mock('@/auth', () => ({
auth: {
api: {
getSession: vi.fn().mockResolvedValue(null),
},
},
}));
// Route zustand store creation through __mocks__/zustand/traditional.ts so every
// store resets to its initial state between tests without per-file opt-in
vi.mock('zustand/traditional');
// Key-passthrough translations so component tests assert locale keys, not copy;
// defaultValue still wins to match real t() fallback semantics. Tests needing
// custom t/Trans behavior override with their own vi.mock
vi.mock('react-i18next', async (importOriginal) => ({
...(await importOriginal<object>()),
useTranslation: () => ({
i18n: { changeLanguage: vi.fn(), language: 'en-US' },
t: (key: string, defaultValueOrOptions?: unknown, maybeOptions?: unknown) => {
if (typeof defaultValueOrOptions === 'string') return defaultValueOrOptions;
const options =
(defaultValueOrOptions as { defaultValue?: unknown } | undefined) ??
(maybeOptions as { defaultValue?: unknown } | undefined);
return typeof options?.defaultValue === 'string' ? options.defaultValue : key;
},
}),
}));
type NativeButtonType = 'button' | 'submit' | 'reset';
type TestButtonIcon = ComponentType<{ size?: number }> | ReactNode;
interface TestBaseUIButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
block?: boolean;
color?: string;
danger?: boolean;
fill?: string;
ghost?: boolean;
htmlType?: NativeButtonType;
icon?: TestButtonIcon;
iconPosition?: 'end' | 'start';
iconProps?: Record<string, unknown>;
loading?: boolean;
shadow?: boolean;
size?: string;
type?: string;
variant?: string;
}
interface TestBaseUISwitchProps extends Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'defaultValue' | 'onChange' | 'onClick' | 'value'
> {
checked?: boolean;
checkedChildren?: ReactNode;
defaultChecked?: boolean;
defaultValue?: boolean;
loading?: boolean;
onChange?: (checked: boolean, event: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: (checked: boolean, event: React.MouseEvent<HTMLButtonElement>) => void;
size?: string;
unCheckedChildren?: ReactNode;
value?: boolean;
}
const getNativeButtonType = (type?: string): NativeButtonType =>
type === 'submit' || type === 'reset' ? type : 'button';
const renderTestButtonIcon = (icon?: TestButtonIcon) => {
if (!icon) return null;
if (React.isValidElement(icon)) return icon;
return typeof icon === 'function' || (typeof icon === 'object' && '$$typeof' in icon)
? React.createElement(icon as ElementType<{ size?: number }>, { size: 16 })
: icon;
};
const TestBaseUIButton = (props: TestBaseUIButtonProps) => {
const {
block: _block,
children,
color: _color,
danger: _danger,
disabled,
fill: _fill,
ghost: _ghost,
htmlType,
icon,
iconPosition = 'start',
iconProps: _iconProps,
loading,
shadow: _shadow,
size: _size,
type,
variant: _variant,
...buttonProps
} = props;
const renderedIcon = renderTestButtonIcon(icon);
const nativeType = htmlType ?? getNativeButtonType(type);
return React.createElement(
'button',
{
...buttonProps,
'aria-busy': loading || undefined,
'disabled': disabled || loading,
'type': nativeType,
},
iconPosition === 'end' ? children : renderedIcon,
iconPosition === 'end' ? renderedIcon : children,
);
};
const TestBaseUISwitch = (props: TestBaseUISwitchProps) => {
const {
checked,
checkedChildren,
defaultChecked,
defaultValue,
disabled,
loading,
onChange,
onClick,
size: _size,
unCheckedChildren,
value,
...buttonProps
} = props;
const [innerChecked, setInnerChecked] = React.useState(defaultValue ?? defaultChecked ?? false);
const currentChecked = value ?? checked ?? innerChecked;
return React.createElement(
'button',
{
...buttonProps,
'aria-busy': loading || undefined,
'aria-checked': currentChecked,
'disabled': disabled || loading,
'role': 'switch',
'type': 'button',
'onClick': (event: React.MouseEvent<HTMLButtonElement>) => {
const nextChecked = !currentChecked;
if (value === undefined && checked === undefined) {
setInnerChecked(nextChecked);
}
onChange?.(nextChecked, event);
onClick?.(nextChecked, event);
},
},
currentChecked ? checkedChildren : unCheckedChildren,
);
};
// base-ui Button requires the app-level motion provider. Unit tests exercise
// consuming components, so a native button keeps interaction behavior stable.
vi.mock('@lobehub/ui/base-ui', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>;
return {
...actual,
Button: TestBaseUIButton,
Switch: TestBaseUISwitch,
};
});
// node runtime
if (typeof globalThis.window === 'undefined') {
// test with polyfill crypto
const { Crypto } = await import('@peculiar/webcrypto');
Object.defineProperty(globalThis, 'crypto', {
value: new Crypto(),
writable: true,
});
}
installTestStorage();
beforeEach(installTestStorage);
// init i18n for non-React modules (stores/utils) using i18next.t(...)
// Use in-memory resources to avoid interfering with Vitest module mocking.
await i18n.init({
defaultNS: 'common',
fallbackLng: 'zh-CN',
interpolation: { escapeValue: false },
lng: 'zh-CN',
ns: ['common', 'chat', 'discover', 'home', 'oauth'],
resources: {
'zh-CN': {
chat,
common,
discover,
home,
oauth,
},
},
});
// Set React as a global variable so it doesn't need to be imported in each test file
(globalThis as any).React = React;