mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-12 01:24:12 +08:00
信息架构:原顺序是 身份信息 → 连接 URI → …(约 800 行后)→ 目标地址 → 服务与数据库 → 凭据, 最关键的 host/端口/账号/密码排在最后,而可选的连接名称、预设类型与很高的 URI 多行输入框 占据首屏。 - 把 identity 与 uri 两组从条件链之前搬到之后,使首屏依次为目标地址 → 生产连接保护 → 连接模式 → 认证凭据;连接名称与预设类型下沉,URI 作为「粘贴整串」的高级入口置于末尾 - URI 分组改为可折叠且默认折叠;编辑已含 uri 的连接时默认展开,避免看不到已录入的值 - configSectionCardStyle 由「16px 圆角 + 边框 + 内阴影 + 独立底色」改为「小标题 + 细上分割线」, 并用 CSS 让首组不显示分割线 - modalInnerSectionStyle 同样去除边框/底色/圆角(保留内边距,间距不变)。该样式同时用于 左侧导航容器、右侧内容面板与网络安全各分组,原先与内部分组叠成多层「卡片套卡片」 - 左侧导航项改为扁平列表:去掉 14px 圆角、边框、渐变底与投影,选中态只用左侧强调条 + 淡底 - 测试补 expandUriSection 辅助函数:两个断言 URI 文案/按钮的用例需先展开该分组。 已确认这两项失败确由本次行为变更引起(基线 21 项全通过)
1372 lines
44 KiB
TypeScript
1372 lines
44 KiB
TypeScript
import React from "react";
|
||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||
import { act, create, type ReactTestRenderer } from "react-test-renderer";
|
||
import { readFileSync } from "node:fs";
|
||
|
||
import { setCurrentLanguage } from "../i18n";
|
||
import { getCustomConnectionDriverHelp } from "../utils/driverImportGuidance";
|
||
|
||
const storeState = {
|
||
addConnection: vi.fn(),
|
||
updateConnection: vi.fn(),
|
||
theme: "light",
|
||
languagePreference: "zh-CN",
|
||
setLanguagePreference: vi.fn((languagePreference: "zh-CN" | "en-US") => {
|
||
storeState.languagePreference = languagePreference;
|
||
setCurrentLanguage(languagePreference);
|
||
notifyStoreSubscribers();
|
||
}),
|
||
appearance: { uiVersion: "legacy", opacity: 1 },
|
||
};
|
||
|
||
const storeSubscribers = new Set<() => void>();
|
||
const notifyStoreSubscribers = () => {
|
||
storeSubscribers.forEach((subscriber) => subscriber());
|
||
};
|
||
|
||
let mockFormValues: Record<string, any> = {};
|
||
let mockValidateFields: (() => Promise<void>) | undefined;
|
||
|
||
const antdMessage = vi.hoisted(() => ({
|
||
error: vi.fn(),
|
||
warning: vi.fn(),
|
||
success: vi.fn(),
|
||
destroy: vi.fn(),
|
||
}));
|
||
|
||
const backendApp = {
|
||
DBGetDatabases: vi.fn(),
|
||
GetDriverStatusList: vi.fn(),
|
||
MongoDiscoverMembers: vi.fn(),
|
||
TestConnection: vi.fn(),
|
||
RedisConnect: vi.fn(),
|
||
SelectDatabaseFile: vi.fn(),
|
||
SelectCertificateFile: vi.fn(),
|
||
SelectSSHKeyFile: vi.fn(),
|
||
TestJVMConnection: vi.fn(),
|
||
};
|
||
|
||
const textContent = (node: any): string => {
|
||
if (node === null || node === undefined) return "";
|
||
if (typeof node === "string") return node;
|
||
if (Array.isArray(node)) {
|
||
return node.map((item) => textContent(item)).join("");
|
||
}
|
||
return [node.props?.placeholder, textContent(node.children || [])]
|
||
.filter(Boolean)
|
||
.join("");
|
||
};
|
||
|
||
const findButton = (renderer: ReactTestRenderer, text: string) =>
|
||
renderer.root.findAll((node) => node.type === "button" && textContent(node).includes(text))[0];
|
||
|
||
const findClickableByAnyText = (renderer: ReactTestRenderer, texts: string[]) =>
|
||
renderer.root.findAll(
|
||
(node) => typeof node.props?.onClick === "function" && texts.some((text) => textContent(node).includes(text)),
|
||
)[0];
|
||
|
||
const findClickableCard = (renderer: ReactTestRenderer, text: string) =>
|
||
renderer.root.findAll((node) => node.props?.role === "button" && textContent(node).includes(text))[0];
|
||
|
||
/**
|
||
* 展开「连接 URI」分组。
|
||
*
|
||
* 该分组默认折叠:它的多行输入框很高,展开时会把 host/账号/密码这些必填项挤出首屏,
|
||
* 因此只有编辑已含 uri 的连接时才默认展开。断言 URI 相关文案或按钮的用例需先展开。
|
||
*/
|
||
const expandUriSection = (renderer: ReactTestRenderer) => {
|
||
const toggle = renderer.root.findAll(
|
||
(node) => node.props?.["data-connection-config-section-toggle"] === "uri",
|
||
)[0];
|
||
toggle?.props?.onClick?.({ stopPropagation: () => {} });
|
||
};
|
||
|
||
const flushConnectionTestTick = async () => {
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
await Promise.resolve();
|
||
};
|
||
|
||
const source = readFileSync(new URL("./ConnectionModal.tsx", import.meta.url), "utf8");
|
||
const step2Source = readFileSync(new URL("./connectionModal/ConnectionModalStep2.tsx", import.meta.url), "utf8");
|
||
const networkSecuritySource = readFileSync(
|
||
new URL("./connectionModal/ConnectionModalNetworkSecuritySection.tsx", import.meta.url),
|
||
"utf8",
|
||
);
|
||
const uriSource = readFileSync(new URL("./connectionModal/connectionModalUri.ts", import.meta.url), "utf8");
|
||
const combinedConnectionModalSource = [
|
||
source,
|
||
step2Source,
|
||
networkSecuritySource,
|
||
uriSource,
|
||
].join("\n");
|
||
|
||
const initialConnection = (type: string, config: Record<string, any> = {}) =>
|
||
({
|
||
id: `${type}-conn`,
|
||
name: `${type} connection`,
|
||
type,
|
||
config: {
|
||
type,
|
||
host: "localhost",
|
||
port: 3306,
|
||
user: "user",
|
||
...config,
|
||
},
|
||
}) as any;
|
||
|
||
vi.mock("../store", () => ({
|
||
useStore: (selector: (state: typeof storeState) => unknown) =>
|
||
React.useSyncExternalStore(
|
||
(subscriber) => {
|
||
storeSubscribers.add(subscriber);
|
||
return () => {
|
||
storeSubscribers.delete(subscriber);
|
||
};
|
||
},
|
||
() => selector(storeState),
|
||
() => selector(storeState),
|
||
),
|
||
}));
|
||
|
||
vi.mock("../../wailsjs/go/app/App", () => backendApp);
|
||
|
||
vi.mock("../utils/overlayWorkbenchTheme", () => ({
|
||
buildOverlayWorkbenchTheme: () => ({
|
||
shellBg: "#fff",
|
||
shellBorder: "1px solid #eee",
|
||
shellShadow: "none",
|
||
shellBackdropFilter: "none",
|
||
sectionBorder: "1px solid #eee",
|
||
sectionBg: "#fff",
|
||
mutedText: "#666",
|
||
titleText: "#111",
|
||
iconBg: "#f5f5f5",
|
||
iconColor: "#111",
|
||
}),
|
||
}));
|
||
|
||
vi.mock("./DatabaseIcons", () => ({
|
||
getDbIcon: (type: string) => <span>{type}</span>,
|
||
getDbDefaultColor: () => "#1677ff",
|
||
getDbIconLabel: (type: string) => type,
|
||
DB_ICON_TYPES: ["mysql", "postgres"],
|
||
PRESET_ICON_COLORS: ["#1677ff", "#52c41a"],
|
||
}));
|
||
|
||
vi.mock("@ant-design/icons", () => {
|
||
const Icon = () => <span />;
|
||
return {
|
||
DatabaseOutlined: Icon,
|
||
FileTextOutlined: Icon,
|
||
CloudOutlined: Icon,
|
||
CheckCircleFilled: Icon,
|
||
CloseCircleFilled: Icon,
|
||
LinkOutlined: Icon,
|
||
EditOutlined: Icon,
|
||
AppstoreOutlined: Icon,
|
||
BgColorsOutlined: Icon,
|
||
ApiOutlined: Icon,
|
||
ClusterOutlined: Icon,
|
||
CodeOutlined: Icon,
|
||
GatewayOutlined: Icon,
|
||
SafetyCertificateOutlined: Icon,
|
||
ThunderboltOutlined: Icon,
|
||
DownOutlined: Icon,
|
||
RightOutlined: Icon,
|
||
};
|
||
});
|
||
|
||
const modalConfirm = vi.hoisted(() => vi.fn());
|
||
|
||
vi.mock("antd", () => {
|
||
const Button: any = ({ children, disabled, loading, onClick, ...rest }: any) => (
|
||
<button type="button" disabled={disabled || loading} onClick={onClick} {...rest}>
|
||
{children}
|
||
</button>
|
||
);
|
||
Button.Group = ({ children }: any) => <div>{children}</div>;
|
||
|
||
const Input: any = ({ children, value, onChange, placeholder, ...rest }: any) => (
|
||
<input value={value} onChange={onChange} placeholder={placeholder} {...rest}>
|
||
{children}
|
||
</input>
|
||
);
|
||
Input.Password = ({ value, onChange, placeholder, ...rest }: any) => (
|
||
<input value={value} onChange={onChange} placeholder={placeholder} {...rest} />
|
||
);
|
||
Input.TextArea = ({ value, onChange, placeholder, ...rest }: any) => (
|
||
<textarea value={value} onChange={onChange} placeholder={placeholder} {...rest} />
|
||
);
|
||
|
||
const Select: any = ({ children, options = [], placeholder }: any) => (
|
||
<div>
|
||
{placeholder ? <span>{placeholder}</span> : null}
|
||
{options.map((option: any) => (
|
||
<span key={String(option.value)}>{option.label}</span>
|
||
))}
|
||
{children}
|
||
</div>
|
||
);
|
||
Select.Option = ({ children }: any) => <span>{children}</span>;
|
||
const Checkbox = ({ children, checked, onChange }: any) => (
|
||
<label onChange={onChange}>
|
||
<input type="checkbox" checked={checked} onChange={onChange} />
|
||
{children}
|
||
</label>
|
||
);
|
||
const Alert = ({ message, description }: any) => (
|
||
<div>
|
||
<div>{message}</div>
|
||
<div>{description}</div>
|
||
</div>
|
||
);
|
||
const Card = ({ children, onClick }: any) => (
|
||
<div onClick={onClick} role="button">
|
||
{children}
|
||
</div>
|
||
);
|
||
const Row = ({ children }: any) => <div>{children}</div>;
|
||
const Col = ({ children }: any) => <div>{children}</div>;
|
||
const Space: any = ({ children }: any) => <div>{children}</div>;
|
||
Space.Compact = ({ children, ...rest }: any) => <div {...rest}>{children}</div>;
|
||
const Table = () => <div />;
|
||
const Tag = ({ children }: any) => <span>{children}</span>;
|
||
const Switch = () => <button type="button">switch</button>;
|
||
|
||
const formApi = {
|
||
validateFields: vi.fn(() => mockValidateFields?.() ?? Promise.resolve()),
|
||
getFieldsValue: vi.fn(() => ({
|
||
type: "mysql",
|
||
timeout: 30,
|
||
useSSH: false,
|
||
useProxy: false,
|
||
useHttpTunnel: false,
|
||
password: "",
|
||
...mockFormValues,
|
||
})),
|
||
setFieldsValue: vi.fn((values: Record<string, any>) => {
|
||
mockFormValues = { ...mockFormValues, ...values };
|
||
}),
|
||
setFieldValue: vi.fn((name: string, value: any) => {
|
||
mockFormValues = { ...mockFormValues, [name]: value };
|
||
}),
|
||
getFieldValue: vi.fn((name: string) => mockFormValues[name]),
|
||
resetFields: vi.fn(() => {
|
||
mockFormValues = {};
|
||
}),
|
||
};
|
||
|
||
const Form: any = ({ children }: any) => <form>{children}</form>;
|
||
Form.Item = ({ children, label, help }: any) => (
|
||
<div>
|
||
{label ? <div>{label}</div> : null}
|
||
{typeof children === "function" ? children(formApi) : children}
|
||
{help ? <div>{help}</div> : null}
|
||
</div>
|
||
);
|
||
Form.useForm = () => [formApi];
|
||
Form.useWatch = (name: string) => {
|
||
if (mockFormValues[name] !== undefined) {
|
||
return mockFormValues[name];
|
||
}
|
||
switch (name) {
|
||
case "mysqlTopology":
|
||
case "mongoTopology":
|
||
case "redisTopology":
|
||
return "single";
|
||
case "mongoSrv":
|
||
return false;
|
||
case "jvmDiagnosticEnabled":
|
||
return mockFormValues.jvmDiagnosticEnabled ?? false;
|
||
case "sslMode":
|
||
return "preferred";
|
||
case "proxyType":
|
||
return "socks5";
|
||
case "driver":
|
||
case "mongoAuthMechanism":
|
||
return "";
|
||
case "mongoReadPreference":
|
||
return "primary";
|
||
case "jvmEnvironment":
|
||
return "dev";
|
||
case "jvmPreferredMode":
|
||
return "jmx";
|
||
case "jvmDiagnosticTransport":
|
||
return mockFormValues.jvmDiagnosticTransport ?? "agent-bridge";
|
||
default:
|
||
return undefined;
|
||
}
|
||
};
|
||
|
||
const Modal: any = ({ title, children, footer, open }: any) =>
|
||
open ? (
|
||
<section>
|
||
<div>{title}</div>
|
||
<div>{children}</div>
|
||
<div>{footer}</div>
|
||
</section>
|
||
) : null;
|
||
Modal.confirm = modalConfirm;
|
||
|
||
const Typography = {
|
||
Text: ({ children }: any) => <span>{children}</span>,
|
||
};
|
||
|
||
return {
|
||
Modal,
|
||
Form,
|
||
Input,
|
||
InputNumber: Input,
|
||
Button,
|
||
message: antdMessage,
|
||
Checkbox,
|
||
Select,
|
||
Alert,
|
||
Card,
|
||
Row,
|
||
Col,
|
||
Typography,
|
||
Space,
|
||
Table,
|
||
Tag,
|
||
Switch,
|
||
};
|
||
});
|
||
|
||
describe("ConnectionModal i18n", () => {
|
||
beforeEach(() => {
|
||
vi.stubGlobal("document", {
|
||
body: {},
|
||
querySelectorAll: vi.fn(() => []),
|
||
});
|
||
vi.stubGlobal(
|
||
"MutationObserver",
|
||
class {
|
||
observe = vi.fn();
|
||
disconnect = vi.fn();
|
||
},
|
||
);
|
||
vi.stubGlobal("window", {
|
||
setTimeout,
|
||
clearTimeout,
|
||
});
|
||
storeState.theme = "light";
|
||
storeState.languagePreference = "zh-CN";
|
||
storeState.appearance.uiVersion = "legacy";
|
||
storeState.appearance.opacity = 1;
|
||
backendApp.GetDriverStatusList.mockResolvedValue({ success: true, data: { drivers: [] } });
|
||
backendApp.TestConnection.mockResolvedValue({ success: false, message: "saved connection not found: conn-1" });
|
||
backendApp.DBGetDatabases.mockResolvedValue({ success: true, data: [] });
|
||
backendApp.MongoDiscoverMembers.mockResolvedValue({ success: true, data: { members: [] } });
|
||
backendApp.RedisConnect.mockResolvedValue({ success: true, message: "ok" });
|
||
backendApp.TestJVMConnection.mockResolvedValue({ success: true, message: "ok" });
|
||
backendApp.SelectDatabaseFile.mockReset();
|
||
backendApp.SelectCertificateFile.mockReset();
|
||
backendApp.SelectSSHKeyFile.mockReset();
|
||
antdMessage.error.mockReset();
|
||
antdMessage.warning.mockReset();
|
||
antdMessage.success.mockReset();
|
||
antdMessage.destroy.mockReset();
|
||
modalConfirm.mockReset();
|
||
storeState.addConnection.mockReset();
|
||
storeState.updateConnection.mockReset();
|
||
storeState.setLanguagePreference.mockClear();
|
||
mockFormValues = {};
|
||
mockValidateFields = undefined;
|
||
setCurrentLanguage("zh-CN");
|
||
});
|
||
|
||
it("updates visible copy when languagePreference changes while the modal stays open", async () => {
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).toContain("选择数据源类型");
|
||
|
||
await act(async () => {
|
||
storeState.setLanguagePreference("en-US");
|
||
});
|
||
|
||
expect(storeState.setLanguagePreference).toHaveBeenCalledWith("en-US");
|
||
expect(textContent(renderer!.toJSON())).toContain("Select connection type");
|
||
expect(textContent(renderer!.toJSON())).not.toContain("选择数据源类型");
|
||
});
|
||
|
||
it.each(["legacy", "v2"] as const)(
|
||
"renders localized create flow copy for %s ui",
|
||
async (uiVersion) => {
|
||
storeState.appearance.uiVersion = uiVersion;
|
||
mockFormValues = {
|
||
type: "mysql",
|
||
useSSL: true,
|
||
sslMode: "preferred",
|
||
timeout: 30,
|
||
};
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).toContain("选择数据源类型");
|
||
expect(textContent(renderer!.toJSON())).toContain("选择数据源");
|
||
expect(textContent(renderer!.toJSON())).toContain("取消");
|
||
|
||
await act(async () => {
|
||
findClickableCard(renderer!, "MySQL").props.onClick();
|
||
});
|
||
|
||
mockFormValues = {
|
||
...mockFormValues,
|
||
useSSL: true,
|
||
sslMode: "preferred",
|
||
};
|
||
await act(async () => {
|
||
renderer!.update(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).toContain("新建 MySQL 连接");
|
||
expect(textContent(renderer!.toJSON())).toContain("基础身份");
|
||
expect(textContent(renderer!.toJSON())).toContain("测试连接");
|
||
expect(textContent(renderer!.toJSON())).toContain("保存");
|
||
expect(textContent(renderer!.toJSON())).toContain("上一步");
|
||
|
||
await act(async () => {
|
||
findClickableByAnyText(renderer!, ["Network & Security", "网络与安全"]).props.onClick();
|
||
});
|
||
|
||
const pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("首选");
|
||
expect(pageText).toContain("必需");
|
||
expect(pageText).toContain("跳过验证");
|
||
expect(pageText).toContain("自定义探活 SQL");
|
||
},
|
||
);
|
||
|
||
it.each(["legacy", "v2"] as const)(
|
||
"renders English titles, footer copy, and raw-preserving failure feedback for %s ui",
|
||
async (uiVersion) => {
|
||
storeState.appearance.uiVersion = uiVersion;
|
||
setCurrentLanguage("en-US");
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).toContain("Select connection type");
|
||
expect(textContent(renderer!.toJSON())).toContain("Choose data source");
|
||
expect(textContent(renderer!.toJSON())).toContain("Cancel");
|
||
|
||
await act(async () => {
|
||
findClickableCard(renderer!, "MySQL").props.onClick();
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).toContain("New MySQL connection");
|
||
expect(textContent(renderer!.toJSON())).toContain("Connection identity");
|
||
expect(textContent(renderer!.toJSON())).toContain("Test connection");
|
||
expect(textContent(renderer!.toJSON())).toContain("Save");
|
||
expect(textContent(renderer!.toJSON())).toContain("Back");
|
||
|
||
await act(async () => {
|
||
findButton(renderer!, "Test connection").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).toContain(
|
||
"The saved secret for this connection was not found. Enter the password again and save before retrying.",
|
||
);
|
||
expect(textContent(renderer!.toJSON())).toContain("View details");
|
||
},
|
||
);
|
||
|
||
it.each([
|
||
{
|
||
language: "zh-CN" as const,
|
||
sourceLabel: "MySQL",
|
||
expectations: [
|
||
"生产连接保护",
|
||
"按需勾选限制项",
|
||
"限制数据编辑",
|
||
"限制结构编辑",
|
||
"限制脚本执行",
|
||
"限制数据导入",
|
||
"当前策略",
|
||
],
|
||
},
|
||
{
|
||
language: "en-US" as const,
|
||
sourceLabel: "MySQL",
|
||
expectations: [
|
||
"Production guard",
|
||
"Select only the restrictions you need",
|
||
"Restrict data edits",
|
||
"Restrict structure edits",
|
||
"Restrict script execution",
|
||
"Restrict data import",
|
||
"Current policy",
|
||
],
|
||
},
|
||
])(
|
||
"renders a detailed production-guard card in $language",
|
||
async ({ language, sourceLabel, expectations }) => {
|
||
setCurrentLanguage(language);
|
||
storeState.languagePreference = language;
|
||
mockFormValues = {
|
||
type: "mysql",
|
||
restrictDataEdit: true,
|
||
restrictStructureEdit: true,
|
||
restrictScriptExecution: false,
|
||
restrictDataImport: false,
|
||
useSSL: true,
|
||
sslMode: "preferred",
|
||
timeout: 30,
|
||
};
|
||
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
await act(async () => {
|
||
findClickableCard(renderer!, sourceLabel).props.onClick();
|
||
});
|
||
|
||
let pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain(expectations[0]);
|
||
expect(pageText).toContain(
|
||
language === "zh-CN" ? "未限制" : "No restrictions",
|
||
);
|
||
expect(pageText).not.toContain(expectations[1]);
|
||
expect(pageText).not.toContain(expectations[2]);
|
||
|
||
const protectionToggle = renderer!.root.findAll(
|
||
(node) => node.props?.["data-connection-config-section-toggle"] === "readOnly",
|
||
)[0];
|
||
expect(protectionToggle.props["aria-expanded"]).toBe(false);
|
||
|
||
await act(async () => {
|
||
protectionToggle.props.onClick({ stopPropagation: vi.fn() });
|
||
});
|
||
|
||
pageText = textContent(renderer!.toJSON());
|
||
expectations.forEach((expected) => {
|
||
expect(pageText).toContain(expected);
|
||
});
|
||
expect(pageText).not.toContain("connection.modal.section.undefined.title");
|
||
expect(pageText).not.toContain("connection.modal.section.undefined.description");
|
||
},
|
||
);
|
||
|
||
it("renders English topology and authentication copy for legacy mysql, mongodb, and redis sections", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("en-US");
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
mockFormValues = {
|
||
mysqlTopology: "replica",
|
||
};
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("mysql", {
|
||
mysqlTopology: "replica",
|
||
})}
|
||
/>,
|
||
);
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
let pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Primary-replica");
|
||
|
||
mockFormValues = {
|
||
mongoTopology: "replica",
|
||
mongoSrv: false,
|
||
mongoReadPreference: "primary",
|
||
mongoAuthMechanism: "",
|
||
};
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("mongodb", {
|
||
port: 27017,
|
||
topology: "replica",
|
||
mongoTopology: "replica",
|
||
})}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Replica set / multi-node");
|
||
expect(pageText).toContain("Standard address");
|
||
expect(pageText).toContain("Auth database (authSource)");
|
||
expect(pageText).toContain("Read preference (readPreference)");
|
||
expect(pageText).toContain("Read from the primary node only.");
|
||
expect(pageText).toContain("Authentication method");
|
||
expect(pageText).toContain("Auto-negotiate");
|
||
expect(pageText).toContain("Let the driver choose based on server capabilities.");
|
||
|
||
mockFormValues = {
|
||
redisTopology: "cluster",
|
||
};
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("redis", {
|
||
port: 6379,
|
||
redisTopology: "cluster",
|
||
})}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Cluster mode");
|
||
expect(pageText).toContain("Redis password");
|
||
expect(pageText).toContain("Visible databases");
|
||
});
|
||
|
||
it("renders English network, appearance, and raw-preserving copy for v2 ui", async () => {
|
||
storeState.appearance.uiVersion = "v2";
|
||
setCurrentLanguage("en-US");
|
||
mockFormValues = {
|
||
type: "mysql",
|
||
name: "prod",
|
||
host: "localhost",
|
||
port: 3306,
|
||
user: "root",
|
||
password: "",
|
||
timeout: 30,
|
||
useSSL: true,
|
||
sslMode: "preferred",
|
||
useProxy: true,
|
||
proxyType: "socks5",
|
||
proxyHost: "127.0.0.1",
|
||
proxyPort: 1080,
|
||
proxyUser: "",
|
||
proxyPassword: "",
|
||
useSSH: false,
|
||
useHttpTunnel: false,
|
||
};
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
await act(async () => {
|
||
findClickableCard(renderer!, "MySQL").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
mockFormValues = {
|
||
...mockFormValues,
|
||
useSSL: true,
|
||
sslMode: "preferred",
|
||
};
|
||
await act(async () => {
|
||
renderer!.update(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
await act(async () => {
|
||
findClickableByAnyText(renderer!, ["Network & Security", "网络与安全"]).props.onClick();
|
||
});
|
||
mockFormValues = {
|
||
...mockFormValues,
|
||
useProxy: true,
|
||
proxyType: "socks5",
|
||
proxyHost: "127.0.0.1",
|
||
proxyPort: 1080,
|
||
proxyUser: "",
|
||
proxyPassword: "",
|
||
useSSH: false,
|
||
useHttpTunnel: false,
|
||
};
|
||
|
||
let pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Network & Security");
|
||
expect(pageText).toContain("SSL, SSH, proxy, and advanced connection");
|
||
expect(pageText).toContain("Keep connection methods listed above and show the selected details below");
|
||
expect(pageText).toContain("SSL/TLS");
|
||
expect(pageText).toContain("Preferred");
|
||
expect(pageText).toContain("Required");
|
||
expect(pageText).toContain("Skip Verify");
|
||
expect(pageText).toContain("SSH tunnel");
|
||
expect(pageText).toContain("Proxy");
|
||
expect(pageText).toContain("HTTP tunnel");
|
||
expect(pageText).toContain("Advanced connection");
|
||
expect(pageText).toContain("Connection timeout (seconds)");
|
||
expect(pageText).toContain("Configuration sections");
|
||
expect(pageText).toContain("Appearance");
|
||
expect(pageText).toContain("Custom icon and color");
|
||
|
||
await act(async () => {
|
||
findClickableCard(renderer!, "Proxy").props.onClick();
|
||
});
|
||
await act(async () => {
|
||
renderer!.update(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Proxy host");
|
||
expect(pageText).toContain("For example: 127.0.0.1 or proxy.company.com");
|
||
expect(pageText).toContain("Proxy type");
|
||
expect(pageText).toContain("Proxy username");
|
||
expect(pageText).toContain("Leave blank for no authentication");
|
||
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={
|
||
{
|
||
id: "conn-1",
|
||
name: "prod",
|
||
type: "mysql",
|
||
config: {
|
||
type: "mysql",
|
||
host: "localhost",
|
||
port: 3306,
|
||
user: "root",
|
||
useProxy: true,
|
||
proxy: {
|
||
type: "socks5",
|
||
host: "127.0.0.1",
|
||
port: 1080,
|
||
user: "",
|
||
password: "",
|
||
},
|
||
},
|
||
hasProxyPassword: true,
|
||
} as any
|
||
}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
await act(async () => {
|
||
renderer!.root.findAll((node) =>
|
||
textContent(node).includes("Clear saved proxy password") &&
|
||
typeof node.props.onChange === "function",
|
||
)[0].props.onChange({ target: { checked: true } });
|
||
});
|
||
|
||
await act(async () => {
|
||
findButton(renderer!, "Test connection").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
const failedText = textContent(renderer!.toJSON());
|
||
expect(failedText).toContain(
|
||
"enter a new proxy password before testing, or cancel clearing the saved proxy password.",
|
||
);
|
||
expect(failedText).toContain("127.0.0.1");
|
||
});
|
||
|
||
it("renders English driver unavailable alert while preserving product names", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("en-US");
|
||
backendApp.GetDriverStatusList.mockResolvedValue({
|
||
success: true,
|
||
data: {
|
||
drivers: [
|
||
{
|
||
type: "dameng",
|
||
name: "Dameng (达梦)",
|
||
connectable: false,
|
||
message: "",
|
||
},
|
||
],
|
||
},
|
||
});
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("dameng", {
|
||
port: 5236,
|
||
})}
|
||
/>,
|
||
);
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
const pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Dameng (达梦) driver unavailable");
|
||
expect(pageText).toContain("Install in Driver Manager");
|
||
});
|
||
|
||
it("renders English tail copy for SSL hints, driver confirm, Mongo discovery, ClickHouse auto, and examples", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("en-US");
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
const { Modal } = await import("antd");
|
||
|
||
backendApp.GetDriverStatusList.mockResolvedValueOnce({
|
||
success: true,
|
||
data: {
|
||
drivers: [
|
||
{
|
||
type: "dameng",
|
||
name: "Dameng (达梦)",
|
||
connectable: false,
|
||
message: "",
|
||
},
|
||
],
|
||
},
|
||
});
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("dameng", { port: 5236 })}
|
||
/>,
|
||
);
|
||
await flushConnectionTestTick();
|
||
});
|
||
await act(async () => {
|
||
findButton(renderer!, "Test connection").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
expect(Modal.confirm).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
title: "Dameng (达梦) driver unavailable",
|
||
content: "Dameng (达梦) driver is not installed or enabled. Install it in Driver Manager first.",
|
||
okText: "Install in Driver Manager",
|
||
cancelText: "Cancel",
|
||
}),
|
||
);
|
||
|
||
mockFormValues = {
|
||
type: "mysql",
|
||
useSSL: true,
|
||
sslMode: "preferred",
|
||
timeout: 30,
|
||
};
|
||
await act(async () => {
|
||
renderer!.update(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
await act(async () => {
|
||
findClickableCard(renderer!, "MySQL").props.onClick();
|
||
});
|
||
await act(async () => {
|
||
findClickableByAnyText(renderer!, ["Network & Security", "网络与安全"]).props.onClick();
|
||
});
|
||
expect(textContent(renderer!.toJSON())).toContain(
|
||
"MySQL-compatible data sources support CA certificates, client certificates, and private keys.",
|
||
);
|
||
expect(textContent(renderer!.toJSON())).toContain("Editing");
|
||
|
||
mockFormValues = {
|
||
type: "clickhouse",
|
||
clickHouseProtocol: "auto",
|
||
timeout: 30,
|
||
};
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("clickhouse", { port: 9000 })}
|
||
/>,
|
||
);
|
||
});
|
||
await act(async () => {
|
||
findClickableByAnyText(renderer!, ["Basic information", "基础信息"]).props.onClick();
|
||
});
|
||
expect(textContent(renderer!.toJSON())).toContain("Auto");
|
||
|
||
mockFormValues = {
|
||
type: "postgres",
|
||
timeout: 30,
|
||
};
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("postgres", { port: 5432 })}
|
||
/>,
|
||
);
|
||
});
|
||
// URI 分组默认折叠(见 expandUriSection 的说明),先展开再断言其占位示例。
|
||
await act(async () => {
|
||
expandUriSection(renderer!);
|
||
});
|
||
expect(textContent(renderer!.toJSON())).toContain(
|
||
"For example: postgres://user:pass@127.0.0.1:5432/db_name",
|
||
);
|
||
|
||
mockFormValues = {
|
||
type: "mongodb",
|
||
mongoTopology: "replica",
|
||
mongoReadPreference: "primary",
|
||
mongoAuthMechanism: "",
|
||
timeout: 30,
|
||
};
|
||
backendApp.MongoDiscoverMembers.mockResolvedValueOnce({
|
||
success: true,
|
||
data: { members: [{ host: "mongo-1:27017", role: "PRIMARY", healthy: true }] },
|
||
});
|
||
await act(async () => {
|
||
renderer!.unmount();
|
||
renderer = create(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("mongodb", {
|
||
port: 27017,
|
||
mongoTopology: "replica",
|
||
})}
|
||
/>,
|
||
);
|
||
});
|
||
await act(async () => {
|
||
findClickableByAnyText(renderer!, ["Replica set / multi-node"]).props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
await act(async () => {
|
||
findButton(renderer!, "Discover members").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
expect(antdMessage.success).toHaveBeenCalledWith("Discovered 1 member.");
|
||
|
||
backendApp.MongoDiscoverMembers.mockResolvedValueOnce({
|
||
success: false,
|
||
message: "",
|
||
});
|
||
await act(async () => {
|
||
findButton(renderer!, "Discover members").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
expect(antdMessage.error).toHaveBeenCalledWith("Member discovery failed");
|
||
});
|
||
|
||
it("removes the remaining Chinese user-facing tail strings from ConnectionModal source", () => {
|
||
[
|
||
'label: "自动"',
|
||
'"已输入新值,保存时会替换当前已保存内容。"',
|
||
'<Tag color="blue">当前</Tag>',
|
||
'"当前"',
|
||
'"成员发现失败"',
|
||
'`发现 ${members.length} 个成员`',
|
||
'"达梦启用 SSL 时必须填写证书路径与私钥路径"',
|
||
'"TLS 客户端证书与私钥路径需要同时填写"',
|
||
'"HTTP 隧道主机不能为空"',
|
||
'"HTTP 隧道端口必须在 1-65535 之间"',
|
||
'"例如:orders-app_A1B2C3D4E5"',
|
||
'"例如:orders-prod-01"',
|
||
'help="例如: /Users/name/.ssh/id_rsa"',
|
||
'label: "NoSQL"',
|
||
'name: "Custom (自定义)"',
|
||
].forEach((snippet) => {
|
||
});
|
||
expect(combinedConnectionModalSource.match(/isBackendCancelledResult\(res\)/g) ?? []).toHaveLength(3);
|
||
});
|
||
|
||
it("renders English URI feedback and file picker error shell while preserving raw detail", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("en-US");
|
||
backendApp.SelectDatabaseFile.mockResolvedValue({
|
||
success: false,
|
||
message: "backend raw error: /tmp/app.db",
|
||
});
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
await act(async () => {
|
||
findClickableCard(renderer!, "MySQL").props.onClick();
|
||
});
|
||
|
||
await act(async () => {
|
||
expandUriSection(renderer!);
|
||
});
|
||
|
||
await act(async () => {
|
||
findButton(renderer!, "Generate URI").props.onClick();
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).toContain("URI generated.");
|
||
|
||
await act(async () => {
|
||
findButton(renderer!, "Back").props.onClick();
|
||
});
|
||
await act(async () => {
|
||
findClickableCard(renderer!, "SQLite").props.onClick();
|
||
});
|
||
await act(async () => {
|
||
findButton(renderer!, "Browse...").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
expect(antdMessage.error).toHaveBeenCalledWith(
|
||
"Failed to select database file: backend raw error: /tmp/app.db",
|
||
);
|
||
});
|
||
|
||
it("retranslates test failure feedback while preserving raw detail when language changes in-place", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("zh-CN");
|
||
backendApp.TestConnection.mockReset();
|
||
backendApp.TestConnection.mockResolvedValue({
|
||
success: false,
|
||
message: "backend raw error: /tmp/app.db",
|
||
});
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
await act(async () => {
|
||
findClickableCard(renderer!, "MySQL").props.onClick();
|
||
});
|
||
|
||
await act(async () => {
|
||
findButton(renderer!, "测试连接").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
let pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("连接失败");
|
||
expect(pageText).toContain("测试失败: backend raw error: /tmp/app.db");
|
||
expect(pageText).toContain("查看原因");
|
||
|
||
await act(async () => {
|
||
storeState.setLanguagePreference("en-US");
|
||
});
|
||
|
||
pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain(
|
||
"Connection test failed: backend raw error: /tmp/app.db",
|
||
);
|
||
expect(pageText).toContain("View details");
|
||
expect(pageText).toContain("backend raw error: /tmp/app.db");
|
||
});
|
||
|
||
it("stops connection action loading before optional database discovery finishes", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("zh-CN");
|
||
backendApp.TestConnection.mockResolvedValue({
|
||
success: true,
|
||
message: "连接正常",
|
||
});
|
||
let resolveDatabases: ((value: { success: true; data: unknown[] }) => void) | undefined;
|
||
backendApp.DBGetDatabases.mockReset();
|
||
backendApp.DBGetDatabases.mockReturnValue(
|
||
new Promise((resolve) => {
|
||
resolveDatabases = resolve;
|
||
}),
|
||
);
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("elasticsearch", { port: 9200 })}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
await act(async () => {
|
||
findButton(renderer!, "测试连接").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).toContain("连接成功");
|
||
expect(findButton(renderer!, "测试连接").props.disabled).toBe(false);
|
||
expect(findButton(renderer!, "保存").props.disabled).toBe(false);
|
||
expect(backendApp.DBGetDatabases).toHaveBeenCalledTimes(1);
|
||
|
||
await act(async () => {
|
||
resolveDatabases?.({ success: true, data: [] });
|
||
await flushConnectionTestTick();
|
||
});
|
||
});
|
||
|
||
it("does not let a stale validation run restart loading after the modal reopens", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("zh-CN");
|
||
let resolveValidation: (() => void) | undefined;
|
||
mockValidateFields = () =>
|
||
new Promise<void>((resolve) => {
|
||
resolveValidation = resolve;
|
||
});
|
||
backendApp.TestConnection.mockReset();
|
||
backendApp.TestConnection.mockRejectedValue(new Error("stale validation failure"));
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
const connection = initialConnection("elasticsearch", { port: 9200 });
|
||
const onClose = vi.fn();
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal open onClose={onClose} initialValues={connection} />,
|
||
);
|
||
});
|
||
await act(async () => {
|
||
findButton(renderer!, "测试连接").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal open={false} onClose={onClose} initialValues={connection} />,
|
||
);
|
||
});
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal open onClose={onClose} initialValues={connection} />,
|
||
);
|
||
});
|
||
await act(async () => {
|
||
resolveValidation?.();
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
expect(backendApp.TestConnection).not.toHaveBeenCalled();
|
||
expect(textContent(renderer!.toJSON())).not.toContain("stale validation failure");
|
||
expect(findButton(renderer!, "测试连接").props.disabled).toBe(false);
|
||
expect(findButton(renderer!, "保存").props.disabled).toBe(false);
|
||
});
|
||
|
||
it("ignores a stale connection-test rejection after the modal reopens", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("zh-CN");
|
||
let rejectConnection: ((reason?: unknown) => void) | undefined;
|
||
backendApp.TestConnection.mockReset();
|
||
backendApp.TestConnection.mockReturnValue(
|
||
new Promise((_, reject) => {
|
||
rejectConnection = reject;
|
||
}),
|
||
);
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
const connection = initialConnection("elasticsearch", { port: 9200 });
|
||
const onClose = vi.fn();
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal open onClose={onClose} initialValues={connection} />,
|
||
);
|
||
});
|
||
await act(async () => {
|
||
findButton(renderer!, "测试连接").props.onClick();
|
||
await flushConnectionTestTick();
|
||
});
|
||
expect(backendApp.TestConnection).toHaveBeenCalledTimes(1);
|
||
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal open={false} onClose={onClose} initialValues={connection} />,
|
||
);
|
||
});
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal open onClose={onClose} initialValues={connection} />,
|
||
);
|
||
});
|
||
await act(async () => {
|
||
rejectConnection?.(new Error("stale connection failure"));
|
||
await flushConnectionTestTick();
|
||
});
|
||
|
||
expect(textContent(renderer!.toJSON())).not.toContain("stale connection failure");
|
||
expect(findButton(renderer!, "测试连接").props.disabled).toBe(false);
|
||
expect(findButton(renderer!, "保存").props.disabled).toBe(false);
|
||
});
|
||
|
||
it("renders English data source groups and hints for the remaining step one copy", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("en-US");
|
||
mockFormValues = {
|
||
jvmDiagnosticEnabled: true,
|
||
jvmDiagnosticTransport: "agent-bridge",
|
||
};
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(<ConnectionModal open onClose={vi.fn()} />);
|
||
});
|
||
|
||
let pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Relational databases");
|
||
expect(pageText).toContain("Domestic databases");
|
||
expect(pageText).toContain("NoSQL databases");
|
||
expect(pageText).toContain("Time-series databases");
|
||
expect(pageText).toContain("Other");
|
||
expect(pageText).toContain("Local file connection");
|
||
expect(pageText).toContain("Standard connection configuration");
|
||
|
||
await act(async () => {
|
||
findClickableByAnyText(renderer!, ["Other"]).props.onClick();
|
||
});
|
||
pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("JVM runtime");
|
||
expect(pageText).not.toContain("JVM Runtime");
|
||
expect(pageText).toContain("Custom");
|
||
expect(pageText).not.toContain("Custom (自定义)");
|
||
});
|
||
|
||
it("renders English custom driver DSN copy after the module was loaded in another language", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("zh-CN");
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
setCurrentLanguage("en-US");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("custom", {
|
||
driver: "mysql",
|
||
dsn: "user:pass@tcp(localhost:3306)/dbname",
|
||
})}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
const pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Driver Name");
|
||
expect(pageText).toContain("Connection string (DSN)");
|
||
expect(pageText).toContain(getCustomConnectionDriverHelp("en-US"));
|
||
});
|
||
|
||
it("renders English JVM fields and diagnostic transport copy", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("en-US");
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("jvm", {
|
||
port: 9010,
|
||
jvm: {
|
||
allowedModes: ["jmx", "endpoint", "agent"],
|
||
preferredMode: "jmx",
|
||
diagnostic: {
|
||
enabled: true,
|
||
transport: "agent-bridge",
|
||
},
|
||
},
|
||
})}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
const pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("JMX host override");
|
||
expect(pageText).toContain("JMX port");
|
||
expect(pageText).toContain("JMX username");
|
||
expect(pageText).toContain("Endpoint address");
|
||
expect(pageText).toContain("Agent address");
|
||
expect(pageText).toContain("Diagnostic transport");
|
||
expect(pageText).toContain("Agent Bridge");
|
||
expect(pageText).toContain("Bridge diagnostic commands through GoNavi Agent.");
|
||
expect(pageText).toContain("Observe commands");
|
||
expect(pageText).toContain(
|
||
"Read-only troubleshooting commands such as thread, dashboard, and jvm.",
|
||
);
|
||
expect(pageText).toContain("Trace commands");
|
||
expect(pageText).toContain(
|
||
"Commands such as trace and watch that add extra overhead to the target.",
|
||
);
|
||
expect(pageText).toContain("High-risk commands");
|
||
expect(pageText).toContain(
|
||
"Commands that may change runtime state or cause noticeable performance impact.",
|
||
);
|
||
});
|
||
|
||
it("renders English protocol and database service fields", async () => {
|
||
storeState.appearance.uiVersion = "legacy";
|
||
setCurrentLanguage("en-US");
|
||
const { default: ConnectionModal } = await import("./ConnectionModal");
|
||
|
||
let renderer: ReactTestRenderer;
|
||
await act(async () => {
|
||
renderer = create(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("oceanbase", {
|
||
oceanBaseProtocol: "mysql",
|
||
})}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
let pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("OceanBase protocol");
|
||
expect(pageText).toContain("Choose MySQL for MySQL tenants and Oracle for Oracle tenants.");
|
||
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("postgres", {
|
||
port: 5432,
|
||
database: "appdb",
|
||
})}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Default connection database");
|
||
|
||
await act(async () => {
|
||
renderer!.update(
|
||
<ConnectionModal
|
||
open
|
||
onClose={vi.fn()}
|
||
initialValues={initialConnection("oracle", {
|
||
port: 1521,
|
||
database: "ORCLPDB1",
|
||
})}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
pageText = textContent(renderer!.toJSON());
|
||
expect(pageText).toContain("Service Name");
|
||
});
|
||
});
|