Langflow 前端异步测试模式:Jest 与 React Testing Library 的实战指南
Langflow 前端异步测试模式Jest 与 React Testing Library 的实战指南【免费下载链接】langflowLangflow is a powerful tool for building and deploying AI-powered agents and workflows.项目地址: https://gitcode.com/GitHub_Trending/la/langflow本文基于 Langflow 前端测试技能文档references/async-testing.md系统讲解在 Langflow 前端Jest 30 React Testing Library 16 jsdom 技术栈中测试异步行为的完整模式waitFor的轮询机制与选项、waitForElementToBeRemoved、findBy*查询、Fake Timers 及其与异步操作的组合、Promise 拒绝、防抖函数、事件流/模拟 WebSocket、React Query mutations 以及受控加载态。读完后你将能够独立编写确定性、无 flaky 的异步组件测试并理解 Langflow 仓库中这些模式的实际落地方式。测试基础设施这些模式运行在什么环境上理解异步测试模式之前先确认 Langflow 前端测试环境的实际构成因为waitFor的默认行为、Fake Timers 的可用性以及全局 mock 都依赖这一套配置技术版本以 package.json 为准用途Jest^30.0.3测试运行器与断言框架ts-jest^29.4.0TypeScript 转换testing-library/react^16.3.1组件渲染与 DOM 查询testing-library/user-event^14.5.2真实用户交互模拟testing-library/jest-dom^6.9.1扩展 DOM 匹配器toBeInTheDocument等jest-environment-jsdom^30.0.2浏览器环境模拟tanstack/react-query^5.49.2服务端状态管理关键配置位于 jest.config.jspreset: ts-jest、testEnvironment: jsdom覆盖率由coverageProvider: v8提供testMatch匹配src/**/__tests__/**/*.{test,spec}.{ts,tsx}与src/**/*.{test,spec}.{ts,tsx}即测试文件放在__tests__目录或源码同目录setupFiles: [rootDir/jest.setup.js]负责全局 mockreact-i18next、localStorage、sessionStorage、crypto、import.meta.env等setupFilesAfterEach阶段的 setupTests.ts 则引入testing-library/jest-dom匹配器并 mock 了ResizeObserver、IntersectionObserver、window.matchMedia等 jsdom 缺失的浏览器 API——很多异步组件懒加载、滚动监听在 jsdom 下不 mock 这些就会直接崩溃moduleNameMapper将/别名映射到rootDir/src/样式文件统一映射到空 mock。常用命令在src/frontend目录下执行npm test # 运行全部测试 npm test -- path/to/file.test.tsx # 运行单个测试文件 npm test -- --testPathPatternalertStore # 按模式匹配运行 npm run test:watch # watch 模式 npm run test:coverage # 带覆盖率运行需要牢记的约束Langflow 测试栈只用 Jest APIjest.fn()、jest.mock()、jest.spyOn()、jest.mocked()禁止使用 Vitest 的vi.*等价物用户交互一律用testing-library/user-event而非fireEvent涉及 React 状态更新时必须用act()包裹。waitFor等待异步操作完成的核心工具waitFor用于在断言前等待异步操作完成——它会对回调进行轮询直到断言通过或超时。这是所有异步断言的基石import { render, screen, waitFor } from testing-library/react; it(should load and display data, async () { jest.mocked(api.get).mockResolvedValueOnce({ data: { items: [{ id: 1, name: Test Item }] }, }); render(ItemList /); // Wait for the async data to appear await waitFor(() { expect(screen.getByText(Test Item)).toBeInTheDocument(); }); });waitFor 选项waitFor的第二个参数可以控制轮询行为await waitFor( () { expect(screen.getByText(loaded)).toBeInTheDocument(); }, { timeout: 3000, // Max time to wait (default: 1000ms) interval: 50, // Polling interval (default: 50ms) }, );timeout最大等待时间默认 1000ms。对于本仓库这类走 axios 网络层或 React Query 缓存的组件若 mock 的 resolve 较慢或组件有防抖逻辑可按需调大interval轮询间隔默认 50ms。配合 Fake Timers 时该值决定了需要推进多少虚拟时间才能命中一次轮询。waitFor 最佳实践waitFor内只放一个断言。多个断言组合会导致误导性的失败若第二条断言失败无法判断第一条是否曾经为真。waitFor用于“元素出现”而非“元素消失”。等待消失应使用waitForElementToBeRemoved。同步场景不要用waitFor——它只会引入不必要的轮询延迟。元素在render后已存在时直接用getBy*断言即可。正反例对比// GOOD: Single assertion in waitFor await waitFor(() { expect(screen.getByText(Data loaded)).toBeInTheDocument(); }); // BAD: Multiple assertions -- if second fails, first may have been true await waitFor(() { expect(screen.getByText(Data loaded)).toBeInTheDocument(); expect(screen.getByText(5 items)).toBeInTheDocument(); // unreliable }); // GOOD: Chain waitFor calls await waitFor(() { expect(screen.getByText(Data loaded)).toBeInTheDocument(); }); expect(screen.getByText(5 items)).toBeInTheDocument();waitForElementToBeRemoved等待元素消失加载指示器、临时 toast、下拉弹层等“先出现后消失”的 UI用waitForElementToBeRemoved断言其消失时刻it(should hide loading spinner after data loads, async () { jest.mocked(api.get).mockResolvedValueOnce({ data: [] }); render(ItemList /); // Spinner appears immediately expect(screen.getByTestId(loading-spinner)).toBeInTheDocument(); // Wait for it to disappear await waitForElementToBeRemoved(() screen.queryByTestId(loading-spinner), ); // Now assert the loaded state expect(screen.getByText(No items found)).toBeInTheDocument(); });注意这里查询回调使用的是queryByTestId而非getByTestId元素消失后getBy*会抛错而waitForElementToBeRemoved依赖查询返回null来判断“已移除”。findBy 查询waitFor getBy 的语法糖findBy*系列查询返回一个 Promise在元素出现时 resolve等价于waitFor(() getBy*(...))it(should display async content, async () { jest.mocked(api.get).mockResolvedValueOnce({ data: { name: Test } }); render(AsyncComponent /); // findByText waitFor(() getByText(...)) const element await screen.findByText(Test); expect(element).toBeInTheDocument(); });需要自定义超时时把选项作为第三个参数传入const element await screen.findByText(Slow content, {}, { timeout: 5000 });Langflow 仓库中findBy*是实际在用的写法例如 ModelInputComponent.test.tsx、ChunksMetadataFilter.test.tsx 等测试均依赖它断言异步加载的模型列表与过滤结果这与文档中的模式完全一致。Fake Timers可控时间的定时器测试当组件使用setTimeout、setInterval或Date.now时用 Fake Timers 让时间完全受测试控制。文档给出的标准结构如下describe(TimerComponent, () { beforeEach(() { jest.useFakeTimers(); }); afterEach(() { jest.runOnlyPendingTimers(); jest.useRealTimers(); }); it(should update after interval, () { render(TimerComponent interval{1000} /); expect(screen.getByText(0 seconds)).toBeInTheDocument(); act(() { jest.advanceTimersByTime(3000); }); expect(screen.getByText(3 seconds)).toBeInTheDocument(); }); it(should clean up interval on unmount, () { const clearIntervalSpy jest.spyOn(global, clearInterval); const { unmount } render(TimerComponent interval{1000} /); unmount(); expect(clearIntervalSpy).toHaveBeenCalled(); clearIntervalSpy.mockRestore(); }); });这个“beforeEach开启 fake timers /afterEach先执行jest.runOnlyPendingTimers()再jest.useRealTimers()”的模板在 Langflow 的真实测试中反复出现。一个几乎逐字对应的实例是 use-debounce.test.ts它测试防抖 hook 时使用了同一套清理结构并通过多次调用 jest.advanceTimersByTime(500)验证“连续触发只回调一次且携带最后一次参数”的防抖语义beforeEach(() { jest.clearAllMocks(); jest.useFakeTimers(); }); afterEach(() { jest.runOnlyPendingTimers(); jest.useRealTimers(); });组合 Fake Timers 与异步操作当组件同时使用定时器和异步操作如轮询 API时需要交替使用“推进虚拟时间”与“等待 Promise 落定”两个手段it(should poll for updates, async () { jest.useFakeTimers(); const mockGet jest.mocked(api.get); mockGet .mockResolvedValueOnce({ data: { status: pending } }) .mockResolvedValueOnce({ data: { status: complete } }); render(PollingComponent /); // First fetch happens immediately await waitFor(() { expect(screen.getByText(pending)).toBeInTheDocument(); }); // Advance past the polling interval act(() { jest.advanceTimersByTime(5000); }); // Second fetch resolves await waitFor(() { expect(screen.getByText(complete)).toBeInTheDocument(); }); jest.useRealTimers(); });这个“定时器触发 waitFor断言结果”的交替模式正是 Langflow 知识库轮询 hook 测试的核心结构。useKnowledgeBasePolling.test.ts 测试了知识底座在“ingesting”状态下的 6 秒轮询行为beforeEach中jest.useFakeTimers()用例内通过await act(async () { jest.advanceTimersByTime(6000); })推进一个轮询周期随后断言api.get是否被调用未处于轮询状态时expect(mockApiGet).not.toHaveBeenCalled()。该文件同时展示了jest.mock(/controllers/API/api, ...)模块 mock 与renderHookQueryClientProviderwrapper 的组合是上述模式的完整落地范例。Mock Date.now对于用Date.now()计算经过时间的组件手动替换Date.now再配合 fake timers 推进间隔回调it(should track elapsed time, async () { const realDateNow Date.now; let mockTime 1000000; Date.now jest.fn(() mockTime); jest.useFakeTimers(); render(ElapsedTimer /); // Advance mock time by 2 seconds mockTime 2000; act(() { jest.advanceTimersByTime(100); // Trigger interval callback }); await waitFor(() { expect(screen.getByText(2.0s)).toBeInTheDocument(); }); Date.now realDateNow; jest.useRealTimers(); });要点修改mockTime只是让组件下一次读取Date.now时得到新值真正让组件“感知”时间变化、触发重渲染的是act内推进的定时器。用完必须恢复Date.now并切回 real timers否则会污染同文件后续用例。测试 Promise 拒绝API 失败路径mockRejectedValueOnce让 mock 精确地拒绝一次请求配合waitFor断言错误 UIit(should display error message on API failure, async () { jest.mocked(api.get).mockRejectedValueOnce(new Error(Server error)); render(DataComponent /); await waitFor(() { expect(screen.getByText(/server error/i)).toBeInTheDocument(); }); });这也是文档中“对抗性测试”原则不只测 happy path在异步场景的直接体现网络 5xx、401/403/404、超时等错误分支都应按此模式覆盖。测试防抖函数搜索输入、自动保存等防抖场景必须组合 fake timers 与userEvent且要在userEvent.setup时传入advanceTimersit(should debounce search input, async () { jest.useFakeTimers(); const user userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); render(SearchInput onSearch{mockSearch} debounceMs{300} /); const input screen.getByRole(textbox); await user.type(input, hello); // Should not have called search yet expect(mockSearch).not.toHaveBeenCalled(); // Advance past debounce delay act(() { jest.advanceTimersByTime(300); }); expect(mockSearch).toHaveBeenCalledWith(hello); jest.useRealTimers(); });重要在 fake timers 下使用userEvent时必须传入advanceTimers: jest.advanceTimersByTime这样 userEvent 才能在其内部延迟按键间隔、click 的 pointer 阶段中推进虚拟时间否则await user.type(...)会永远等待真实时间而卡死或超时。仓库中 use-debounce.test.ts 展示了 hook 层的防抖测试写法先 mocklodash.debounce保留真实的setTimeout语义以便 fake timers 接管再用“快速多次调用 → 未到延迟断言未触发 →advanceTimersByTime后断言仅触发一次”验证防抖边界包括“rapid calls”场景下回调只携带最后一个参数toHaveBeenCalledWith(9)。测试事件流与 WebSocketLangflow 前端的对话、流式输出等场景依赖事件源。测试组件消费 SSE/WebSocket 事件时用jest.spyOn替换全局EventSource为受控 mock再手动触发监听器it(should handle streaming messages, async () { const mockEventSource { addEventListener: jest.fn(), removeEventListener: jest.fn(), close: jest.fn(), }; jest.spyOn(global, EventSource as any).mockImplementation( () mockEventSource, ); render(StreamingChat /); // Simulate incoming message const onMessage mockEventSource.addEventListener.mock.calls.find( ([event]: [string]) event message, )?.[1]; act(() { onMessage?.({ data: JSON.stringify({ text: Hello from stream }) }); }); expect(screen.getByText(Hello from stream)).toBeInTheDocument(); });核心手法从addEventListener.mock.calls中按事件名找回message监听器然后在act中调用它模拟一条入站消息——act包裹是必要的因为监听器内部会触发 React 状态更新。测试 React Query MutationsLangflow 大量使用tanstack/react-query管理服务端状态mutation 测试模式为包上QueryClientProvider→ 用userEvent触发表单提交 →waitFor等待成功 UI → 断言 API 调用参数it(should save and show success, async () { const user userEvent.setup(); jest.mocked(api.post).mockResolvedValueOnce({ data: { id: new-1 } }); render( QueryClientProvider client{createTestQueryClient()} CreateForm / /QueryClientProvider, ); await user.type(screen.getByLabelText(Name), New Item); await user.click(screen.getByRole(button, { name: /save/i })); await waitFor(() { expect(screen.getByText(/saved successfully/i)).toBeInTheDocument(); }); expect(api.post).toHaveBeenCalledWith(/api/v1/items, { name: New Item, }); });其中createTestQueryClient()的推荐做法是构造一个关闭重试的客户端避免失败用例触发多次请求干扰断言——仓库内的 useKnowledgeBasePolling.test.ts 就采用new QueryClient({ defaultOptions: { queries: { retry: false } } })这一标准配置。最后一步的toHaveBeenCalledWith断言把“UI 显示成功”与“发出了正确请求”绑定在一起这正是黑盒测试原则按用户可见行为断言同时验证对外副作用的体现。测试加载态手动控制的 Promise要精确断言“加载中 → 加载完成”的过渡例如确认 spinner 与内容不重叠可以用一个手动 resolve 的 Promise 掌控异步边界it(should show loading then content, async () { let resolvePromise: (value: any) void; const promise new Promise((resolve) { resolvePromise resolve; }); jest.mocked(api.get).mockReturnValueOnce(promise as any); render(DataDisplay /); // Loading state expect(screen.getByTestId(loading-spinner)).toBeInTheDocument(); expect(screen.queryByText(Data content)).not.toBeInTheDocument(); // Resolve the promise await act(async () { resolvePromise!({ data: { content: Data content } }); }); // Loaded state expect(screen.queryByTestId(loading-spinner)).not.toBeInTheDocument(); expect(screen.getByText(Data content)).toBeInTheDocument(); });关键在于resolvePromise由测试握有控制权先断言加载态spinner 存在、内容不存在再在await act(async () {...})中 resolve让 React 完整处理状态更新与重渲染最后断言加载完成态。常见陷阱Common Pitfalls文档归纳的五类高频错误逐条对应前面各节的要点忘记act()包裹定时器推进当jest.advanceTimersByTime()会触发 React 状态更新时务必包在act()中否则会出现 “An update to X inside a test was not wrapped in act(...)” 警告且断言可能读到过期 DOM。不清理定时器afterEach中先jest.runOnlyPendingTimers()再jest.useRealTimers()防止定时器泄漏到下一个用例use-debounce.test.ts 即此结构。对同步断言使用waitFor元素在render后已存在于 DOM 时直接用getBy*waitFor的轮询只会无谓拖慢测试。漏掉await忘记awaitwaitFor、findBy*或userEvent方法测试会“空洞地通过”——断言实际从未执行。这是文档中所列“说谎测试The Liar”反模式的典型来源。Fake timers 卡住 Promise若 fake timers 下 Promise 迟迟不 resolve尝试在act(async () { ... })内调用jest.advanceTimersByTime()让宏/微任务队列有机会在受控时间内跑完。小结在 Langflow 前端落地这些模式将本文模式放进 Langflow 的测试工程语境完整工作流是测试文件按src/**/__tests__/**/*.test.tsx约定放置见 jest.config.js 的testMatch编写前检查 jest.setup.js 已全局 mock 的模块react-i18next、darkStore、react-markdown、radix-ui/react-form、存储与crypto等不要重复 mockAPI 层统一 mock/controllers/API/apijest.mock模块工厂 mockResolvedValueOnce/mockRejectedValueOnce控制单次行为出现类异步用waitFor/findBy*消失类异步用waitForElementToBeRemoved时间类异步用 fake timers 并在act中推进运行npm test或npm run test:coverage验证CI 下 jest.config.js 会自动接入jest-junit报告。所有模式均可在技能目录 .agents/skills/frontend-testing/ 中对照阅读references/mocking.md覆盖 mock 策略references/common-patterns.md覆盖查询优先级与表单/弹层模式references/checklist.md提供提交前核对清单与本文的异步测试篇互为补充。【免费下载链接】langflowLangflow is a powerful tool for building and deploying AI-powered agents and workflows.项目地址: https://gitcode.com/GitHub_Trending/la/langflow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
