Browse Source

feat: add AI Assistant chatbot page (#11681)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
pull/11684/head
afc163 1 month ago
committed by GitHub
parent
commit
a7605030b2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .gitignore
  2. 15
      cloudflare-worker/src/routes/list.ts
  3. 6
      config/routes.ts
  4. 5
      jest.config.ts
  5. 5
      package.json
  6. 23020
      pnpm-lock.yaml
  7. 1
      src/locales/en-US/menu.ts
  8. 1
      src/locales/zh-CN/menu.ts
  9. 4
      src/pages/account/center/components/AvatarList/index.tsx
  10. 12
      src/pages/chatbot/data.d.ts
  11. 326
      src/pages/chatbot/index.tsx
  12. 18
      src/pages/chatbot/service.ts
  13. 75
      src/pages/chatbot/style.ts
  14. 6
      src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx
  15. 4
      src/pages/dashboard/analysis/components/NumberInfo/index.tsx
  16. 4
      src/pages/dashboard/analysis/components/Trend/index.tsx
  17. 4
      src/pages/list/search/applications/components/StandardFormRow/index.tsx
  18. 4
      src/pages/list/search/applications/components/TagSelect/index.tsx
  19. 4
      src/pages/list/search/articles/components/StandardFormRow/index.tsx
  20. 4
      src/pages/list/search/articles/components/TagSelect/index.tsx
  21. 4
      src/pages/list/search/projects/components/AvatarList/index.tsx
  22. 4
      src/pages/list/search/projects/components/StandardFormRow/index.tsx
  23. 4
      src/pages/list/search/projects/components/TagSelect/index.tsx
  24. 4
      src/pages/profile/advanced/index.tsx
  25. 4
      src/pages/user/login/__snapshots__/login.test.tsx.snap
  26. 18
      src/pages/user/login/login.test.tsx
  27. 1
      tests/__mocks__/mermaid.js

1
.gitignore

@ -48,3 +48,4 @@ build
# cloudflare wrangler
.wrangler
requestRecord.mock.js

15
cloudflare-worker/src/routes/list.ts

@ -1,5 +1,5 @@
import { Hono } from 'hono';
import { fakeList, defaultUser } from '../data/common';
import { defaultUser, fakeList } from '../data/common';
// Count bounds to prevent memory exhaustion
const COUNT_MIN = 1;
@ -61,6 +61,17 @@ app.post('/post_fake_list', async (c) => {
});
});
// GET /api/fake_list
app.get('/fake_list', (c) => {
const count = parseCount(c.req.query('count'), DEFAULT_COUNT);
const result = fakeList(count);
return c.json({
data: {
list: result,
},
});
});
// GET /api/card_fake_list
app.get('/card_fake_list', (c) => {
const count = parseCount(c.req.query('count'), DEFAULT_COUNT);
@ -108,4 +119,4 @@ app.get('/currentUserDetail', (c) => {
});
});
export default app;
export default app;

6
config/routes.ts

