feat: allow editing local WeChat card names
This commit is contained in:
1 parent
436f7ba1ea
commit
f1dabdb358
6 files changed
+314
-11
No files matched your search
@@ -0,0 +1,54 @@
|
||||
# Task: Make WeChat card display name editable
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20261014-wechat-card-name-7b36a2d9
|
||||
- Mode: Feature
|
||||
- Branch: codex/20261014-wechat-card-name-7b36a2d9-wechat-card-name
|
||||
- Worktree: /Users/chillishark/Makelore 麦洛/.codex-worktrees/makelore/20261014-wechat-card-name-7b36a2d9
|
||||
- Base commit: 436f7ba1eab500e41866f890f49816ff622ef6fe
|
||||
- Owner: codex
|
||||
- Status: In Progress
|
||||
|
||||
## Scope
|
||||
|
||||
- Make the WeChat card display name editable as an account-scoped local remark; save/cancel/Enter/Escape, reload persistence and failure feedback. Preserve current complete temporary runtime436f7ba and deliver the Renderer correction.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- User explicitly requests the card name (微信4) to be editable. Display remarks are local UI preferences keyed by tenant/user/stable channel account ID, following existing device-only card removal. No cloud rename operation exists; do not rename the real WeChat profile or invent server sync. Preserve all binding/QR/revision/health/operation recovery/auto-reply contracts. No live rename value, account binding mutation, model request, cloud deployment, main merge/push/cleanup or Main restart is authorized/needed.
|
||||
|
||||
## Project Context Loaded
|
||||
|
||||
- Task/mode/branch/worktree/base exactly match successful official check/start/status: new managed feature worktree from436f7ba1eab500e41866f890f49816ff622ef6fe, the actual current Renderer/Main source. Runtime metadata is fresh: MainPID35142/Host58003, consolidated-five-20261014-2d069b40, same trial profile/bundle.
|
||||
- Read AGENTS, read-before-planning/memory-index, own record, positioning, historical integrated current-state, decision-index/system overview, community boundary, success criteria, domain WeChat identity rules, personal-cloud ADR User-Owned/Scan-Bound sections and evidence/reflection/commitment indexes. Integrated documents are historical; pointer/artifact source identify current app.
|
||||
- All72 peer Scope/Intent/Promotion sections were read only. Another same-name placeholder9e3a72c4 was investigated via Agent--渠道 thread: its naming turn is interrupted and the human explicitly said to ignore it; latest agent work stops naming and diagnoses offline readiness. It remains untouched; current chat still explicitly requests card-name editing. The binding diagnostic and other ready WeChat tasks do not define a different card-name authority. No unresolved semantic conflict.
|
||||
- Existing shared/Main API exposes display_name only at create; no rename/update operation. Core account ID/router/revision/health remain authoritative. Local remarks are presentation preferences, not cloned cloud configuration; no cross-device promise. Design skill/database reviewed: retain current card palette/type, visible pencil focus target, accessible labeled field and responsive wrapping.
|
||||
- Evidence/constraints: existing unit/native fixtures cover binding/recovery and must remain valid. New tests focus persistence by stable identity, account isolation, cancel/validation/storage failure and no remote mutation. Explicit edit state must block automatic restore-replies start and its async preflight; auth changes hide/close old drafts immediately and writes verify current owner.
|
||||
- Likely files: ChannelAccounts.tsx, a small local remark preference helper, README and current channel unit/native fixtures. Unknown: native saved/idle UI availability for refresh; check at delivery. Gate: Passed before implementation plan.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add current-account local remark preference and inline pencil/edit/save/cancel controls; preserve original account DTO and consistent title/aria/dialog display. Handle validation, storage failure and owner switching, guard restore preflight during editing.
|
||||
2. Run focused unit/native rename and binding regressions, typecheck, scoped lint and default build. Document exact local preference scope.
|
||||
3. Prepare immutable Renderer overlay over existing Main bytes, verify live saved/idle UI, reload through native CUA and confirm edit/save/cancel controls without changing the user's real card name. Finalize only after actual UI evidence.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Added visible title/pencil editing, a labeled inline name field and save/cancel controls. Enter saves (except IME composition), Escape cancels; trim/nonempty/64-character validation and local storage failure keep the original display and draft. Remarks are scoped by tenant/user plus stable account ID, persist across refresh/revision/rebinding and are consistently used by card headings/aria/dialog title. Returning to the original source name clears its local override.
|
||||
- Owner changes immediately hide the old editor/alias and close its draft; saves verify live auth owner. Missing/hidden cards close the editor rather than locking the page. Editing prevents competing binding actions and automatic restore start/preflight while read-only health refresh remains available. Main/API/DTO/account/revision/operation identities are unchanged. README describes local-only remarks honestly.
|
||||
- Renderer build ready; immutable overlay preparation and actual native open/cancel verification remain before runtime finalization.
|
||||
|
||||
## Verification
|
||||
|
||||
- Typecheck, scoped ESLint, default full build:vite (NIANCODE_RENDERER_ONLY unset), git diff --check and task-aware doc drift passed. Independent source review found no blockers.
|
||||
- 116 channel unit cases passed, including remark persistence after revision change, cancel/Escape, validation/storage retry, user+tenant isolation, malformed storage, vanished-card unlock and Worker recovery without enabling during editing. Initial storage-failure fixture spy targeted Storage.prototype despite the suite's own mock; corrected to localStorage and reran all116 successfully.
|
||||
- Isolated Electron channel lifecycle scenario passed8.0s, including added keyboard edit/Escape/Enter, 64-character long remark, real reload persistence, original-name restoration and zero extra remote operations. Existing synthetic binding/switch/unbind/recovery assertions also passed. Reviewed compact980px editor screenshot; controls/badge fit, saved long name has no card overflow. Logs `/tmp/makelore-wechatname7b36-{unit,type,lint,build,electron}.log`, images `/tmp/makelore-wechatname7b36-electron`.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- None recorded.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- None recorded.
|
||||
@@ -68,6 +68,8 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智
|
||||
- 界面语言:仅保留中文;系统语言和历史设置中的其他语言会自动归一为中文。
|
||||
- 品牌资产:生产 SVG、PNG、应用图标、托盘图标与安装器视觉位于 [`resources/brand/`](resources/brand/README.md) 和 `resources/icons/`。
|
||||
|
||||
微信卡片名称可点击名称旁的铅笔修改,支持保存、取消、Enter 保存与 Escape 取消。备注名按当前账号与微信账号 ID 保存在本机,刷新或重新绑定后保留;不修改真实微信昵称或云端名称,不跨设备同步。保存失败保留原名称与编辑内容,可重试。
|
||||
|
||||
## 安装与开发
|
||||
|
||||
项目使用 `package.json` 中固定版本的 pnpm。
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Link2,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Plus,
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
@@ -34,6 +35,7 @@ import type {
|
||||
import { CloudAvatar } from './CloudAvatar';
|
||||
import { CloudRecovery } from './CloudRecovery';
|
||||
import { CloudWechatQr } from './CloudWechatQr';
|
||||
import { MAX_WECHAT_CARD_NAME_LENGTH, readWechatCardNames, wechatCardNameKey } from './wechat-card-names';
|
||||
|
||||
type Flow = {
|
||||
mode: 'new' | 'switch' | 'reconnect' | 'unbind' | 'delete';
|
||||
@@ -42,6 +44,7 @@ type Flow = {
|
||||
target: string;
|
||||
};
|
||||
type Expected = { target: string | null; enabled: boolean; disconnected?: boolean };
|
||||
type NameEdit = { ownerKey: string; accountId: string; value: string; error: string };
|
||||
const errorText = (error: unknown) => (error instanceof Error ? error.message : '操作没有完成,请重试');
|
||||
const isChannelManagementOperation = (operation: string) =>
|
||||
/channelaccount|channelbinding|^wechat/iu.test(operation);
|
||||
@@ -155,6 +158,22 @@ export function ChannelAccounts() {
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const cardStorageKey = cardPreferenceKey(user);
|
||||
const nameStorageKey = wechatCardNameKey(user);
|
||||
const [cardNames, setCardNames] = useState(() => ({
|
||||
key: nameStorageKey, values: readWechatCardNames(nameStorageKey),
|
||||
}));
|
||||
const [nameEdit, setNameEdit] = useState<NameEdit | null>(null);
|
||||
const nameEditRef = useRef<NameEdit | null>(null);
|
||||
const editingName = nameEdit?.ownerKey === nameStorageKey ? nameEdit : null;
|
||||
useEffect(() => {
|
||||
setCardNames({ key: nameStorageKey, values: readWechatCardNames(nameStorageKey) });
|
||||
nameEditRef.current = null;
|
||||
setNameEdit(null);
|
||||
}, [nameStorageKey]);
|
||||
const cardName = (account: CloudUserChannelAccount) =>
|
||||
cardNames.key === nameStorageKey && Object.hasOwn(cardNames.values, account.id)
|
||||
? cardNames.values[account.id]
|
||||
: account.display_name;
|
||||
const [removedCards, setRemovedCards] = useState(() => ({
|
||||
key: cardStorageKey, revisions: readRemovedCards(cardStorageKey),
|
||||
}));
|
||||
@@ -209,7 +228,50 @@ export function ChannelAccounts() {
|
||||
const ownerLogoutPending = accounts.some((account) =>
|
||||
account.blockers.includes('wechat_disconnect_pending')
|
||||
);
|
||||
const disabled = Boolean(busy) || restoringReplies || recoveryBlocked || !recoveryLoaded || ownerLogoutPending;
|
||||
const disabled = Boolean(busy) || restoringReplies || Boolean(editingName) || recoveryBlocked || !recoveryLoaded || ownerLogoutPending;
|
||||
const cancelNameEdit = () => {
|
||||
nameEditRef.current = null;
|
||||
setNameEdit(null);
|
||||
};
|
||||
const editingAccountVisible = !editingName || visibleAccounts.some((account) => account.id === editingName.accountId);
|
||||
useEffect(() => {
|
||||
if (accountsLoaded && !editingAccountVisible) {
|
||||
nameEditRef.current = null;
|
||||
setNameEdit(null);
|
||||
}
|
||||
}, [accountsLoaded, editingAccountVisible]);
|
||||
const startNameEdit = (account: CloudUserChannelAccount) => {
|
||||
if (!nameStorageKey || busyRef.current || restoringReplies || flow || editingName) return;
|
||||
const edit = { ownerKey: nameStorageKey, accountId: account.id, value: cardName(account), error: '' };
|
||||
nameEditRef.current = edit;
|
||||
setNameEdit(edit);
|
||||
setMore('');
|
||||
};
|
||||
const saveNameEdit = () => {
|
||||
if (!editingName) return;
|
||||
if (wechatCardNameKey(useAuthStore.getState().user) !== editingName.ownerKey) {
|
||||
cancelNameEdit();
|
||||
return;
|
||||
}
|
||||
const value = editingName.value.trim();
|
||||
if (!value || value.length > MAX_WECHAT_CARD_NAME_LENGTH) {
|
||||
setNameEdit({ ...editingName, error: `请填写1–${MAX_WECHAT_CARD_NAME_LENGTH}个字符的名称。` });
|
||||
return;
|
||||
}
|
||||
const account = accounts.find((item) => item.id === editingName.accountId);
|
||||
if (!account) { cancelNameEdit(); return; }
|
||||
const values = { ...readWechatCardNames(nameStorageKey) };
|
||||
if (value === account.display_name) delete values[account.id];
|
||||
else values[account.id] = value;
|
||||
try { localStorage.setItem(nameStorageKey, JSON.stringify(values)); }
|
||||
catch {
|
||||
setNameEdit({ ...editingName, error: '名称没有保存:暂时无法保存到这台设备,请重试。' });
|
||||
return;
|
||||
}
|
||||
if (wechatCardNameKey(useAuthStore.getState().user) === nameStorageKey)
|
||||
setCardNames({ key: nameStorageKey, values });
|
||||
cancelNameEdit();
|
||||
};
|
||||
const pendingFor = (id: string) =>
|
||||
pending.some(
|
||||
(item) => isChannelManagementOperation(item.operation) && item.input.channel_account_id === id
|
||||
@@ -573,7 +635,7 @@ export function ChannelAccounts() {
|
||||
if (
|
||||
loading || !accountsLoaded || !agentsLoaded || agentsError ||
|
||||
!recoveryLoaded || recoveryBlocked || ownerLogoutPending ||
|
||||
busy || busyRef.current || restoringReplies || flow || more ||
|
||||
busy || busyRef.current || restoringReplies || flow || more || editingName ||
|
||||
checkingIds.length || replyRestoreStopped.current
|
||||
) return;
|
||||
const candidate = accounts.find((account) =>
|
||||
@@ -587,7 +649,7 @@ export function ChannelAccounts() {
|
||||
setRestoringReplies(true);
|
||||
void run('restore-replies', candidate.id, async () => {
|
||||
const [catalog, recovery] = await Promise.all([loadAgents(), cloudAgentsApi.recovery()]);
|
||||
if (!alive.current || flowRef.current) return { restored: false };
|
||||
if (!alive.current || flowRef.current || nameEditRef.current) return { restored: false };
|
||||
agentsRef.current = catalog;
|
||||
setAgents(catalog);
|
||||
setPending(recovery.pending);
|
||||
@@ -599,7 +661,7 @@ export function ChannelAccounts() {
|
||||
if (!account || account.target_agent_slug !== candidate.target_agent_slug)
|
||||
return { restored: false, stop: true };
|
||||
if (
|
||||
!alive.current || flowRef.current || ownerPending.current ||
|
||||
!alive.current || flowRef.current || nameEditRef.current || ownerPending.current ||
|
||||
observed.some((item) => item.blockers.includes('wechat_disconnect_pending')) ||
|
||||
!canRestoreReplies(account, catalog)
|
||||
) return { restored: false };
|
||||
@@ -619,7 +681,7 @@ export function ChannelAccounts() {
|
||||
if (alive.current) setRestoringReplies(false);
|
||||
});
|
||||
}, [accounts, accountsLoaded, agents, agentsLoaded, agentsError, recoveryLoaded,
|
||||
recoveryBlocked, ownerLogoutPending, busy, restoringReplies, flow, more,
|
||||
recoveryBlocked, ownerLogoutPending, busy, restoringReplies, flow, more, editingName,
|
||||
checkingIds, loading, run, readAccounts, operationId, verifyFinal]);
|
||||
useEffect(() => {
|
||||
if (flow || recoveryBlocked || ownerLogoutPending || !accounts.some((account) =>
|
||||
@@ -848,6 +910,8 @@ export function ChannelAccounts() {
|
||||
>
|
||||
{visibleAccounts.map((account) => {
|
||||
const agent = agents.find((item) => item.slug === account.target_agent_slug);
|
||||
const displayName = cardName(account);
|
||||
const edit = editingName?.accountId === account.id ? editingName : null;
|
||||
const uncertain =
|
||||
!recoveryLoaded || checkingIds.includes(account.id) || pendingFor(account.id);
|
||||
const targetDisabled = agents.some(
|
||||
@@ -868,15 +932,51 @@ export function ChannelAccounts() {
|
||||
return (
|
||||
<article
|
||||
key={account.id}
|
||||
aria-label={`${account.display_name}微信`}
|
||||
aria-label={`${displayName}微信`}
|
||||
className="flex min-w-0 flex-col rounded-[26px] border border-[#e4e2de] bg-white p-5 shadow-[0_3px_0_0_#f1eeea] sm:p-6"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium text-[#788271]">微信</p>
|
||||
<h2 className="mt-1 break-words text-lg font-bold text-[#303747]">
|
||||
{account.display_name}
|
||||
</h2>
|
||||
{edit ? (
|
||||
<form className="mt-2 space-y-2" onSubmit={(event) => { event.preventDefault(); saveNameEdit(); }}>
|
||||
<Input
|
||||
autoFocus
|
||||
aria-label="微信卡片名称"
|
||||
aria-invalid={Boolean(edit.error)}
|
||||
aria-describedby={'wechat-name-help-' + account.id}
|
||||
maxLength={MAX_WECHAT_CARD_NAME_LENGTH}
|
||||
value={edit.value}
|
||||
onChange={(event) => setNameEdit({ ...edit, value: event.target.value, error: '' })}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') { event.preventDefault(); cancelNameEdit(); }
|
||||
if (event.key === 'Enter' && event.nativeEvent.isComposing) event.preventDefault();
|
||||
}}
|
||||
className="h-10 min-w-0 rounded-xl"
|
||||
/>
|
||||
<p id={'wechat-name-help-' + account.id} className="text-xs leading-5 text-muted-foreground">
|
||||
{edit.error ? <span role="alert" className="text-destructive">{edit.error}</span> : '备注名仅保存在这台电脑的当前账号中。'}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="submit" size="sm" className="rounded-lg">保存</Button>
|
||||
<Button type="button" variant="outline" size="sm" className="rounded-lg" onClick={cancelNameEdit}>取消</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<h2 className="mt-1 text-lg font-bold text-[#303747]">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`修改微信卡片名称:${displayName}`}
|
||||
title="修改卡片名称"
|
||||
disabled={!nameStorageKey || Boolean(busy) || restoringReplies || Boolean(flow) || Boolean(editingName)}
|
||||
className="flex max-w-full items-start gap-2 rounded-md text-left hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 disabled:cursor-default disabled:opacity-60"
|
||||
onClick={() => startNameEdit(account)}
|
||||
>
|
||||
<span className="min-w-0 break-words">{displayName}</span>
|
||||
<Pencil aria-hidden="true" className="mt-1.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
</button>
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`shrink-0 rounded-full px-3 py-1.5 text-xs font-semibold ${state.tone}`}
|
||||
@@ -1027,7 +1127,7 @@ export function ChannelAccounts() {
|
||||
<DialogDescription>
|
||||
{flow?.mode === 'new'
|
||||
? '选一个智能体,再用微信扫码。'
|
||||
: (selected?.display_name ?? '微信绑定')}
|
||||
: (selected ? cardName(selected) : '微信绑定')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{feedback}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { AuthUser } from '@/stores/auth';
|
||||
|
||||
export const MAX_WECHAT_CARD_NAME_LENGTH = 64;
|
||||
|
||||
export function wechatCardNameKey(user: AuthUser | null) {
|
||||
return user?.userId
|
||||
? 'makelore.wechat-card-names:' + JSON.stringify([user.tenantId, user.userId])
|
||||
: '';
|
||||
}
|
||||
|
||||
export function readWechatCardNames(key: string): Record<string, string> {
|
||||
if (!key) return {};
|
||||
try {
|
||||
const value: unknown = JSON.parse(localStorage.getItem(key) ?? '{}');
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
||||
return Object.fromEntries(Object.entries(value).flatMap(([id, name]) => {
|
||||
if (typeof name !== 'string') return [];
|
||||
const trimmed = name.trim();
|
||||
return trimmed && trimmed.length <= MAX_WECHAT_CARD_NAME_LENGTH ? [[id, trimmed]] : [];
|
||||
}));
|
||||
} catch { return {}; }
|
||||
}
|
||||
@@ -578,6 +578,34 @@ test('WeChat binding cards connect an account and switch its Agent through the c
|
||||
).toHaveCount(0);
|
||||
await expect(legacyCard.getByText('状态待确认', { exact: true })).toHaveCount(0);
|
||||
await expect(spareCard.getByRole('button', { name: '绑定智能体', exact: true })).toBeVisible();
|
||||
const callsBeforeRemarks = await app.evaluate(() => (globalThis as typeof globalThis & {
|
||||
cloudChannelFixtureCalls: { operation: string }[];
|
||||
}).cloudChannelFixtureCalls.filter((item) => item.operation !== 'channelAccounts'));
|
||||
await spareCard.getByRole('button', { name: '修改微信卡片名称:备用微信', exact: true }).click();
|
||||
const cardNameField = page.getByRole('textbox', { name: '微信卡片名称', exact: true });
|
||||
await cardNameField.fill('未保存备注');
|
||||
await cardNameField.press('Escape');
|
||||
await expect(cardNameField).toHaveCount(0);
|
||||
await spareCard.getByRole('button', { name: '修改微信卡片名称:备用微信', exact: true }).click();
|
||||
await cardNameField.fill('A'.repeat(64));
|
||||
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(980, 700));
|
||||
await page.screenshot({ path: testInfo.outputPath('wechat-card-name-edit-compact.png'), fullPage: true });
|
||||
await cardNameField.press('Enter');
|
||||
const remarkedCard = page.getByRole('article', { name: 'A'.repeat(64) + '微信', exact: true });
|
||||
await expect(remarkedCard).toBeVisible();
|
||||
expect(await remarkedCard.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
||||
await page.reload();
|
||||
await expect(remarkedCard).toBeVisible();
|
||||
await expect(remarkedCard.getByRole('button', { name: '绑定智能体', exact: true })).toBeVisible();
|
||||
await remarkedCard.getByRole('button', { name: '修改微信卡片名称:' + 'A'.repeat(64), exact: true }).click();
|
||||
await cardNameField.fill('备用微信');
|
||||
await cardNameField.press('Enter');
|
||||
await expect(spareCard).toBeVisible();
|
||||
const callsAfterRemarks = await app.evaluate(() => (globalThis as typeof globalThis & {
|
||||
cloudChannelFixtureCalls: { operation: string }[];
|
||||
}).cloudChannelFixtureCalls.filter((item) => item.operation !== 'channelAccounts'));
|
||||
expect(callsAfterRemarks).toEqual(callsBeforeRemarks);
|
||||
await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1366, 900));
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
await expect(page.getByText('活动记录', { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '微信对话', exact: true })).toHaveCount(0);
|
||||
|
||||
@@ -197,6 +197,103 @@ afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const nameKey = (userId = 'creator', tenantId: string | null = null) =>
|
||||
'makelore.wechat-card-names:' + JSON.stringify([tenantId, userId]);
|
||||
async function editCardName(name = legacy.display_name) {
|
||||
const current = await card(name);
|
||||
fireEvent.click(within(current).getByRole('button', { name: '修改微信卡片名称:' + name }));
|
||||
return within(current).getByRole('textbox', { name: '微信卡片名称' });
|
||||
}
|
||||
it('saves a local card remark through refresh and revision changes without touching the binding', async () => {
|
||||
const original = structuredClone(accounts);
|
||||
const view = renderPage();
|
||||
const field = await editCardName();
|
||||
fireEvent.change(field, { target: { value: ' 工作微信 ' } });
|
||||
fireEvent.submit(field.closest('form')!);
|
||||
expect(within(await card('工作微信')).getByRole('heading', { name: '工作微信' })).toBeVisible();
|
||||
expect(accounts).toEqual(original);
|
||||
expect(writes()).toEqual([]);
|
||||
view.unmount();
|
||||
accounts[0].revision += 1;
|
||||
renderPage();
|
||||
expect(await card('工作微信')).toBeVisible();
|
||||
expect(await card(spare.display_name)).toBeVisible();
|
||||
expect(writes()).toEqual([]);
|
||||
});
|
||||
it('cancels typed remarks with either Cancel or Escape', async () => {
|
||||
renderPage();
|
||||
let field = await editCardName();
|
||||
fireEvent.change(field, { target: { value: '不保存的名称' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '取消', exact: true }));
|
||||
expect(screen.queryByRole('textbox', { name: '微信卡片名称' })).not.toBeInTheDocument();
|
||||
field = await editCardName();
|
||||
fireEvent.change(field, { target: { value: '也不保存' } });
|
||||
fireEvent.keyDown(field, { key: 'Escape' });
|
||||
expect(await card()).toBeVisible();
|
||||
expect(localStorage.getItem(nameKey())).toBeNull();
|
||||
expect(writes()).toEqual([]);
|
||||
});
|
||||
it('keeps the original name and typed draft on validation or storage failure, then allows retry', async () => {
|
||||
renderPage();
|
||||
const field = await editCardName();
|
||||
fireEvent.change(field, { target: { value: ' ' } });
|
||||
fireEvent.submit(field.closest('form')!);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('请填写1–64个字符');
|
||||
expect(localStorage.getItem(nameKey())).toBeNull();
|
||||
fireEvent.change(field, { target: { value: '生活微信' } });
|
||||
const write = vi.spyOn(localStorage, 'setItem').mockImplementation(() => { throw new Error('storage unavailable'); });
|
||||
fireEvent.submit(field.closest('form')!);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('名称没有保存');
|
||||
expect(field).toHaveValue('生活微信');
|
||||
expect(await card()).toBeVisible();
|
||||
write.mockRestore();
|
||||
fireEvent.submit(field.closest('form')!);
|
||||
expect(await card('生活微信')).toBeVisible();
|
||||
expect(writes()).toEqual([]);
|
||||
});
|
||||
it.each([
|
||||
['account', 'another-creator', null],
|
||||
['tenant', 'creator', 'another-tenant'],
|
||||
] as const)('isolates card remarks and closes the old draft when the %s changes', async (_scope, userId, tenantId) => {
|
||||
localStorage.setItem(nameKey(), JSON.stringify({ [legacy.id]: '本账号备注' }));
|
||||
localStorage.setItem(nameKey(userId, tenantId), JSON.stringify({ [legacy.id]: '其他账号备注' }));
|
||||
renderPage();
|
||||
const field = await editCardName('本账号备注');
|
||||
fireEvent.change(field, { target: { value: '旧账号未保存内容' } });
|
||||
await act(async () => useAuthStore.setState({ user: { username: userId, userId, tenantId, deptId: null, authorities: [] } }));
|
||||
expect(screen.queryByRole('textbox', { name: '微信卡片名称' })).not.toBeInTheDocument();
|
||||
expect(await card('其他账号备注')).toBeVisible();
|
||||
expect(JSON.parse(localStorage.getItem(nameKey())!)[legacy.id]).toBe('本账号备注');
|
||||
expect(JSON.parse(localStorage.getItem(nameKey(userId, tenantId))!)[legacy.id]).toBe('其他账号备注');
|
||||
});
|
||||
it('ignores malformed local remarks and falls back to the original card name', async () => {
|
||||
localStorage.setItem(nameKey(), JSON.stringify({ [legacy.id]: { name: 'not a string' }, [spare.id]: ' ' }));
|
||||
renderPage();
|
||||
expect(await card()).toBeVisible();
|
||||
expect(await card(spare.display_name)).toBeVisible();
|
||||
});
|
||||
it('closes an edit when its card disappears on refresh and leaves the page usable', async () => {
|
||||
renderPage();
|
||||
await editCardName();
|
||||
accounts = [{ ...spare }];
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新微信列表' }));
|
||||
await waitFor(() => expect(screen.queryByRole('textbox', { name: '微信卡片名称' })).not.toBeInTheDocument());
|
||||
expect(await card(spare.display_name)).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '添加微信', exact: true })).toBeEnabled();
|
||||
expect(writes()).toEqual([]);
|
||||
});
|
||||
it('does not restore replies while a card name is being edited after Worker recovery', async () => {
|
||||
accounts = [restorable({ worker_online: false, blockers: ['worker_offline', 'binding_paused'] })];
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
renderPage();
|
||||
await editCardName();
|
||||
accounts[0].worker_online = true;
|
||||
accounts[0].blockers = ['binding_paused'];
|
||||
await act(async () => { await vi.advanceTimersByTimeAsync(10000); });
|
||||
expect(calls('enableChannelAccount')).toHaveLength(0);
|
||||
expect(screen.getByRole('textbox', { name: '微信卡片名称' })).toBeVisible();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['none', []],
|
||||
['drafts', [{ ...agentA, published_version: null }]],
|
||||
|
||||
Reference in new issue
Block a user