Files
open-design/apps/web/tests/components/AmrOwnerTopUpDialog.copy.test.tsx
lefarcenandGitHub 7eb1e217be fix(web): align client error copy with the product error-copy doc (OPEND-2849) (#8620)
* fix(web): align export and update-check error copy with product (OPEND-2849)

- S26a: a generic export failure now shows the product title + body
  ("导出失败" / "本次导出未完成,请重新尝试。") through the same Toast
  message + details shape as S26c. fileViewer.exportFailed is split into
  fileViewer.exportFailedTitle / fileViewer.exportFailedDescription in all
  19 locales; the old key is removed.
- S31b: drop the sentence-final period from the title line of
  updater.dialogCheckFailed (zh-CN and 17 other locales; th had none).

* fix(web): never surface raw exporter errors on export failure (OPEND-2849)

Product copy S26: an export failure shows the S26a title + body by default;
only an already-localized, user-facing reason may replace it. Raw exporter
strings (HTTP status lines, renderer/daemon errors, internal exceptions)
no longer reach the toast.

- New export-failure-toast module: LocalizedExportError marks errors whose
  message comes from an i18n key; exportFailureToast passes only those
  through and falls back to S26a for everything else.
- Localized reasons now thrown as LocalizedExportError: PPTX daemon
  unreachable / PPTX not available, version image capture failed.
- Image export catch-all uses the same rule instead of echoing err.message.
- The original error object is unchanged, so console.warn and
  exportErrorCode analytics still see the raw message.

* fix(web): align disk-write failure and browser-open hint copy with product (OPEND-2849)

- chat.runError.localStorageFailureMessage now matches the product supplement
  row 13 verbatim in zh-CN ("…请确认有足够的剩余空间后,再重新尝试。"); the
  old "permission to save to the current folder" clause is dropped and all
  19 locales are retranslated to the new meaning.
- settings.amrActivationBrowserFailed (S32 browser not opened): half-width
  comma replaced by a full-width one in zh-CN and zh-TW; wording unchanged.

* fix(web): align S03/S04/S06/S09a/S15/S24/S28a/S31c/S32a error copy with product (OPEND-2849)

zh-CN copied verbatim from the product error-copy doc; all 19 locales translated.

- S04 pre-send signed out (balance gate dialog): "Open Design 尚未登录" + body.
- S06 pre-send insufficient allowance: owner dialog "可用额度不足" + body (no
  balance figure); member dialog "团队额度不足" + body. Benefits list and
  buttons unchanged.
- S15 mid-run allowance exhausted: the turn-anchored upgrade card's $0 state
  shows title + body, personal vs. member chosen by the billing audience
  ProjectView already resolves. Low-balance tier, amount and Upgrade button
  unchanged; the pre-send tail card keeps "现在无法开始新任务".
- S24a unfinished todos: "任务尚未完成" + "本次任务仍有 {n} 项内容未完成。" with the
  real unfinished count; S24c user stop: "任务已停止" + body. Buttons kept.
- S03 / S28a Home send failures: the optimistic create's rollback toast (the
  surface users actually see) and HomeView's inline block now map
  AMR_AUTH_REQUIRED to S03 and transport failures to S28a, as title + body.
- S31c update download failed (updater error code download-failed): update
  dialog shows "更新下载未完成" + body; Settings status shows the title.
- S32a browser not opened during sign-in: title + body via one shared
  component for the three sign-in surfaces; the sign-in page link is kept.
- S09a rate-limit auto retry: run_retry_attempted now carries
  failure_category in the SSE contract; a rate_limit retry row reads
  "模型服务请求繁忙" + body, other retries keep "正在重试".
2026-10-09 10:00:10 +00:00

116 lines
5.0 KiB
TypeScript

