mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 06:30:45 +08:00
Add profile-scoped npm updates for Desktop and Web, protect source installations, and keep restarts manual. Cover stale status recovery, request races, and dialog focus with regression tests. Verified with npm run check (1626 tests) and isolated Desktop/Web npm installation flows. Refs #61
885 lines
41 KiB
JavaScript
885 lines
41 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile, readdir } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
|
|
import { transform } from 'esbuild';
|
|
import React from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
|
|
import {
|
|
apply as applyClient,
|
|
IM_PLUGIN_VERSION,
|
|
IMSettingsTab,
|
|
inject as clientInject,
|
|
} from '../plugin-src/client/index.js';
|
|
import { CredentialBindingPanel } from '../plugin-src/client/credential-binding.js';
|
|
import { ChannelListHeading } from '../plugin-src/client/channel-card-meta.js';
|
|
import { DINGTALK_ENDPOINTS } from '../plugin-src/client/channels/dingtalk/api.js';
|
|
import {
|
|
AccountCard as DingtalkAccountCard,
|
|
DingtalkSettingsTab,
|
|
} from '../plugin-src/client/channels/dingtalk/index.js';
|
|
import {
|
|
BotCard as FeishuBotCard,
|
|
FeishuSettingsTab,
|
|
} from '../plugin-src/client/channels/feishu/index.js';
|
|
import {
|
|
AccountCard as WeixinAccountCard,
|
|
WeixinSettingsTab,
|
|
} from '../plugin-src/client/channels/weixin/index.js';
|
|
import {
|
|
AccountCard as WecomAccountCard,
|
|
WecomSettingsTab,
|
|
} from '../plugin-src/client/channels/wecom/index.js';
|
|
import {
|
|
AccountCard as QqAccountCard,
|
|
QqSettingsTab,
|
|
} from '../plugin-src/client/channels/qq/index.js';
|
|
import {
|
|
SlackAccountCard,
|
|
SlackSettingsTab,
|
|
} from '../plugin-src/client/channels/slack/index.js';
|
|
import {
|
|
TelegramAccountCard,
|
|
TelegramSettingsTab,
|
|
} from '../plugin-src/client/channels/telegram/index.js';
|
|
import {
|
|
DiscordAccountCard,
|
|
DiscordSettingsTab,
|
|
} from '../plugin-src/client/channels/discord/index.js';
|
|
import {
|
|
WhatsappAccountCard,
|
|
WhatsappSettingsTab,
|
|
} from '../plugin-src/client/channels/whatsapp/index.js';
|
|
import {
|
|
en,
|
|
IM_LOCALE_NAMESPACE,
|
|
localizeText,
|
|
setImTranslator,
|
|
zh,
|
|
} from '../plugin-src/client/i18n.js';
|
|
|
|
const STYLES_URL = new URL('../plugin-src/client/styles.js', import.meta.url);
|
|
const FEISHU_STYLES_URL = new URL(
|
|
'../plugin-src/client/channels/feishu/styles.js',
|
|
import.meta.url,
|
|
);
|
|
const WEIXIN_STYLES_URL = new URL(
|
|
'../plugin-src/client/channels/weixin/styles.js',
|
|
import.meta.url,
|
|
);
|
|
const DINGTALK_STYLES_URL = new URL(
|
|
'../plugin-src/client/channels/dingtalk/styles.js',
|
|
import.meta.url,
|
|
);
|
|
const WECOM_STYLES_URL = new URL(
|
|
'../plugin-src/client/channels/wecom/styles.js',
|
|
import.meta.url,
|
|
);
|
|
const FEISHU_SOURCE_URL = new URL(
|
|
'../plugin-src/client/channels/feishu/index.js',
|
|
import.meta.url,
|
|
);
|
|
const WEIXIN_SOURCE_URL = new URL(
|
|
'../plugin-src/client/channels/weixin/index.js',
|
|
import.meta.url,
|
|
);
|
|
const CLIENT_BUNDLE_URL = new URL('../lib/client.js', import.meta.url);
|
|
const CLIENT_SOURCE_DIRECTORY_URL = new URL('../plugin-src/client/', import.meta.url);
|
|
const DINGTALK_CLIENT_SOURCE_URL = new URL(
|
|
'../plugin-src/client/channels/dingtalk/index.js',
|
|
import.meta.url,
|
|
);
|
|
const WECOM_SOURCE_URL = new URL(
|
|
'../plugin-src/client/channels/wecom/index.js',
|
|
import.meta.url,
|
|
);
|
|
const QQ_SOURCE_URL = new URL(
|
|
'../plugin-src/client/channels/qq/index.js',
|
|
import.meta.url,
|
|
);
|
|
|
|
test('IM settings renders nine IM channels plus the AI Office connector', async () => {
|
|
const { default: packageMetadata } = await import('../package.json', {
|
|
with: { type: 'json' },
|
|
});
|
|
const { version: packageVersion } = packageMetadata;
|
|
const styles = await readFile(STYLES_URL, 'utf8');
|
|
const markup = renderToStaticMarkup(React.createElement(IMSettingsTab, {
|
|
feishuRpcCall: async () => ({ ok: true, value: {} }),
|
|
weixinRpcCall: async () => ({ ok: true, value: {} }),
|
|
dingtalkRpcCall: async () => ({ ok: true, value: {} }),
|
|
wecomRpcCall: async () => ({ ok: true, value: {} }),
|
|
qqRpcCall: async () => ({ ok: true, value: {} }),
|
|
slackRpcCall: async () => ({ ok: true, value: {} }),
|
|
telegramRpcCall: async () => ({ ok: true, value: {} }),
|
|
discordRpcCall: async () => ({ ok: true, value: {} }),
|
|
whatsappRpcCall: async () => ({ ok: true, value: {} }),
|
|
officeRpcCall: async () => ({ ok: true, value: {} }),
|
|
}));
|
|
|
|
assert.match(markup, /IM机器人/);
|
|
assert.match(markup, /让 DeepSeek Harness 触手可及/);
|
|
assert.match(markup, /class="dim-brand"/);
|
|
assert.equal(IM_PLUGIN_VERSION, packageVersion);
|
|
assert.match(markup, new RegExp(
|
|
`<div class="dim-brandHeading"><strong class="dim-brandName">DSH-IM<\\/strong><span class="dim-brandVersion">v${packageVersion.replaceAll('.', '\\.')}<\\/span><\\/div>`,
|
|
));
|
|
assert.doesNotMatch(markup, /dim-versionTooltip|当前版本/);
|
|
assert.doesNotMatch(markup, /dim-brandLogo|<img/);
|
|
assert.match(markup, /href="https:\/\/github\.com\/xmanrui\/dsh-im"/);
|
|
assert.match(markup, /target="_blank"/);
|
|
assert.match(markup, /rel="noopener noreferrer"/);
|
|
assert.match(markup, /aria-label="dsh-im GitHub"/);
|
|
assert.match(markup, /dim-updateTrigger[^>]*aria-haspopup="dialog"[^>]*>检查更新</);
|
|
assert.ok(markup.indexOf('dim-updateTrigger') < markup.indexOf('dim-githubAction'));
|
|
assert.match(markup, /aria-describedby="[^"]+"/);
|
|
assert.match(markup, /role="tooltip"[^>]*>帮助与反馈 · 前往 GitHub</);
|
|
assert.match(styles, /\.dim-title \{[^}]*margin: 0 0 18px;/);
|
|
assert.match(styles, /\.dim-title p \{[^}]*color: var\(--dsw-alias-label-secondary, #646a73\);[^}]*font-size: 12px;[^}]*font-weight: 500;/);
|
|
assert.match(styles, /\.dim-brand \{[^}]*display: flex;[^}]*flex-direction: column;[^}]*align-items: flex-start;[^}]*gap: 1px;/);
|
|
assert.match(styles, /\.dim-brandHeading \{[^}]*display: flex;[^}]*align-items: baseline;[^}]*gap: 8px;[^}]*white-space: nowrap;/);
|
|
assert.match(styles, /\.dim-brandName \{[^}]*font-size: 20px;[^}]*font-weight: 800;[^}]*letter-spacing: \.04em;/);
|
|
assert.match(styles, /\.dim-brandVersion \{[^}]*color: var\(--dsw-alias-label-tertiary, #8f959e\);[^}]*font: 500 10px\/16px[^}]*letter-spacing: 0;/);
|
|
assert.doesNotMatch(styles, /dim-versionTooltip|\.dim-brand:focus-visible/);
|
|
assert.doesNotMatch(styles, /\.dim-brandLogo/);
|
|
assert.match(styles, /\.dim-githubLink \{[^}]*border: 1px solid var\(--dsw-alias-border-l2, #dfe1e5\);[^}]*text-decoration: none;/);
|
|
assert.match(styles, /\.dim-githubTooltip \{[^}]*top: calc\(100% \+ 8px\);[^}]*transform: translateY\(-3px\);/);
|
|
assert.match(styles, /\.dim-githubAction:hover \.dim-githubTooltip, \.dim-githubAction:focus-within \.dim-githubTooltip \{[^}]*opacity: 1;[^}]*visibility: visible;/);
|
|
assert.doesNotMatch(markup, /\d+ 个渠道|dim-channelCount/);
|
|
assert.match(markup, />微信</);
|
|
assert.match(markup, />飞书</);
|
|
assert.match(markup, />钉钉</);
|
|
assert.match(markup, />企业微信</);
|
|
assert.match(markup, />QQ</);
|
|
assert.match(markup, />Slack</);
|
|
assert.match(markup, />Telegram</);
|
|
assert.match(markup, />Discord</);
|
|
assert.match(markup, />WhatsApp</);
|
|
assert.match(markup, />AI Office<\/strong><small class="dim-channelNote">(实验功能)<\/small>/);
|
|
assert.match(markup, /dim-logoWeixin/);
|
|
assert.match(markup, /dim-logoFeishu/);
|
|
assert.match(markup, /dim-logoDingtalk/);
|
|
assert.match(markup, /dim-logoWecom/);
|
|
assert.match(markup, /dim-logoQq/);
|
|
assert.match(markup, /dim-logoSlack/);
|
|
assert.match(markup, /dim-logoTelegram/);
|
|
assert.match(markup, /dim-logoDiscord/);
|
|
assert.match(markup, /dim-logoWhatsapp/);
|
|
assert.match(markup, /dim-logoOffice/);
|
|
assert.match(styles, /\.dim-logoFeishu svg \{ width: 28px; height: 28px; \}/);
|
|
assert.equal((markup.match(/role="tab"/g) ?? []).length, 10);
|
|
assert.equal((markup.match(/aria-selected="true"/g) ?? []).length, 1);
|
|
assert.doesNotMatch(markup, /role="switch"|type="checkbox"/);
|
|
assert.doesNotMatch(markup, /dim-chevron|扫码绑定<\/small>|扫码接入<\/small>/);
|
|
assert.doesNotMatch(markup, />INSTANT MESSAGING<|>Channel<|>微信设置</);
|
|
});
|
|
|
|
test('all channel styles use the current Harness theme tokens', async () => {
|
|
const styles = (await Promise.all([
|
|
readFile(STYLES_URL, 'utf8'),
|
|
readFile(FEISHU_STYLES_URL, 'utf8'),
|
|
readFile(WEIXIN_STYLES_URL, 'utf8'),
|
|
readFile(DINGTALK_STYLES_URL, 'utf8'),
|
|
readFile(WECOM_STYLES_URL, 'utf8'),
|
|
])).join('\n');
|
|
|
|
assert.doesNotMatch(
|
|
styles,
|
|
/--dsw-alias-(?:bg-body|line-border|line-divider|fill-secondary|fill-tertiary|state-warning-primary)/,
|
|
);
|
|
assert.match(styles, /--dsw-alias-bg-layer-1/);
|
|
assert.match(styles, /--dsw-alias-bg-module-platform/);
|
|
assert.match(styles, /--dsw-alias-interactive-bg-hover/);
|
|
assert.match(styles, /--dsw-alias-border-l1/);
|
|
assert.match(styles, /--dsw-alias-border-l2/);
|
|
assert.match(styles, /--dim-blue: var\(--dsw-alias-state-business-primary, #3370ff\)/);
|
|
assert.match(
|
|
styles,
|
|
/\.dim-channel\[aria-selected="true"\][^}]*var\(--dsw-alias-bg-layer-3/,
|
|
);
|
|
assert.match(
|
|
styles,
|
|
/\.dim-panel \.dim-qrExpired[^}]*--dsw-static-neutral-bluish-1000/,
|
|
);
|
|
});
|
|
|
|
test('shared QR cards stay square and stack within the narrow combined-channel panel', async () => {
|
|
const styles = await readFile(STYLES_URL, 'utf8');
|
|
assert.match(styles, /\.dim-panel \{ min-width: 0; container-type: inline-size; \}/);
|
|
assert.match(styles, /\.dim-panel \.dim-qrFrame \{[^}]*width: min\(270px, 100%\);[^}]*height: auto;[^}]*aspect-ratio: 1;/);
|
|
assert.match(
|
|
styles,
|
|
/@container \(max-width: 680px\)[\s\S]*\.dim-panel \.ddt-qrLayout \{ grid-template-columns: minmax\(0, 1fr\); justify-items: center;/,
|
|
);
|
|
assert.match(styles, /\.dim-panel \.ddt-qrFrame, \.dim-panel \.ddt-countdown \{ width: min\(270px, 100%\); \}/);
|
|
assert.match(styles, /\.dim-panel \.ddt-qrColumn \{ width: 100%; min-width: 0; \}/);
|
|
assert.match(styles, /\.dim-panel \.ddt-qrCopy \{ width: 100%; min-width: 0; overflow-wrap: anywhere; \}/);
|
|
});
|
|
|
|
test('Feishu bot cards place the application identifier under the bot name', async () => {
|
|
const styles = await readFile(FEISHU_STYLES_URL, 'utf8');
|
|
const markup = renderToStaticMarkup(React.createElement(FeishuBotCard, {
|
|
connection: {
|
|
botId: 'bot-feishu-card',
|
|
state: 'connected',
|
|
connected: true,
|
|
bot: {
|
|
name: '今天是牢梁',
|
|
appIdMasked: 'cli_aaf4••••1234',
|
|
domain: 'feishu',
|
|
avatarUrl: 'https://example.com/custom-bot-avatar.png',
|
|
},
|
|
health: {
|
|
summary: '长连接运行正常',
|
|
lastCheckedAt: '2026-08-15T07:30:49.000Z',
|
|
},
|
|
},
|
|
onReconnect() {},
|
|
onRequestRemove() {},
|
|
onConfirmRemove() {},
|
|
onCancelRemove() {},
|
|
}));
|
|
|
|
assert.match(markup, /<h3[^>]*>今天是牢梁<\/h3><p[^>]*>cli_aaf4••••1234<\/p>/);
|
|
assert.match(markup, /data-im-channel-logo="feishu"/);
|
|
assert.match(markup, /class="bxf-card bxf-botCard dim-botCard"/);
|
|
assert.match(markup, /class="bxf-healthPill dim-botHealth"/);
|
|
assert.match(markup, /<button[^>]*aria-label="检查连接今天是牢梁"[^>]*><span>检查连接<\/span><\/button>/);
|
|
assert.match(markup, /class="bxf-repairAction"[^]*role="tooltip"/);
|
|
assert.match(markup, /card\.action\.trigger[^]*im:message:readonly[^]*im:resource/);
|
|
assert.match(markup, /class="bxf-connectedFooter dim-cardFooter"/);
|
|
assert.doesNotMatch(markup, /dim-cardSummary|长连接运行正常/);
|
|
assert.equal((markup.match(/dim-cardAction(?: |")/g) ?? []).length, 3);
|
|
assert.doesNotMatch(markup, /连接状态:|bxf-divider/);
|
|
assert.doesNotMatch(markup, /custom-bot-avatar/);
|
|
assert.match(markup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
|
|
assert.doesNotMatch(markup, /消息通道|dim-botMetric/);
|
|
assert.match(markup, /class="dim-presetSelect"/);
|
|
assert.doesNotMatch(markup, />应用标识<|>飞书机器人</);
|
|
assert.doesNotMatch(styles, /\.bxf-statusGrid|\.bxf-metric/);
|
|
assert.match(styles, /\.bxf-repairAction:hover \.bxf-repairTooltip,[^]*\.bxf-repairAction:focus-within \.bxf-repairTooltip \{[^}]*visibility: visible;/);
|
|
});
|
|
|
|
test('Feishu keeps its heading controls on one row without a plus icon', async () => {
|
|
const styles = await readFile(FEISHU_STYLES_URL, 'utf8');
|
|
const markup = renderToStaticMarkup(React.createElement(FeishuSettingsTab, {
|
|
rpcCall: async () => ({ ok: true, value: {} }),
|
|
}));
|
|
|
|
assert.match(markup, /aria-label="扫码接入飞书机器人"/);
|
|
assert.match(markup, /class="dim-actionIcon"[^]*<span>扫码接入机器人<\/span>/);
|
|
assert.doesNotMatch(markup, />添加机器人</);
|
|
assert.match(styles, /\.bxf-headingTools \{[^}]*justify-content: space-between;[^}]*flex-wrap: nowrap;/);
|
|
assert.match(styles, /@container \(max-width: 620px\)[^]*\.bxf-headingTools \{ gap: 6px; \}/);
|
|
assert.doesNotMatch(styles, /\.bxf-headingTools \.bxf-button \{ margin-left: auto; \}/);
|
|
});
|
|
|
|
test('credential binding is a distinct secondary action beside QR binding in four channels', async () => {
|
|
const settings = [
|
|
['飞书', FeishuSettingsTab],
|
|
['QQ', QqSettingsTab],
|
|
['钉钉', DingtalkSettingsTab],
|
|
['企业微信', WecomSettingsTab],
|
|
];
|
|
for (const [channel, Component] of settings) {
|
|
const markup = renderToStaticMarkup(React.createElement(Component, {
|
|
rpcCall: async () => ({ ok: true, value: {} }),
|
|
}));
|
|
const scanIndex = markup.indexOf('dim-scanButton');
|
|
const credentialIndex = markup.indexOf('dim-credentialButton');
|
|
assert.ok(scanIndex >= 0, `${channel} should render a QR button`);
|
|
assert.ok(credentialIndex > scanIndex, `${channel} should place credential binding after QR binding`);
|
|
assert.match(markup, /data-kind="credential"/);
|
|
const credentialMarkup = markup.slice(credentialIndex, markup.indexOf('</button>', credentialIndex));
|
|
assert.match(credentialMarkup, /dim-actionIcon/);
|
|
assert.match(credentialMarkup, /手动接入/);
|
|
}
|
|
|
|
const styles = await readFile(STYLES_URL, 'utf8');
|
|
assert.match(styles, /\.dim-panel \.dim-bindActions \{[^}]*flex-wrap: nowrap;/);
|
|
assert.match(styles, /\.dim-panel \.dim-credentialButton \{[^}]*border: 1px solid #86909c;[^}]*background: var\(--dsw-alias-bg-layer-1, #fff\)/);
|
|
assert.match(styles, /\.dim-panel \.dim-actionIcon \{[^}]*flex: 0 0 15px;/);
|
|
assert.doesNotMatch(styles, /\.dim-panel \.dim-credentialPanel \{[^}]*border-left:/);
|
|
});
|
|
|
|
test('credential form stays compact while using a protected password input', () => {
|
|
const markup = renderToStaticMarkup(React.createElement(CredentialBindingPanel, {
|
|
channel: '企业微信',
|
|
identityLabel: 'Bot ID',
|
|
identityPlaceholder: '填写 Bot ID',
|
|
secretLabel: 'Secret',
|
|
secretPlaceholder: '填写 Secret',
|
|
onSubmit() {},
|
|
onCancel() {},
|
|
}));
|
|
assert.match(markup, />Bot ID</);
|
|
assert.match(markup, /type="password"/);
|
|
assert.match(markup, /autoComplete="new-password"/i);
|
|
assert.match(markup, />手动接入企业微信机器人</);
|
|
assert.doesNotMatch(markup, /已有机器人应用|Harness 会校验凭据|可见范围|受保护的凭据存储/);
|
|
assert.doesNotMatch(markup, /value="[^"]+"/);
|
|
});
|
|
|
|
test('scan actions align left while online totals align right in every channel', async () => {
|
|
const [imStyles, feishuStyles, weixinStyles, dingtalkStyles, wecomStyles, feishuSource, weixinSource, dingtalkSource, wecomSource] = await Promise.all([
|
|
readFile(STYLES_URL, 'utf8'),
|
|
readFile(FEISHU_STYLES_URL, 'utf8'),
|
|
readFile(WEIXIN_STYLES_URL, 'utf8'),
|
|
readFile(DINGTALK_STYLES_URL, 'utf8'),
|
|
readFile(WECOM_STYLES_URL, 'utf8'),
|
|
readFile(FEISHU_SOURCE_URL, 'utf8'),
|
|
readFile(WEIXIN_SOURCE_URL, 'utf8'),
|
|
readFile(DINGTALK_CLIENT_SOURCE_URL, 'utf8'),
|
|
readFile(WECOM_SOURCE_URL, 'utf8'),
|
|
]);
|
|
|
|
assert.match(imStyles, /\.dim-panel \.bxf-headingTools, \.dim-panel \.dxw-tools, \.dim-panel \.ddt-tools \{[^}]*display: grid;[^}]*grid-template-columns: minmax\(0, 1fr\) max-content;[^}]*justify-content: stretch;/);
|
|
assert.match(imStyles, /\.dim-panel \.dim-bindActions > button \{[^}]*min-width: 0;/);
|
|
assert.match(imStyles, /\.dim-panel \.bxf-headingTools \.dim-scanButton,[^}]*justify-self: start;/);
|
|
assert.match(imStyles, /\.dim-panel \.bxf-headingTools \.dim-onlineBadge,[^}]*justify-self: end;/);
|
|
assert.match(feishuStyles, /\.bxf-headingTools \{[^}]*justify-content: space-between;/);
|
|
assert.match(weixinStyles, /\.dxw-tools \{[^}]*justify-content: space-between;/);
|
|
assert.match(dingtalkStyles, /\.ddt-tools \{[^}]*justify-content: space-between;/);
|
|
assert.match(wecomStyles, /\.dwecom-page/);
|
|
|
|
const headingSource = (source) => source.slice(
|
|
source.indexOf('function Heading'),
|
|
source.indexOf('function LoadingView'),
|
|
);
|
|
const feishuHeading = headingSource(feishuSource);
|
|
const weixinHeading = headingSource(weixinSource);
|
|
const dingtalkHeading = headingSource(dingtalkSource);
|
|
const wecomHeading = headingSource(wecomSource);
|
|
assert.ok(feishuHeading.indexOf('扫码接入机器人') < feishuHeading.indexOf('bxf-totalBadge'));
|
|
assert.ok(weixinHeading.indexOf('扫码接入机器人') < weixinHeading.indexOf('dxw-badge'));
|
|
assert.ok(dingtalkHeading.indexOf('扫码接入机器人') < dingtalkHeading.indexOf('ddt-badge'));
|
|
assert.ok(wecomHeading.indexOf('扫码接入机器人') < wecomHeading.indexOf('ddt-badge'));
|
|
|
|
for (const heading of [feishuHeading, weixinHeading, dingtalkHeading, wecomHeading]) {
|
|
assert.match(heading, /dim-scanButton/);
|
|
assert.match(heading, /dim-onlineBadge/);
|
|
}
|
|
assert.doesNotMatch(weixinHeading, /dxw-dot/);
|
|
assert.doesNotMatch(dingtalkHeading, /ddt-dot/);
|
|
assert.match(imStyles, /\.dim-panel \.bxf-headingTools \.dim-scanButton,[^}]*border: 1px solid #1677ff;[^}]*border-radius: 8px;[^}]*background: #1677ff;[^}]*box-shadow: none;/);
|
|
assert.match(imStyles, /\.dim-panel \.bxf-headingTools \.dim-onlineBadge,[^}]*border-radius: 999px;[^}]*background: var\(--dsw-alias-bg-module-platform, #f2f3f5\);[^}]*font-size: 12px;/);
|
|
});
|
|
|
|
test('channel headings omit the redundant local credential badge', () => {
|
|
const components = [FeishuSettingsTab, WeixinSettingsTab, DingtalkSettingsTab, WecomSettingsTab];
|
|
|
|
for (const Component of components) {
|
|
const markup = renderToStaticMarkup(React.createElement(Component, {
|
|
rpcCall: async () => ({ ok: true, value: {} }),
|
|
}));
|
|
assert.doesNotMatch(markup, /凭据仅保存在本机/);
|
|
}
|
|
});
|
|
|
|
test('bot list headings omit the total already shown by the online badge', async () => {
|
|
const sources = await Promise.all([
|
|
FEISHU_SOURCE_URL,
|
|
WEIXIN_SOURCE_URL,
|
|
DINGTALK_CLIENT_SOURCE_URL,
|
|
WECOM_SOURCE_URL,
|
|
QQ_SOURCE_URL,
|
|
].map((url) => readFile(url, 'utf8')));
|
|
|
|
for (const source of sources) {
|
|
assert.doesNotMatch(source, /length} 个/);
|
|
assert.match(source, /ChannelListHeading/);
|
|
}
|
|
});
|
|
|
|
test('channel connection details live in an accessible heading tooltip', async () => {
|
|
const styles = await readFile(STYLES_URL, 'utf8');
|
|
const markup = renderToStaticMarkup(React.createElement(ChannelListHeading, {
|
|
className: 'dxw-listHeading',
|
|
title: '已接入的微信账号',
|
|
connectionLabel: 'iLink 长轮询',
|
|
}));
|
|
|
|
assert.match(markup, /<h3>已接入的微信账号<\/h3>/);
|
|
assert.match(markup, /aria-label="查看消息通道说明"/);
|
|
assert.match(markup, /role="tooltip"><span>消息通道<\/span><strong>iLink 长轮询<\/strong>/);
|
|
assert.match(styles, /\.dim-panel \.dim-channelHelp:hover \.dim-channelTooltip, \.dim-panel \.dim-channelHelp:focus-within \.dim-channelTooltip \{[^}]*opacity: 1;[^}]*visibility: visible;/);
|
|
});
|
|
|
|
test('all channel settings states use the DingTalk page treatment', async () => {
|
|
const [styles, feishuSource, weixinSource, dingtalkSource, wecomSource] = await Promise.all([
|
|
readFile(STYLES_URL, 'utf8'),
|
|
readFile(FEISHU_SOURCE_URL, 'utf8'),
|
|
readFile(WEIXIN_SOURCE_URL, 'utf8'),
|
|
readFile(DINGTALK_CLIENT_SOURCE_URL, 'utf8'),
|
|
readFile(WECOM_SOURCE_URL, 'utf8'),
|
|
]);
|
|
|
|
for (const Component of [FeishuSettingsTab, WeixinSettingsTab, DingtalkSettingsTab, WecomSettingsTab]) {
|
|
const markup = renderToStaticMarkup(React.createElement(Component, {
|
|
rpcCall: async () => ({ ok: true, value: {} }),
|
|
}));
|
|
assert.match(markup, /dim-channelPage/);
|
|
assert.match(markup, /dim-surfaceCard dim-loadingView/);
|
|
assert.match(markup, /dim-spinner/);
|
|
}
|
|
|
|
for (const source of [feishuSource, weixinSource, dingtalkSource, wecomSource]) {
|
|
for (const className of [
|
|
'dim-channelPage',
|
|
'dim-surfaceCard',
|
|
'dim-loadingView',
|
|
'dim-emptyView',
|
|
'dim-qrLayout',
|
|
'dim-inlineError',
|
|
'dim-confirm',
|
|
]) {
|
|
assert.match(source, new RegExp(className));
|
|
}
|
|
}
|
|
|
|
assert.match(styles, /\.dim-panel \.dim-channelPage \{[^}]*flex-direction: column;[^}]*gap: 12px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-listHeading \{[^}]*margin: 0 0 6px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-botList \{[^}]*gap: 8px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-surfaceCard \{[^}]*border-radius: 14px;[^}]*box-shadow: 0 1px 2px/);
|
|
assert.match(styles, /\.dim-panel \.dim-loadingView \{[^}]*padding: 38px;[^}]*text-align: center;/);
|
|
assert.match(styles, /\.dim-panel \.dim-emptyView \{[^}]*grid-template-columns: minmax\(0, 1fr\) 180px;[^}]*gap: 30px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-qrLayout \{[^}]*grid-template-columns: 300px minmax\(0, 1fr\);[^}]*gap: 34px;[^}]*align-items: start;/);
|
|
assert.match(styles, /\.dim-panel \.dim-viewActions \.bxf-button,[^}]*min-height: 34px;[^}]*border-radius: 8px;[^}]*font-size: 13px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-inlineError \{[^}]*padding: 22px;[^}]*background:/);
|
|
assert.match(styles, /\.dim-panel \.dim-confirm \{[^}]*padding: 18px 24px;[^}]*border-top: 1px solid/);
|
|
});
|
|
|
|
test('bot cards reuse the same channel brand logos as the channel rail', () => {
|
|
const railMarkup = renderToStaticMarkup(React.createElement(IMSettingsTab, {
|
|
feishuRpcCall: async () => ({ ok: true, value: {} }),
|
|
weixinRpcCall: async () => ({ ok: true, value: {} }),
|
|
dingtalkRpcCall: async () => ({ ok: true, value: {} }),
|
|
wecomRpcCall: async () => ({ ok: true, value: {} }),
|
|
}));
|
|
const accountMarkup = renderToStaticMarkup(React.createElement(WeixinAccountCard, {
|
|
account: {
|
|
botId: 'bot-weixin-card',
|
|
state: 'connected',
|
|
connected: true,
|
|
bot: { name: '微信机器人', accountIdMasked: 'wxid••••1234' },
|
|
stats: { messagesReceived: 2, messagesReplied: 2 },
|
|
health: { summary: '长轮询运行正常', lastCheckedAt: '2026-08-15T07:30:49.000Z' },
|
|
},
|
|
onReconnect() {},
|
|
onRequestRemove() {},
|
|
onConfirmRemove() {},
|
|
onCancelRemove() {},
|
|
}));
|
|
|
|
assert.match(railMarkup, /data-im-channel-logo="weixin"/);
|
|
assert.match(railMarkup, /data-im-channel-logo="feishu"/);
|
|
assert.match(railMarkup, /data-im-channel-logo="wecom"/);
|
|
assert.match(accountMarkup, /class="dxw-card dim-botCard"/);
|
|
assert.match(accountMarkup, /class="dxw-avatar dim-botAvatar"[^]*data-im-channel-logo="weixin"/);
|
|
assert.match(accountMarkup, /class="dxw-health dim-botHealth"/);
|
|
assert.match(accountMarkup, /class="dxw-accountFooter dim-cardFooter"/);
|
|
assert.match(accountMarkup, /class="dim-presetSelect"/);
|
|
assert.doesNotMatch(accountMarkup, /dim-cardSummary|微信消息长轮询运行正常/);
|
|
assert.equal((accountMarkup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
|
|
assert.match(accountMarkup, /class="dim-botHealthGroup"[^]*class="dim-lastChecked"><span>最近检查<\/span>/);
|
|
assert.doesNotMatch(accountMarkup, /消息通道|dim-botMetric/);
|
|
assert.doesNotMatch(accountMarkup, /收到 \/ 回复/);
|
|
});
|
|
|
|
test('Enterprise WeChat cards reuse the rail logo and compact action treatment', () => {
|
|
const markup = renderToStaticMarkup(React.createElement(WecomAccountCard, {
|
|
account: {
|
|
botId: 'wecom-card', state: 'connected', connected: true,
|
|
bot: { name: '企业微信机器人', appIdMasked: 'bot••••001' },
|
|
health: { summary: '企业微信 WebSocket 长连接运行正常', lastCheckedAt: Date.now() },
|
|
},
|
|
onReconnect() {}, onRequestRemove() {}, onConfirmRemove() {}, onCancelRemove() {},
|
|
}));
|
|
assert.match(markup, /data-im-channel-logo="wecom"/);
|
|
assert.equal((markup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
|
|
assert.match(markup, /class="dim-lastChecked"><span>最近检查<\/span>/);
|
|
assert.doesNotMatch(markup, /消息通道|dim-botMetric/);
|
|
});
|
|
|
|
test('DingTalk bot cards omit the redundant received and replied metric', () => {
|
|
const markup = renderToStaticMarkup(React.createElement(DingtalkAccountCard, {
|
|
account: {
|
|
botId: 'bot-dingtalk-card',
|
|
state: 'connected',
|
|
connected: true,
|
|
bot: { name: '钉钉机器人', clientIdMasked: 'ding••••oioy' },
|
|
stats: { messagesReceived: 2, messagesReplied: 2 },
|
|
health: { summary: 'Stream 长连接运行正常', lastCheckedAt: '2026-08-15T07:30:49.000Z' },
|
|
},
|
|
onReconnect() {},
|
|
onRequestRemove() {},
|
|
onConfirmRemove() {},
|
|
onCancelRemove() {},
|
|
}));
|
|
|
|
assert.match(markup, /class="ddt-card dim-botCard"/);
|
|
assert.match(markup, /class="ddt-health dim-botHealth"/);
|
|
assert.match(markup, /class="dim-lastChecked"><span>最近检查<\/span>/);
|
|
assert.match(markup, /class="ddt-accountFooter dim-cardFooter"/);
|
|
assert.doesNotMatch(markup, /dim-cardSummary|Stream 长连接运行正常/);
|
|
assert.equal((markup.match(/dim-cardAction(?: |")/g) ?? []).length, 2);
|
|
assert.doesNotMatch(markup, /消息通道|dim-botMetric/);
|
|
assert.doesNotMatch(markup, /收到 \/ 回复/);
|
|
});
|
|
|
|
test('all IM channel cards keep localized actions visible above full-width feedback', async () => {
|
|
const [imStyles, feishuStyles, weixinStyles, dingtalkStyles] = await Promise.all([
|
|
readFile(STYLES_URL, 'utf8'),
|
|
readFile(FEISHU_STYLES_URL, 'utf8'),
|
|
readFile(WEIXIN_STYLES_URL, 'utf8'),
|
|
readFile(DINGTALK_STYLES_URL, 'utf8'),
|
|
]);
|
|
|
|
assert.match(feishuStyles, /\.bxf-botActions \{[^}]*width: 100%;[^}]*flex-wrap: wrap;/);
|
|
assert.match(weixinStyles, /\.dxw-accountFooter \.dxw-actions \{[^}]*flex-wrap: nowrap;/);
|
|
assert.match(dingtalkStyles, /\.ddt-accountFooter \.ddt-actions \{[^}]*flex-wrap: nowrap;/);
|
|
assert.match(imStyles, /\.dim-panel \.dim-cardFooter \{[^}]*gap: 12px;[^}]*padding-top: 6px;[^}]*border-top: 1px solid/);
|
|
assert.match(imStyles, /\.dim-panel \.dim-cardFooterLayout \{[^}]*width: 100%;[^}]*flex-direction: column;[^}]*align-items: stretch;/);
|
|
assert.match(imStyles, /\.dim-panel \.dim-cardFooterLayout > \.dim-cardActions \{[^}]*align-self: stretch;/);
|
|
assert.match(imStyles, /\.dim-panel \.dim-cardActions \{[^}]*width: 100%;[^}]*justify-content: flex-end;[^}]*flex-wrap: wrap;/);
|
|
assert.match(imStyles, /\.dim-panel \.dim-cardFeedback \{[^}]*width: 100%;[^}]*overflow-wrap: anywhere;[^}]*white-space: normal;/);
|
|
assert.match(imStyles, /\.dim-panel \.dim-cardActions \.dim-cardAction \{[^}]*min-height: 32px;[^}]*border-radius: 8px;[^}]*font-size: 13px;/);
|
|
assert.match(imStyles, /\.dim-panel \.dim-cardActions \.dim-cardAction\[data-kind="danger"\] \{[^}]*#d54941/);
|
|
|
|
const account = {
|
|
botId: 'footer-layout-bot',
|
|
connected: true,
|
|
configured: true,
|
|
state: 'connected',
|
|
groupResponseMode: 'mention',
|
|
bot: {
|
|
name: '布局测试机器人',
|
|
username: 'layout_bot',
|
|
appIdMasked: 'cli_test••••1234',
|
|
accountIdMasked: 'wx_test••••1234',
|
|
clientIdMasked: 'ding_test••••1234',
|
|
idMasked: 'bot_test••••1234',
|
|
},
|
|
health: { summary: '连接运行正常', lastCheckedAt: Date.now() },
|
|
error: null,
|
|
};
|
|
const callbacks = {
|
|
onReconnect() {},
|
|
onRequestRemove() {},
|
|
onConfirmRemove() {},
|
|
onCancelRemove() {},
|
|
};
|
|
const notice = '测试消息已发送,请到对应机器人会话中确认。';
|
|
const cards = [
|
|
['飞书', FeishuBotCard, { connection: account, testNotice: notice }],
|
|
['微信', WeixinAccountCard, { account, feedback: notice }],
|
|
['钉钉', DingtalkAccountCard, { account, feedback: notice }],
|
|
['企业微信', WecomAccountCard, { account, feedback: notice }],
|
|
['QQ', QqAccountCard, { account, feedback: notice }],
|
|
['Slack', SlackAccountCard, { account, testNotice: notice }],
|
|
['Telegram', TelegramAccountCard, { account, testNotice: notice }],
|
|
['Discord', DiscordAccountCard, { account, testNotice: notice }],
|
|
['WhatsApp', WhatsappAccountCard, { account, testNotice: notice }],
|
|
];
|
|
|
|
for (const [channel, Card, props] of cards) {
|
|
const markup = renderToStaticMarkup(React.createElement(Card, { ...callbacks, ...props }));
|
|
assert.match(markup, /class="dim-cardFooterLayout"><div class="[^"]*dim-cardActions[^"]*">[^]*?<\/div><div class="[^"]*dim-cardFeedback[^"]*" role="status"/, channel);
|
|
assert.ok(markup.indexOf('dim-cardActions') < markup.indexOf('dim-cardFeedback'), channel);
|
|
}
|
|
});
|
|
|
|
test('all channel bot cards use the DingTalk card treatment', async () => {
|
|
const styles = await readFile(STYLES_URL, 'utf8');
|
|
|
|
assert.match(styles, /\.dim-panel \.dim-botCard \{[^}]*border-radius: 14px;[^}]*background: var\(--dsw-alias-bg-layer-1, #fff\);[^}]*box-shadow: 0 1px 2px/);
|
|
assert.match(styles, /\.dim-panel \.dim-botCardBody \{[^}]*padding: 12px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-botCardTop \{[^}]*align-items: flex-start;[^}]*gap: 12px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-botAvatar \{[^}]*width: 38px;[^}]*height: 38px;[^}]*border-radius: 11px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-botName h3 \{[^}]*font-size: 15px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-botHealthGroup \{[^}]*display: grid;[^}]*justify-items: end;[^}]*gap: 5px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-botCard \.dim-botHealth \{[^}]*background: transparent;[^}]*font-size: 12px;[^}]*font-weight: 400;/);
|
|
assert.match(styles, /\.dim-panel \.dim-lastChecked \{[^}]*display: inline-flex;[^}]*font-size: 11px;[^}]*white-space: nowrap;/);
|
|
assert.doesNotMatch(styles, /\.dim-panel \.dim-botMetrics|\.dim-panel \.dim-botMetric/);
|
|
});
|
|
|
|
test('bot card status stays in the top-right corner at every responsive breakpoint', async () => {
|
|
const styles = await readFile(STYLES_URL, 'utf8');
|
|
|
|
assert.match(styles, /\.dim-panel \.dim-botCardTop \{[^}]*display: flex;[^}]*align-items: flex-start;[^}]*justify-content: space-between;/);
|
|
assert.match(styles, /\.dim-panel \.dim-botIdentity \{[^}]*min-width: 0;[^}]*flex: 1 1 0;/);
|
|
assert.match(styles, /\.dim-panel \.dim-botHealthGroup \{[^}]*flex: none;[^}]*justify-items: end;/);
|
|
assert.doesNotMatch(styles, /\.dim-panel \.dim-botCardTop \{ flex-direction: column;/);
|
|
assert.doesNotMatch(styles, /\.dim-panel \.dim-botHealthGroup \{ justify-items: start;/);
|
|
});
|
|
|
|
test('bot cards wrap full workspace paths without horizontal scrolling', async () => {
|
|
const styles = await readFile(STYLES_URL, 'utf8');
|
|
|
|
assert.match(styles, /\.dim-panel \.dim-workspace \{[^}]*grid-template-columns: minmax\(0, 1fr\) max-content;[^}]*row-gap: 4px;[^}]*margin-top: 6px;[^}]*padding: 6px 10px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-workspaceHeader \{[^}]*display: contents;/);
|
|
assert.match(styles, /\.dim-panel \.dim-workspacePath \{[^}]*grid-column: 1 \/ -1;[^}]*grid-row: 2;[^}]*overflow: hidden;[^}]*overflow-wrap: anywhere;[^}]*white-space: normal;/);
|
|
assert.doesNotMatch(styles, /\.dim-panel \.dim-workspacePath \{[^}]*overflow-x: auto;/);
|
|
assert.match(styles, /\.dim-panel \.dim-workspaceEdit \{[^}]*grid-column: 2;[^}]*grid-row: 1;[^}]*white-space: nowrap;/);
|
|
});
|
|
|
|
test('bot cards keep Agent Preset guidance in a keyboard-accessible help tooltip', async () => {
|
|
const styles = await readFile(STYLES_URL, 'utf8');
|
|
|
|
assert.match(styles, /\.dim-panel \.dim-preset \{[^}]*grid-template-columns: minmax\(0, 1fr\) max-content;[^}]*margin-top: 6px;[^}]*padding: 6px 10px;/);
|
|
assert.match(styles, /\.dim-panel \.dim-presetHeader \{[^}]*position: relative;[^}]*grid-column: 1 \/ -1;[^}]*display: flex;[^}]*justify-content: space-between;/);
|
|
assert.match(styles, /\.dim-panel \.dim-presetTitle \{[^}]*display: inline-flex;[^}]*gap: 5px;[^}]*white-space: nowrap;/);
|
|
assert.match(styles, /\.dim-panel \.dim-presetHelpButton:focus-visible \{[^}]*box-shadow:/);
|
|
assert.match(styles, /\.dim-panel \.dim-presetTooltip \{[^}]*position: absolute;[^}]*width: min\(320px, 100%\);[^}]*white-space: normal;[^}]*opacity: 0;[^}]*visibility: hidden;[^}]*pointer-events: none;/);
|
|
assert.match(styles, /\.dim-panel \.dim-presetHelp:hover \.dim-presetTooltip, \.dim-panel \.dim-presetHelp:focus-within \.dim-presetTooltip \{[^}]*opacity: 1;[^}]*visibility: visible;/);
|
|
assert.match(styles, /\.dim-panel \.dim-presetSelect \{[^}]*grid-column: 1 \/ -1;[^}]*grid-row: 2;/);
|
|
assert.match(styles, /\.dim-panel \.dim-presetError \{[^}]*grid-column: 1 \/ -1;[^}]*grid-row: 3;/);
|
|
assert.doesNotMatch(styles, /\.dim-panel \.dim-presetHelp \{[^}]*grid-row: 3;/);
|
|
});
|
|
|
|
test('the bundled DingTalk channel has no local sender approval workflow', async () => {
|
|
const [source, bundle] = await Promise.all([
|
|
readFile(DINGTALK_CLIENT_SOURCE_URL, 'utf8'),
|
|
readFile(CLIENT_BUNDLE_URL, 'utf8'),
|
|
]);
|
|
|
|
assert.equal('approveSender' in DINGTALK_ENDPOINTS, false);
|
|
assert.equal('revokeSender' in DINGTALK_ENDPOINTS, false);
|
|
assert.doesNotMatch(source, /SenderAccess|onApprove|onRevoke|approveSender|revokeSender/);
|
|
assert.doesNotMatch(
|
|
bundle,
|
|
/bot\.sender\.approve|bot\.sender\.revoke|允许使用机器人的钉钉账号|批准使用/,
|
|
);
|
|
});
|
|
|
|
test('the bilingual dictionary has no duplicate object keys', async () => {
|
|
const source = await readFile(new URL('../plugin-src/client/i18n.js', import.meta.url), 'utf8');
|
|
const { warnings } = await transform(source, { loader: 'js', logLevel: 'silent' });
|
|
assert.deepEqual(warnings.filter((warning) => warning.id === 'duplicate-object-key'), []);
|
|
});
|
|
|
|
test('every shipped Chinese client string has an English projection', async () => {
|
|
const paths = (await readdir(CLIENT_SOURCE_DIRECTORY_URL, { recursive: true }))
|
|
.filter((path) => path.endsWith('.js') && path !== 'i18n.js');
|
|
const sources = await Promise.all(paths.map((path) =>
|
|
readFile(new URL(path, CLIENT_SOURCE_DIRECTORY_URL), 'utf8')));
|
|
const strings = new Set();
|
|
for (const source of sources) {
|
|
for (const match of source.matchAll(/(['"`])((?:\\.|(?!\1)[\s\S])*?)\1/g)) {
|
|
if (/[\p{Script=Han}]/u.test(match[2])) strings.add(match[2]);
|
|
}
|
|
}
|
|
|
|
setImTranslator((key) => en[key] ?? key);
|
|
try {
|
|
const untranslated = [...strings].filter((value) =>
|
|
/[\p{Script=Han}]/u.test(localizeText(value)));
|
|
assert.deepEqual(untranslated, []);
|
|
assert.ok(strings.size > 350);
|
|
} finally {
|
|
setImTranslator(null);
|
|
}
|
|
});
|
|
|
|
test('client source contains no legacy Plugins-tab settings registrations', async () => {
|
|
const paths = (await readdir(CLIENT_SOURCE_DIRECTORY_URL, { recursive: true }))
|
|
.filter((path) => path.endsWith('.js'));
|
|
const sources = await Promise.all(paths.map(async (path) => ({
|
|
path,
|
|
source: await readFile(new URL(path, CLIENT_SOURCE_DIRECTORY_URL), 'utf8'),
|
|
})));
|
|
const legacy = sources
|
|
.filter(({ source }) => source.includes('settings.plugins.tab'))
|
|
.map(({ path }) => path);
|
|
|
|
assert.deepEqual(legacy, []);
|
|
});
|
|
|
|
test('client registers one top-level bilingual IM settings section with a directory picker', async () => {
|
|
const effects = [];
|
|
const registrations = [];
|
|
const dictionaries = [];
|
|
const directoryCalls = [];
|
|
const rpcCalls = [];
|
|
const rpcCall = async (...args) => {
|
|
rpcCalls.push(args);
|
|
return { ok: true, value: {} };
|
|
};
|
|
const ctx = {
|
|
effect(install, label) {
|
|
effects.push({ install, label });
|
|
},
|
|
locale: {
|
|
bind(namespace) {
|
|
assert.equal(namespace, IM_LOCALE_NAMESPACE);
|
|
return (key) => en[key] ?? key;
|
|
},
|
|
register(namespace, value) {
|
|
dictionaries.push({ namespace, value });
|
|
return () => {};
|
|
},
|
|
},
|
|
connection: { rpc: { call: rpcCall } },
|
|
workspaces: {
|
|
async listDirectory(path, signal) {
|
|
directoryCalls.push({ operation: 'list', path, signal });
|
|
return { path, entries: [] };
|
|
},
|
|
async pickDirectory() {
|
|
directoryCalls.push({ operation: 'pick' });
|
|
return '/workspace/chosen';
|
|
},
|
|
},
|
|
slots: {
|
|
inject(name, install) {
|
|
assert.equal(name, 'settings.section');
|
|
install();
|
|
},
|
|
register(options, component) {
|
|
registrations.push({ options, component });
|
|
return () => {};
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
applyClient(ctx);
|
|
const dictionaryEffect = effects.find((entry) => entry.label === 'im-settings: bilingual dictionaries');
|
|
assert.ok(dictionaryEffect);
|
|
dictionaryEffect.install();
|
|
|
|
assert.deepEqual(clientInject, ['slots', 'connection', 'locale', 'workspaces']);
|
|
assert.equal(dictionaries[0].namespace, IM_LOCALE_NAMESPACE);
|
|
assert.deepEqual(Object.keys(dictionaries[0].value.en).sort(), Object.keys(dictionaries[0].value.zh).sort());
|
|
assert.equal(registrations.length, 1);
|
|
assert.equal(registrations[0].options.name, 'settings.section');
|
|
assert.equal(registrations[0].options.id, 'xmanrui-dsh-im');
|
|
assert.equal(registrations[0].options.order, 21);
|
|
assert.equal(registrations[0].options.locale, IM_LOCALE_NAMESPACE);
|
|
assert.equal(registrations[0].options.label(), 'IM bots');
|
|
assert.equal(registrations[0].component, IMSettingsTab);
|
|
|
|
const injected = registrations[0].options.inject();
|
|
const signal = new AbortController().signal;
|
|
await injected.updateRpcCall('update.status', {}, signal);
|
|
assert.deepEqual(rpcCalls, [['/dsh-im', 'update.status', {}, signal]]);
|
|
assert.deepEqual(
|
|
await injected.workspaceDirectoryPicker.listDirectory('/workspace/current', signal),
|
|
{ path: '/workspace/current', entries: [] },
|
|
);
|
|
assert.equal(await injected.workspaceDirectoryPicker.pickDirectory(), '/workspace/chosen');
|
|
assert.deepEqual(directoryCalls, [
|
|
{ operation: 'list', path: '/workspace/current', signal },
|
|
{ operation: 'pick' },
|
|
]);
|
|
|
|
const markup = renderToStaticMarkup(React.createElement(
|
|
registrations[0].component,
|
|
injected,
|
|
));
|
|
assert.match(markup, /Connecting DeepSeek Harness/);
|
|
assert.match(markup, new RegExp(
|
|
`class="dim-brandVersion">v${IM_PLUGIN_VERSION.replaceAll('.', '\\.')}<\\/span>`,
|
|
));
|
|
assert.match(markup, /Help & feedback · Open GitHub/);
|
|
assert.match(markup, />WeChat<|>Feishu<|>DingTalk<|>WeCom</);
|
|
assert.match(markup, />QQ<[^]*>Slack<[^]*>Telegram<[^]*>Discord<[^]*>WhatsApp</);
|
|
assert.match(markup, />AI Office<\/strong><small class="dim-channelNote">\(Experimental\)<\/small>/);
|
|
assert.doesNotMatch(markup, /[\p{Script=Han}]/u);
|
|
} finally {
|
|
setImTranslator(null);
|
|
}
|
|
});
|
|
|
|
test('all nine channel settings and connected cards render English copy', () => {
|
|
const rpcCall = async () => ({ ok: true, value: {} });
|
|
const noop = () => {};
|
|
const account = {
|
|
botId: 'bot-english',
|
|
state: 'connected',
|
|
connected: true,
|
|
bot: {
|
|
name: 'Demo Bot',
|
|
accountIdMasked: 'account••01',
|
|
appIdMasked: 'app••01',
|
|
clientIdMasked: 'client••01',
|
|
idMasked: 'bot••01',
|
|
username: 'demo_bot',
|
|
},
|
|
health: { summary: 'Connection is healthy', lastCheckedAt: '2026-08-16T08:00:00.000Z' },
|
|
};
|
|
|
|
setImTranslator((key) => en[key] ?? key);
|
|
try {
|
|
const pages = [
|
|
WeixinSettingsTab,
|
|
FeishuSettingsTab,
|
|
DingtalkSettingsTab,
|
|
WecomSettingsTab,
|
|
QqSettingsTab,
|
|
SlackSettingsTab,
|
|
TelegramSettingsTab,
|
|
DiscordSettingsTab,
|
|
WhatsappSettingsTab,
|
|
];
|
|
const pageMarkup = pages.map((Component) =>
|
|
renderToStaticMarkup(React.createElement(Component, { rpcCall }))).join('\n');
|
|
assert.match(pageMarkup, /Scan QR code/);
|
|
assert.match(pageMarkup, /Manual setup/);
|
|
assert.match(pageMarkup, /Loading WeChat connection status/);
|
|
assert.match(pageMarkup, /Loading Feishu bots/);
|
|
assert.match(pageMarkup, /Loading DingTalk connection status/);
|
|
assert.match(pageMarkup, /Loading WeCom bot status/);
|
|
assert.match(pageMarkup, /Loading QQ bot status/);
|
|
assert.match(pageMarkup, /Loading Slack bot status/);
|
|
assert.match(pageMarkup, /Loading Telegram bot status/);
|
|
assert.match(pageMarkup, /Loading Discord bot status/);
|
|
assert.match(pageMarkup, /Loading WhatsApp bot status/);
|
|
assert.doesNotMatch(pageMarkup, /[\p{Script=Han}]/u);
|
|
|
|
const sharedCardProps = {
|
|
removing: true,
|
|
onReconnect: noop,
|
|
onRequestRemove: noop,
|
|
onConfirmRemove: noop,
|
|
onCancelRemove: noop,
|
|
};
|
|
const cards = [
|
|
React.createElement(WeixinAccountCard, { ...sharedCardProps, account }),
|
|
React.createElement(FeishuBotCard, { ...sharedCardProps, connection: account }),
|
|
React.createElement(DingtalkAccountCard, { ...sharedCardProps, account }),
|
|
React.createElement(WecomAccountCard, { ...sharedCardProps, account }),
|
|
React.createElement(QqAccountCard, { ...sharedCardProps, account }),
|
|
React.createElement(SlackAccountCard, { ...sharedCardProps, account }),
|
|
React.createElement(TelegramAccountCard, { ...sharedCardProps, account }),
|
|
React.createElement(DiscordAccountCard, { ...sharedCardProps, account }),
|
|
React.createElement(WhatsappAccountCard, { ...sharedCardProps, account }),
|
|
];
|
|
const cardMarkup = cards.map(renderToStaticMarkup).join('\n');
|
|
assert.match(cardMarkup, /Connected/);
|
|
assert.doesNotMatch(cardMarkup, /Message channel/);
|
|
assert.match(cardMarkup, /Last checked/);
|
|
assert.match(cardMarkup, /Check connection/);
|
|
assert.match(cardMarkup, /Remove connection/);
|
|
assert.doesNotMatch(cardMarkup, /[\p{Script=Han}]/u);
|
|
|
|
const qqRetryMarkup = renderToStaticMarkup(React.createElement(QqAccountCard, {
|
|
...sharedCardProps,
|
|
removing: false,
|
|
account: {
|
|
...account,
|
|
state: 'error',
|
|
connected: false,
|
|
error: {
|
|
code: 'connection-failed',
|
|
message: 'QQ 连接未就绪,插件会自动重试。',
|
|
},
|
|
},
|
|
}));
|
|
assert.match(qqRetryMarkup, /The QQ connection is not ready; the plugin will retry automatically\./);
|
|
assert.doesNotMatch(qqRetryMarkup, /[\p{Script=Han}]/u);
|
|
} finally {
|
|
setImTranslator(null);
|
|
}
|
|
});
|