import assert from 'node:assert/strict'; import { readFile, readdir } from 'node:fs/promises'; import test from 'node:test'; 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( `
]*>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, /', 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, /