mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-14 18:00:45 +08:00
Some checks are pending
CI / Test, build, and verify package (push) Waiting to run
178 lines
9.8 KiB
JavaScript
178 lines
9.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import React from 'react';
|
|
import TestRenderer from 'react-test-renderer';
|
|
|
|
import { AccessGrantSettingsPage } from '../../../plugin-src/client/access-grant-settings.js';
|
|
import { en, setImTranslator } from '../../../plugin-src/client/i18n.js';
|
|
|
|
const { act, create } = TestRenderer;
|
|
const groupJid = '120363111111111111@g.us';
|
|
const otherGroupJid = '120363222222222222@g.us';
|
|
const ownerPhone = '8618111111111';
|
|
const textOf = node => typeof node === 'string' || typeof node === 'number'
|
|
? String(node) : node?.children?.map(textOf).join('') ?? '';
|
|
const button = (root, text) => root.findAllByType('button').find(node => textOf(node) === text);
|
|
const tab = (root, label) => root.findAllByProps({ role: 'tab' })
|
|
.find(node => textOf(node).startsWith(label));
|
|
async function flush() { for (let i = 0; i < 12; i += 1) await Promise.resolve(); }
|
|
|
|
function fixture() {
|
|
return {
|
|
version: 1, globalAdmins: [ownerPhone],
|
|
directMembers: [{ phone: '8618222222222', canExecuteCommands: true }],
|
|
groups: {
|
|
[groupJid]: { title: 'Ops', agentPreset: 'netxops', admins: [], members: [] },
|
|
[otherGroupJid]: { title: 'Support', admins: [], members: [] },
|
|
},
|
|
pending: [{ id: 'p_test01', kind: 'direct', phone: '8618444444444',
|
|
createdAt: '2026-10-11T00:00:00.000Z', status: 'pending', notifyRefs: [] }],
|
|
contacts: Array.from({ length: 35 }, (_, index) => ({
|
|
phone: `861900000${String(index).padStart(4, '0')}`,
|
|
pushName: index === 34 ? 'Alice' : `Contact ${index}`,
|
|
scenes: ['direct'], groupJids: [], lids: [], lastSeenAt: '2026-10-11T00:00:00.000Z',
|
|
})),
|
|
};
|
|
}
|
|
|
|
async function mount(t, options = {}) {
|
|
const calls = [];
|
|
let savedGrant = fixture();
|
|
let savedScope = 'user_in_chat';
|
|
const rpcCall = async (endpoint, payload) => {
|
|
calls.push({ endpoint, payload });
|
|
if (endpoint === 'connection.status' && options.load) return options.load;
|
|
if (endpoint === 'bot.access-grant.set') {
|
|
if (options.failSave) throw new Error('Save rejected');
|
|
savedGrant = payload.grant;
|
|
}
|
|
if (endpoint === 'bot.group-session-scope.set') savedScope = payload.groupSessionScope;
|
|
return { ok: true, value: {
|
|
bots: [{ botId: 'wa-test', accessGrant: savedGrant, groupSessionScope: savedScope }],
|
|
agentPresetCatalog: { defaultId: 'netxops', items: [{ id: 'netxops' }, { id: 'safe' }] },
|
|
} };
|
|
};
|
|
function Page() {
|
|
const [accessGrant, setAccessGrant] = React.useState(savedGrant);
|
|
return React.createElement(AccessGrantSettingsPage, {
|
|
channel: 'whatsapp', account: {
|
|
botId: 'wa-test', phone: ownerPhone, accessGrant, groupSessionScope: 'user_in_chat',
|
|
}, rpcCall, onSaved: setAccessGrant,
|
|
});
|
|
}
|
|
let renderer;
|
|
await act(async () => { renderer = create(React.createElement(Page)); await flush(); });
|
|
t.after(async () => { await act(async () => renderer.unmount()); });
|
|
return { renderer, calls };
|
|
}
|
|
|
|
test('access sections support keyboard navigation and preserve edits across sections', async t => {
|
|
const { renderer, calls } = await mount(t);
|
|
const root = renderer.root;
|
|
assert.equal(root.findAllByProps({ role: 'tabpanel' }).filter(node => !node.props.hidden).length, 1);
|
|
assert.equal(button(root, '保存分级访问设置').props.disabled, true);
|
|
await act(async () => tab(root, '私聊与管理员').props.onKeyDown({ key: 'ArrowRight', preventDefault() {} }));
|
|
assert.equal(tab(root, '群授权').props['aria-selected'], true);
|
|
const preset = root.findAllByType('select').find(node => node.props['aria-label'] === '群 Agent Preset');
|
|
await act(async () => preset.props.onChange({ target: { value: 'safe' } }));
|
|
await act(async () => tab(root, '最近联系人').props.onClick());
|
|
await act(async () => tab(root, '群授权').props.onClick());
|
|
assert.equal(root.findAllByType('select').find(node => node.props['aria-label'] === '群 Agent Preset').props.value, 'safe');
|
|
assert.match(textOf(root.findByProps({ className: 'dim-waAccessSaveState' })), /未保存/);
|
|
assert.equal(calls.filter(call => call.endpoint !== 'connection.status').length, 0);
|
|
});
|
|
|
|
test('saving retains the group preset and session scope when the parent updates its grant', async t => {
|
|
const { renderer, calls } = await mount(t);
|
|
const root = renderer.root;
|
|
await act(async () => {
|
|
root.findAllByType('select').find(node => node.props['aria-label'] === '群 Agent Preset')
|
|
.props.onChange({ target: { value: 'safe' } });
|
|
root.findAllByType('select').find(node => node.props.value === 'user_in_chat')
|
|
.props.onChange({ target: { value: 'chat' } });
|
|
});
|
|
await act(async () => { root.findByType('form').props.onSubmit({ preventDefault() {} }); await flush(); });
|
|
const grantCall = calls.find(call => call.endpoint === 'bot.access-grant.set');
|
|
assert.equal(grantCall.payload.grant.groups[groupJid].agentPreset, 'safe');
|
|
assert.deepEqual(calls.find(call => call.endpoint === 'bot.group-session-scope.set').payload,
|
|
{ botId: 'wa-test', groupSessionScope: 'chat' });
|
|
assert.ok(root.findAllByType('select').some(node => node.props.value === 'chat'));
|
|
assert.equal(button(root, '保存分级访问设置').props.disabled, true);
|
|
assert.match(textOf(root.findByProps({ className: 'dim-waAccessSaveState' })), /所有修改已保存/);
|
|
});
|
|
|
|
test('unsaved edits block approval and group sync; discard restores the last saved configuration', async t => {
|
|
const { renderer, calls } = await mount(t);
|
|
const root = renderer.root;
|
|
await act(async () => root.findAllByType('input').find(node => node.props.value === 'Ops')
|
|
.props.onChange({ target: { value: 'Draft title' } }));
|
|
assert.equal(button(root, '批准').props.disabled, true);
|
|
assert.equal(button(root, '同步群名').props.disabled, true);
|
|
await act(async () => { button(root, '批准').props.onClick(); button(root, '同步群名').props.onClick(); await flush(); });
|
|
assert.equal(calls.filter(call => call.endpoint !== 'connection.status').length, 0);
|
|
await act(async () => button(root, '撤销修改').props.onClick());
|
|
assert.ok(root.findAllByType('input').some(node => node.props.value === 'Ops'));
|
|
assert.equal(button(root, '批准').props.disabled, false);
|
|
assert.equal(button(root, '同步群名').props.disabled, false);
|
|
assert.equal(button(root, '保存分级访问设置').props.disabled, true);
|
|
});
|
|
|
|
test('contact search reaches beyond the first thirty contacts and groups match names or JIDs', async t => {
|
|
const { renderer } = await mount(t);
|
|
const root = renderer.root;
|
|
const contactTable = () => root.findByProps({ 'data-kind': 'contact' });
|
|
assert.doesNotMatch(textOf(contactTable()), /Alice/);
|
|
await act(async () => button(root, '显示更多联系人').props.onClick());
|
|
assert.match(textOf(contactTable()), /Alice/);
|
|
const contactSearch = root.findAllByType('input').find(node => node.props.placeholder === '搜索昵称、电话或群名称');
|
|
await act(async () => contactSearch.props.onChange({ target: { value: 'alice' } }));
|
|
assert.match(textOf(contactTable()), /Alice/);
|
|
assert.doesNotMatch(textOf(contactTable()), /Contact 0/);
|
|
const groupSearch = root.findAllByType('input').find(node => node.props.placeholder === '搜索群名称或 JID');
|
|
await act(async () => groupSearch.props.onChange({ target: { value: 'Support' } }));
|
|
assert.equal(root.findAllByType('details').length, 1);
|
|
assert.match(textOf(root.findByType('details')), /Support/);
|
|
await act(async () => groupSearch.props.onChange({ target: { value: groupJid } }));
|
|
assert.match(textOf(root.findByType('details')), /Ops/);
|
|
});
|
|
|
|
test('failed saves retain the draft, keep it dirty and expose the error', async t => {
|
|
const { renderer } = await mount(t, { failSave: true });
|
|
const root = renderer.root;
|
|
await act(async () => root.findAllByType('input').find(node => node.props.value === 'Ops')
|
|
.props.onChange({ target: { value: 'Unsaved title' } }));
|
|
await act(async () => { root.findByType('form').props.onSubmit({ preventDefault() {} }); await flush(); });
|
|
assert.equal(textOf(root.findByProps({ role: 'alert' })), 'Save rejected');
|
|
assert.ok(root.findAllByType('input').some(node => node.props.value === 'Unsaved title'));
|
|
assert.equal(button(root, '保存分级访问设置').props.disabled, false);
|
|
});
|
|
|
|
test('phone suggestions filter names without matching every contact through an empty digit query', async t => {
|
|
const { renderer } = await mount(t);
|
|
const input = renderer.root.findAllByType('input').find(node => node.props.value === '8618222222222');
|
|
await act(async () => {
|
|
input.props.onFocus();
|
|
input.props.onChange({ target: { value: 'Alice' } });
|
|
});
|
|
const suggestions = renderer.root.findByProps({ role: 'listbox' });
|
|
assert.match(textOf(suggestions), /Alice/);
|
|
assert.doesNotMatch(textOf(suggestions), /Contact 0/);
|
|
});
|
|
|
|
test('access editing waits for status and new section labels translate into English', async t => {
|
|
let resolveLoad;
|
|
const load = new Promise(resolve => { resolveLoad = resolve; });
|
|
setImTranslator(key => en[key] ?? key);
|
|
t.after(() => setImTranslator(null));
|
|
const { renderer, calls } = await mount(t, { load });
|
|
assert.equal(renderer.root.findByProps({ className: 'dim-waAccessEditor' }).props.disabled, true);
|
|
assert.doesNotMatch(textOf(renderer.root.findByProps({ role: 'tablist' })), /[\p{Script=Han}]/u);
|
|
await act(async () => { renderer.root.findByType('form').props.onSubmit({ preventDefault() {} }); await flush(); });
|
|
assert.equal(calls.length, 1);
|
|
await act(async () => {
|
|
resolveLoad({ ok: true, value: { bots: [{ botId: 'wa-test', accessGrant: fixture(), groupSessionScope: 'user_in_chat' }] } });
|
|
await flush();
|
|
});
|
|
assert.equal(renderer.root.findByProps({ className: 'dim-waAccessEditor' }).props.disabled, false);
|
|
assert.equal(textOf(renderer.root.findByProps({ className: 'dim-waAccessSaveState' })), 'All changes saved');
|
|
});
|