@ -280,6 +280,12 @@ export default [
},
],
},
{
path: '/chatbot',
name: 'chatbot',
icon: 'robot',
component: './chatbot',
},
{
path: '/',
redirect: '/dashboard/analysis',

5
jest.config.ts

@ -8,6 +8,11 @@ export default async (): Promise<any> => {
});
return {
...config,
testPathIgnorePatterns: ['/node_modules/', '/.worktrees/'],
moduleNameMapper: {
...(config.moduleNameMapper || {}),
'^mermaid$': '<rootDir>/tests/__mocks__/mermaid.js',
},
testEnvironmentOptions: {
...(config?.testEnvironmentOptions || {}),
url: 'http://localhost:8000',

5
package.json

@ -40,6 +40,9 @@
"@ant-design/icons": "^6.1.0",
"@ant-design/plots": "^2.6.0",
"@ant-design/pro-components": "^3.1.12-0",
"@ant-design/x": "^2.5.0",
"@ant-design/x-markdown": "^2.5.0",
"@ant-design/x-sdk": "^2.5.0",
"@antv/l7": "^2.22.7",
"@antv/l7-react": "^2.4.3",
"@rc-component/util": "^1.9.0",
@ -57,6 +60,7 @@
"@biomejs/biome": "^2.1.1",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@tailwindcss/postcss": "^4.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"@types/express": "^5.0.6",
@ -78,7 +82,6 @@
"jest-environment-jsdom": "^30.0.5",
"lint-staged": "^16.1.2",
"mockjs": "^1.1.0",
"@tailwindcss/postcss": "^4.0.0",
"tailwindcss": "^4.0.0",
"ts-node": "^10.9.2",
"typescript": "^6.0.2",

23020
pnpm-lock.yaml

File diff suppressed because it is too large

1
src/locales/en-US/menu.ts

@ -49,4 +49,5 @@ export default {
'menu.editor.flow': 'Flow Editor',
'menu.editor.mind': 'Mind Editor',
'menu.editor.koni': 'Koni Editor',
'menu.chatbot': 'AI Assistant',
};

1
src/locales/zh-CN/menu.ts

@ -49,4 +49,5 @@ export default {
'menu.editor.flow': '流程编辑器',
'menu.editor.mind': '脑图编辑器',
'menu.editor.koni': '拓扑编辑器',
'menu.chatbot': 'AI 助手',
};

4
src/pages/account/center/components/AvatarList/index.tsx

@ -1,5 +1,5 @@
import { Avatar, Tooltip } from 'antd';
import classNames from 'classnames';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
export declare type SizeType = number | 'small' | 'default' | 'large';
@ -22,7 +22,7 @@ export type AvatarListProps = {
};
const avatarSizeToClassName = (styles: any, size?: SizeType | 'mini') =>
classNames(styles.avatarItem, {
clsx(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',

12
src/pages/chatbot/data.d.ts

@ -0,0 +1,12 @@
// src/pages/chatbot/data.d.ts
export interface ConversationItem {
key: string;
label: string;
group?: string;
isDraft?: boolean;
}
export type ParsedMessage =
| { role: 'user'; content: string }
| { role: 'assistant'; content: string; thinkContent?: string };

326
src/pages/chatbot/index.tsx

@ -0,0 +1,326 @@
import { UserOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { Bubble, Conversations, Sender, Think, XProvider } from '@ant-design/x';
import type {
BubbleItemType,
BubbleListProps,
} from '@ant-design/x/es/bubble/interface';
import XMarkdown from '@ant-design/x-markdown';
import { useXChat } from '@ant-design/x-sdk';
import { Avatar, Card } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import type { ConversationItem, ParsedMessage } from './data';
import { createChatProvider } from './service';
import { useStyles } from './style';
const WELCOME_TEXT = '🤖 你好,有什么可以帮你?';
const TypewriterTitle: React.FC = () => {
const { styles } = useStyles();
const [index, setIndex] = useState(0);
const done = index >= WELCOME_TEXT.length;
useEffect(() => {
const timer = setInterval(() => {
setIndex((i) => {
if (i >= WELCOME_TEXT.length) {
clearInterval(timer);
return i;
}
return i + 1;
});
}, 80);
return () => clearInterval(timer);
}, []);
return (
<>
{WELCOME_TEXT.slice(0, index)}
{!done && <span className={styles.cursor}>|</span>}
</>
);
};
const parser = (message: { content: string; role: string }): ParsedMessage => {
const { content, role } = message;
if (role !== 'assistant') return { role: 'user', content };
const trimmed = content.trimStart();
const fullMatch = trimmed.match(/^<think>([\s\S]*?)<\/think>([\s\S]*)$/);
if (fullMatch) {
return {
role: 'assistant',
thinkContent: fullMatch[1],
content: fullMatch[2].trimStart(),
};
}
const partialMatch = trimmed.match(/^<think>([\s\S]*)$/);
if (partialMatch) {
return { role: 'assistant', thinkContent: partialMatch[1], content: '' };
}
return { role: 'assistant', content };
};
const STREAMING_ACTIVE = { hasNextChunk: true, enableAnimation: true };
const STREAMING_IDLE = { hasNextChunk: false, enableAnimation: true };
const roleConfig: BubbleListProps['role'] = {
user: {
placement: 'end',
avatar: <Avatar icon={<UserOutlined />} />,
},
ai: {
placement: 'start',
avatar: (
<Avatar
style={{
background: 'transparent',
fontSize: 22,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
🤖
</Avatar>
),
typing: { effect: 'typing', step: 2, interval: 20 },
contentRender: (
content: string,
info: { status?: string; loading?: boolean },
) => {
if (info?.loading || !content) return undefined;
return (
<XMarkdown
streaming={
info?.status === 'updating' ? STREAMING_ACTIVE : STREAMING_IDLE
}
>
{content}
</XMarkdown>
);
},
},
};
const ChatbotPage: React.FC = () => {
const { styles } = useStyles();
const [conversations, setConversations] = useState<ConversationItem[]>([
{ key: 'default', label: '💬 新对话', group: '今天', isDraft: true },
{
key: 'preset-1',
label: '🧩 Ant Design 的 Form 表单如何做联动校验?',
group: '今天',
},
{
key: 'preset-2',
label: '📋 ProTable 如何自定义工具栏按钮?',
group: '今天',
},
{
key: 'preset-3',
label: '🎨 如何用 antd-style 实现暗色主题切换?',
group: '昨天',
},
{
key: 'preset-4',
label: '🗂️ ProLayout 侧边菜单如何动态生成?',
group: '昨天',
},
{
key: 'preset-5',
label: '📊 Ant Design Charts 折线图数据格式',
group: '昨天',
},
{
key: 'preset-6',
label: '🚀 Ant Design Pro 如何接入后端权限系统?',
group: '更早',
},
{
key: 'preset-7',
label: '🔍 ProForm 中 Select 远程搜索怎么实现?',
group: '更早',
},
{
key: 'preset-8',
label: '⚙️ Ant Design Token 定制主题最佳实践',
group: '更早',
},
]);
const [activeKey, setActiveKey] = useState<string>('default');
const [inputValue, setInputValue] = useState('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const provider = useMemo(() => createChatProvider() as any, []);
const { onRequest, abort, isRequesting, parsedMessages } = useXChat<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any,
ParsedMessage
>({
provider,
conversationKey: activeKey,
parser,
requestPlaceholder: { role: 'assistant', content: '' },
});
const sendMessage = (content: string) => {
setInputValue('');
setConversations((prev) =>
prev.map((c) =>
c.key === activeKey && c.isDraft
? { ...c, label: content.slice(0, 20), isDraft: false }
: c,
),
);
onRequest({ messages: [{ role: 'user', content }] });
};
const newChat = () => {
const key = crypto.randomUUID();
setConversations((prev) => [
{ key, label: '新对话', group: '今天', isDraft: true },
...prev,
]);
setActiveKey(key);
};
const bubbleItems = useMemo<BubbleItemType[]>(
() =>
parsedMessages.map((msg) => {
const parsed = msg.message as ParsedMessage;
const isAI = parsed.role === 'assistant';
const thinkContent =
parsed.role === 'assistant' ? parsed.thinkContent : undefined;
const item: BubbleItemType = {
key: msg.id,
role: isAI ? 'ai' : 'user',
content: parsed.content,
loading: isAI && msg.status === 'loading',
status: msg.status,
};
if (isAI && thinkContent) {
item.header = <Think>{thinkContent}</Think>;
}
return item;
}),
[parsedMessages],
);
const hasMessages = parsedMessages.length > 0;
return (
<PageContainer
ghost
childrenContentStyle={{
paddingBlock: 0,
height: 'calc(100vh - 160px)',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
<Card
variant="borderless"
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
styles={{
body: {
flex: 1,
padding: 0,
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
},
}}
>
<XProvider>
<div className={styles.layout}>
<div className={styles.sidebar}>
<Conversations
items={conversations}
activeKey={activeKey}
onActiveChange={setActiveKey}
groupable
menu={(conversation) => ({
items: [{ key: 'delete', label: '删除', danger: true }],
onClick: ({ key }) => {
if (key === 'delete') {
setConversations((prev) => {
const next = prev.filter(
(c) => c.key !== conversation.key,
);
if (next.length === 0) {
const key = crypto.randomUUID();
next.push({
key,
label: '💬 新对话',
group: '今天',
isDraft: true,
});
setActiveKey(key);
} else if (activeKey === conversation.key) {
setActiveKey(next[0]?.key ?? '');
}
return next;
});
}
},
})}
creation={{ onClick: newChat, label: '新建对话' }}
/>
</div>
<div className={styles.main}>
{hasMessages && (
<div className={styles.messages}>
<Bubble.List
items={bubbleItems}
role={roleConfig}
autoScroll
styles={{ root: { maxWidth: 940 } }}
/>
</div>
)}
<div
className={hasMessages ? styles.footer : styles.footerCenter}
>
{!hasMessages && (
<div className={styles.welcomeTitle}>
<TypewriterTitle />
</div>
)}
<Sender
value={inputValue}
onChange={setInputValue}
loading={isRequesting}
onSubmit={sendMessage}
onCancel={abort}
placeholder="输入消息,按 Enter 发送..."
autoSize={{ minRows: 4, maxRows: 8 }}
style={{ maxWidth: 940, width: '100%' }}
styles={{ input: { paddingBlock: 0 } }}
/>
</div>
</div>
</div>
</XProvider>
</Card>
</PageContainer>
);
};
export default ChatbotPage;

18
src/pages/chatbot/service.ts

@ -0,0 +1,18 @@
// src/pages/chatbot/service.ts
import { OpenAIChatProvider, XRequest } from '@ant-design/x-sdk';
export const CHAT_API_URL =
process.env.CHAT_API_URL ??
'https://api.x.ant.design/api/big_model_glm-4.5-flash';
/**
* Factory call once per component mount (wrap in useMemo).
* OpenAIChatProvider handles SSE parsing and history accumulation internally.
*/
export const createChatProvider = () =>
new OpenAIChatProvider({
request: XRequest(CHAT_API_URL, {
manual: true,
params: { model: 'glm-4.5-flash', stream: true },
}),
});

75
src/pages/chatbot/style.ts

@ -0,0 +1,75 @@
// src/pages/chatbot/style.ts
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css, token }) => ({
layout: css`
display: flex;
flex: 1;
overflow: hidden;
`,
sidebar: css`
width: 260px;
background: ${token.colorBgContainer};
border-right: 1px solid ${token.colorBorderSecondary};
display: flex;
flex-direction: column;
overflow: hidden;
`,
main: css`
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
background: ${token.colorBgContainer};
`,
messages: css`
flex: 1;
overflow-y: auto;
padding: ${token.paddingMD}px;
display: flex;
flex-direction: column;
align-items: center;
> * {
width: 100%;
}
`,
footer: css`
padding: ${token.paddingMD}px;
border-top: 1px solid ${token.colorBorderSecondary};
display: flex;
justify-content: center;
`,
footerCenter: css`
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: ${token.paddingLG}px;
gap: 32px;
margin-top: -10%;
`,
welcomeTitle: css`
font-size: 32px;
font-weight: 600;
color: ${token.colorText};
text-align: center;
`,
cursor: css`
animation: chatbot-blink 0.8s step-end infinite;
@keyframes chatbot-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
`,
}));

6
src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx

@ -1,7 +1,7 @@
import omit from '@rc-component/util/es/omit';
import { Card } from 'antd';
import type { CardProps } from 'antd/es/card';
import classNames from 'classnames';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
@ -53,7 +53,7 @@ const ChartCard: React.FC<ChartCardProps> = (props) => {
return (
<div className={styles.chartCard}>
<div
className={classNames(styles.chartTop, {
className={clsx(styles.chartTop, {
[styles.chartTopMargin]: !children && !footer,
})}
>
@ -80,7 +80,7 @@ const ChartCard: React.FC<ChartCardProps> = (props) => {
)}
{footer && (
<div
className={classNames(styles.footer, {
className={clsx(styles.footer, {
[styles.footerMargin]: !children,
})}
>

4
src/pages/dashboard/analysis/components/NumberInfo/index.tsx

@ -1,5 +1,5 @@
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
export type NumberInfoProps = {
@ -27,7 +27,7 @@ const NumberInfo: React.FC<NumberInfoProps> = ({
const { styles } = useStyles();
return (
<div
className={classNames({
className={clsx({
[styles[`numberInfo${theme}` as keyof typeof styles]]: !!theme,
})}
{...rest}

4
src/pages/dashboard/analysis/components/Trend/index.tsx

@ -1,5 +1,5 @@
import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
@ -21,7 +21,7 @@ const Trend: React.FC<TrendProps> = ({
...rest
}) => {
const { styles } = useStyles();
const classString = classNames(
const classString = clsx(
styles.trendItem,
{
[styles.trendItemGrey]: !colorful,

4
src/pages/list/search/applications/components/StandardFormRow/index.tsx

@ -1,4 +1,4 @@
import classNames from 'classnames';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
@ -19,7 +19,7 @@ const StandardFormRow: React.FC<StandardFormRowProps> = ({
...rest
}) => {
const { styles } = useStyles();
const cls = classNames(styles.standardFormRow, {
const cls = clsx(styles.standardFormRow, {
[styles.standardFormRowBlock]: block,
[styles.standardFormRowLast]: last,
[styles.standardFormRowGrid]: grid,

4
src/pages/list/search/applications/components/TagSelect/index.tsx

@ -1,7 +1,7 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useMergedState } from '@rc-component/util';
import { Tag } from 'antd';
import classNames from 'classnames';
import { clsx } from 'clsx';
import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style';
@ -107,7 +107,7 @@ const TagSelect: FC<TagSelectProps> & {
collapseText = '收起',
selectAllText = '全部',
} = actionsText;
const cls = classNames(styles.tagSelect, className, {
const cls = clsx(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
[styles.expanded]: expand,
});

4
src/pages/list/search/articles/components/StandardFormRow/index.tsx

@ -1,4 +1,4 @@
import classNames from 'classnames';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
@ -19,7 +19,7 @@ const StandardFormRow: React.FC<StandardFormRowProps> = ({
...rest
}) => {
const { styles } = useStyles();
const cls = classNames(styles.standardFormRow, {
const cls = clsx(styles.standardFormRow, {
[styles.standardFormRowBlock]: block,
[styles.standardFormRowLast]: last,
[styles.standardFormRowGrid]: grid,

4
src/pages/list/search/articles/components/TagSelect/index.tsx

@ -1,7 +1,7 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useMergedState } from '@rc-component/util';
import { Tag } from 'antd';
import classNames from 'classnames';
import { clsx } from 'clsx';
import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style';
@ -107,7 +107,7 @@ const TagSelect: FC<TagSelectProps> & {
collapseText = '收起',
selectAllText = '全部',
} = actionsText;
const cls = classNames(styles.tagSelect, className, {
const cls = clsx(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
[styles.expanded]: expand,
});

4
src/pages/list/search/projects/components/AvatarList/index.tsx

@ -1,5 +1,5 @@
import { Avatar, Tooltip } from 'antd';
import classNames from 'classnames';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
export declare type SizeType = number | 'small' | 'default' | 'large';
@ -21,7 +21,7 @@ export type AvatarListProps = {
| React.ReactElement<AvatarItemProps>[];
};
const avatarSizeToClassName = (size: SizeType | 'mini', styles: any) =>
classNames(styles.avatarItem, {
clsx(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',

4
src/pages/list/search/projects/components/StandardFormRow/index.tsx

@ -1,4 +1,4 @@
import classNames from 'classnames';
import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
@ -19,7 +19,7 @@ const StandardFormRow: React.FC<StandardFormRowProps> = ({
...rest
}) => {
const { styles } = useStyles();
const cls = classNames(styles.standardFormRow, {
const cls = clsx(styles.standardFormRow, {
[styles.standardFormRowBlock]: block,
[styles.standardFormRowLast]: last,
[styles.standardFormRowGrid]: grid,

4
src/pages/list/search/projects/components/TagSelect/index.tsx

@ -1,7 +1,7 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useMergedState } from '@rc-component/util';
import { Tag } from 'antd';
import classNames from 'classnames';
import { clsx } from 'clsx';
import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style';
@ -107,7 +107,7 @@ const TagSelect: FC<TagSelectProps> & {
collapseText = '收起',
selectAllText = '全部',
} = actionsText;
const cls = classNames(styles.tagSelect, className, {
const cls = clsx(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
[styles.expanded]: expand,
});

4
src/pages/profile/advanced/index.tsx

@ -25,7 +25,7 @@ import {
Table,
Tooltip,
} from 'antd';
import classNames from 'classnames';
import { clsx } from 'clsx';
import type { FC } from 'react';
import React, { useState } from 'react';
import type { AdvancedProfileData } from './data.d';
@ -183,7 +183,7 @@ const Advanced: FC = () => {
</RouteContext.Consumer>
);
const desc1 = (
<div className={classNames(styles.stepDescription)}>
<div className={clsx(styles.stepDescription)}>
<DingdingOutlined
style={{

4
src/pages/user/login/__snapshots__/login.test.tsx.snap

@ -473,7 +473,7 @@ exports[`Login Page should login success 1`] = `
target="_blank"
title="version"
>
v6.0.0-beta.3
v6.0.0-beta.4
</a>
<a
class="ant-pro-global-footer-list-link"
@ -984,7 +984,7 @@ exports[`Login Page should show login form 1`] = `
target="_blank"
title="version"
>
v6.0.0-beta.3
v6.0.0-beta.4
</a>
<a
class="ant-pro-global-footer-list-link"

18
src/pages/user/login/login.test.tsx

@ -4,14 +4,6 @@ import { TestBrowser } from '@@/testBrowser';
import { fireEvent, render } from '@testing-library/react';
import React, { act } from 'react';
const waitTime = (time: number = 100) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, time);
});
};
let server: {
close: () => void;
};
@ -88,15 +80,13 @@ describe('Login Page', () => {
await (await rootContainer.findByText('Login')).click();
// 等待接口返回结果
await waitTime(5000);
await rootContainer.findAllByText('Ant Design Pro');
// Wait for login to succeed and navigate to home page
await rootContainer.findByText('Ant Design Pro', undefined, {
timeout: 10000,
});
expect(rootContainer.asFragment()).toMatchSnapshot();
await waitTime(2000);
rootContainer.unmount();
});
});

1
tests/__mocks__/mermaid.js

@ -0,0 +1 @@
module.exports = {};
Loading…
Cancel
Save