// @vitest-environment jsdom
/**
* T56 / T57(产品口述 2026-09-06):「找所有者充值」弹窗改成产品文档第四节的
* **正式文案**,并且**只留一颗按钮**。
*
* 产品原话:「不要保留,严格按产品稿,不要私自发挥」——「复制请求」那颗主按钮
* 及其复制逻辑整个删掉。
*
* ⚠️ 这条测试**推翻**了 `ProjectView.amr-balance-branches.test.tsx` 里那条
* 「成员的弹窗上不能只有一颗『暂不需要』」(§6.Y 死胡同的出口)。删掉复制按钮
* 之后这一档回到**单出口**,产品知情并明确要求。
*
* 文案的两个变体(T57,产品已批):Owner 名字拿得到就插名字,拿不到就换成
* 角色名 —— **只换插值那一处,其余逐字不动**。
*/
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { AmrOwnerTopUpDialog } from '../../src/components/chat/AmrOwnerTopUpDialog';
import { I18nProvider } from '../../src/i18n';
function renderDialog(props: { ownerName?: string | null } = {}) {
return render(
<I18nProvider>
<AmrOwnerTopUpDialog inline onClose={() => {}} {...props} />
</I18nProvider>,
);
}
/**
* 按**整段可读文本**找那句话,而不是 `getByText(字符串)`。
*
* ⚠️ 断言的字符**一个都没动** —— 变的只有量法。原因:2026-09-07 产品稿把
* Owner 名字单独加粗一档(`.owner-name { font-weight: 700 }`),那一段于是成了
* 自己的 `<strong>`,整句被拆成三个节点。而 `getByText` 默认的 `getNodeText`
* **只拼元素的直接文本子节点**,`<strong>` 里的名字会被漏掉 —— 同一句话原样
* 渲染在屏幕上,断言却报「找不到」。
*
* 换成读 `textContent` 之后这条守卫反而更紧:它现在同时挡住「文案被改」和
* 「文案被拆坏(多出空格 / 少一段 / 顺序反了)」两类退化。
*/
function textOf(text: string) {
return screen.getByText(
(_content, element) =>
element?.tagName === 'P' && element.textContent === text,
);
}
beforeEach(() => {
window.localStorage.clear();
// 产品稿是中文原件,判据就钉在中文上;其余 18 个 locale 是它的忠实翻译。
window.localStorage.setItem('open-design:locale', 'zh-CN');
window.localStorage.setItem('open-design:locale-source', 'manual');
});
afterEach(() => {
cleanup();
window.localStorage.clear();
});
/*
* ⚠️ OPEND-2849(2026-10-09)换过文案:产品《报错文案|精简版》S06「发送前:团队额度不足」
* 定稿为「团队额度不足」/「当前团队额度不足,请联系团队管理员充值或升级订阅。」。
* 定稿句里**没有** Owner 名字的位置,所以 T57 的「拿得到名字就插名字」那一变体不再成立 ——
* 有没有名字都是同一句。下面两条把这一点正面钉住(名字不出现),而不是删掉断言。
*/
describe('AmrOwnerTopUpDialog 的正式文案', () => {
it('标题逐字照产品稿', () => {
renderDialog();
expect(screen.getByText('团队额度不足')).toBeTruthy();
});
it('拿得到 Owner 名字时仍是定稿那一句,名字不插进去', () => {
renderDialog({ ownerName: '张三' });
expect(textOf('当前团队额度不足,请联系团队管理员充值或升级订阅。')).toBeTruthy();
expect(screen.queryByText('张三')).toBeNull();
});
/**
* 现在走的就是这一条:契约里唯一的 owner 名是 `CollabProject.ownerDisplayName`
* (项目级,注释逐字 "STUB: the real name source is B's member roster"),
* `WorkspaceCollabContext` 上根本没有工作区 owner 名。
*/
it.each([undefined, null, '', ' '])(
'拿不到名字(%s)时也是定稿那一句',
(ownerName) => {
renderDialog({ ownerName: ownerName as string | null | undefined });
expect(textOf('当前团队额度不足,请联系团队管理员充值或升级订阅。')).toBeTruthy();
},
);
});
describe('AmrOwnerTopUpDialog 只有一颗按钮', () => {
it('「复制请求」整颗不在了', () => {
renderDialog();
expect(screen.queryByTestId('amr-balance-owner-copy')).toBeNull();
expect(screen.queryByText('复制请求')).toBeNull();
expect(screen.queryByText('已复制')).toBeNull();
});
/**
* 单出口是这次裁决的**代价**,所以正面钉住它:弹窗里除了标题行那颗关闭图标
* 之外,动作区**只有「知道了」这一颗**。
*/
it('动作区只剩「知道了」', () => {
const { container } = renderDialog();
const dialog = screen.getByTestId('amr-balance-owner-dialog');
const actionLabels = [...dialog.querySelectorAll('button')]
.map((button) => button.textContent?.trim() ?? '')
.filter((label) => label.length > 0);
expect(actionLabels).toEqual(['知道了']);
// 那句「可以直接发给所有者的话」是复制机制的载荷,一并撤掉。
expect(container.textContent).not.toContain('Open Design 控制台');
});
});