合并主分支
解决合并冲突
This commit is contained in:
@@ -6858,23 +6858,29 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
|
||||
await driver.submit('生成同 revision 首次验证');
|
||||
await driver.emitValidation(1, 1000);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
driver.invoke.mock.calls.filter(
|
||||
([command]) => command === 'start_local_game_preview',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(driver.readAuthorization()).toBeNull();
|
||||
});
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(
|
||||
driver.invoke.mock.calls.filter(
|
||||
([command]) => command === 'start_local_game_preview',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(driver.readAuthorization()).toBeNull();
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
|
||||
await driver.submit('同一 Run 新授权后重新验证同一 revision');
|
||||
await waitFor(() => {
|
||||
expect(driver.readAuthorization()).toMatchObject({
|
||||
afterRevision: 1,
|
||||
afterValidatedAt: 1000,
|
||||
authorizationId: expect.any(String),
|
||||
});
|
||||
});
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(driver.readAuthorization()).toMatchObject({
|
||||
afterRevision: 1,
|
||||
afterValidatedAt: 1000,
|
||||
authorizationId: expect.any(String),
|
||||
});
|
||||
},
|
||||
{ timeout: 3000 },
|
||||
);
|
||||
await driver.emitValidation(1, 2000);
|
||||
expect(
|
||||
await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }),
|
||||
@@ -7149,11 +7155,16 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
expect(screen.queryByText('等待新的运行事件')).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '运行详情' }));
|
||||
expect(
|
||||
within(screen.getByLabelText('最近运行活动')).getByText(
|
||||
/新受理 Run 正在执行/,
|
||||
),
|
||||
).not.toBeNull();
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(
|
||||
within(screen.getByLabelText('最近运行活动')).getByText(
|
||||
/新受理 Run 正在执行/,
|
||||
),
|
||||
).not.toBeNull();
|
||||
},
|
||||
{ timeout: 2500 },
|
||||
);
|
||||
harness.setProjectRevision(4);
|
||||
await act(async () => {
|
||||
harness.emitAgentRuntime(
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
- 登录态上传和生成结果必须先落 OSS / asset object,再向 `editor_project_resource` / `editor_asset` 写入轻量 `imageSrc: "/<objectKey>"`、`objectKey` 和 `assetObjectId`;未登录演示态可以在内存里使用 Data URL 预览,但项目、素材库、项目资源和 `editor_canvas.layers_json` 不得写入 `data:image/*`、`data:video/*`、`data:audio/*` 或 `blob:`。旧数据读取时如果已有 `objectKey`,`imageSrc` 归一成 `/<objectKey>`;没有 `objectKey` 的旧 Data URL 需要走修复上传并回写轻量引用。裁扩在项目上下文中虽然由前端 canvas 本地渲染 PNG,也必须先上传 OSS / asset object 并创建 `editor_project_resource`,再把带正式 `resourceId/objectKey/assetObjectId` 的裁扩图层加入画布;不能先把 `local-resource-*` + Data URL 图层交给项目保存或后续去背景。上传到生成面板参考图槽位的图片必须先创建 `editor_project_resource` 行;没有当前工程 ID 时才创建账号级 `editor_asset` 行,随后把对应 `resourceId` 或 `assetId` 写入参考图临时状态;生成请求提交前必须把临时状态解析成 `objectKey`、项目资源 ID 或素材 ID,未登记的本地图片和普通图片路径先上传 OSS,不能直接提交 Data URL、Blob URL 或临时图片源。
|
||||
- 资源表保存资源和素材级元数据,其中 `editor_project_resource.asset_kind` 是资源默认类型;图层位置、层级、分组选中所需 ID、groupId 和可空 `assetKindOverride` 保存在画布布局。legacy 布局 JSON 是混合数组:普通图层按 `layerId/resourceId/assetKindOverride` 保存,生成器占位和生成器对话框按 `itemType: "generation-dialog"` 保存;结构化模式下 `asset_kind_override` 必须是 `editor_canvas_layer` 的 typed 列,不得塞回 `item_json`。普通图层的新保存不再把资源默认 `assetKind` 或 `generationInputs` 写入布局;刷新时从 `editor_project_resource` 恢复资源默认类型,再与图层覆盖合并为 `effectiveAssetKind`,不得把合并结果反写为新的资源或覆盖。旧布局中的 `assetKind` 只用于迁移:与资源默认类型相同则归一为无 override,不同则迁移为图层 override;资源尚无默认值且允许补齐时先补资源默认值,不制造新的资源行。生成器快照必须包含生成器 ID、模式、提示词、参数、参考图、状态、占位框位置和可选 `generatedLayerId`;角色、图标等纯色抠图生成器的前端用户路径不保存或恢复 `screenColor` / `segModel`,同源重绘也不再从 `generationInputs.fields` 恢复 `抠图背景色` 或 `抠图模型`;宣发素材生成器还必须保存并恢复 `publicationWorkflowId`、`publicationGameInfo` 和 `publicationReferences`,避免刷新后生成卡片字段或参考图丢失。生成器快照中的参考图同样只保存 `resourceId/sourceAssetId` 行引用和展示所需 label,不保存图片 Data URL、signed URL 或 `objectKey`;刷新时用 `editor_project_resource` / `editor_asset` 行恢复临时生成请求所需图片源。生成成功后仍保存该快照,只是渲染时由 `generatedLayerId` 锚定到成品图层而不重复显示灰色占位框。`generationInputs.references` 是用户可见输入快照中的行级索引,只允许保存 `{ title, label, refType, refId }`;生成接口只接收提交前临时状态解析出的 `objectKey` 或资源 ID;Data URL、Blob URL 和 signed URL 不进入请求体,不进入资源 / 素材元数据。图层展示尺寸不再作为独立 `Size` 真相保存,刷新与新建图层均按 `Resolution`(`originalWidth/originalHeight`)原分辨率显示。图层组第一版是画布内布局语义,不单独建表。
|
||||
- 图片类、生成视频和音频结果除作为 `editor_project_resource` 和画布图层保存外,还要写入账号级 `editor_asset` 素材库;该写入由生成 BFF 在请求携带 `assetFolderId` 时完成。角色、图标图集、UI 提取和角色动作等多产物任务把实际产生的 provider 原始输出及后处理结果分别入库:所有条目沿用 `character`、`icon-spritesheet`、`character-animation` 等真实类型,provider 原始输出承载任务模型成本,后处理派生产物阶段成本为 0。后台素材查询以最终产物为父行、每个中间产物为可展开的独立子行,分页只计算父任务;手动重拆图集保留独立 `taskId` 用于存储隔离和日志排障,通过私有 provenance 从服务端生成账号素材的 source resource、asset object 或 Object Key 取得可信来源任务,并把它写入 `groupTaskId`,不信任客户端可提交的 resource `taskId/assetKind`;跨项目复用后仍可通过稳定媒体引用找回来源。没有可信来源的新拆分显式归到自身任务,不走历史资源链回溯。每个手动切片同时写入 `groupTaskExpectedAssetCount`,全部切片落库后写独立 cohort 完成事实;后台 read model 只让同一根任务的一个已完成拆分批次并入原图集父项,用户后来删除单片不会让批次脱组,部分失败批次和后续重复拆分批次按各自真实任务分页,避免残缺批次抢占根任务、单组无限增长或素材丢失。历史行在项目资源仍存在时兼容回溯,删除项目资源前只固化直接受影响行的真实来源字段,有界展示 ID 不反写数据库。`GENARRATIVE_EXTERNAL_GENERATION_MODE=queue` 下,画布图片、改图、图标素材、UI 素材提取、角色动作、视频、音效和背景音乐生成都先返回 `queueState`,前端轮询 `/api/runtime/external-generation/jobs/{jobId}` 到完成后重新读取项目快照;`inline` 或无项目上下文时才使用响应中的 resource / asset 快照做本地落画布兜底,不再把同一生成结果二次调用素材创建接口。生成请求失败、inline 完成或 queue 任务终态完成 / 失败后,右上角泥点 chip 必须通过 `/profile/dashboard` 回读余额,不做本地乐观扣减。生成视频会单独抽取首帧封面写入 `thumbnailSrc`,素材栏和拖回画布时沿用该封面作为 poster。
|
||||
- 生成面板不展示资源名称输入,默认使用原有自动编号;提示词输入保持统一可见边框。图片生成、快速编辑、重绘、修改图片、视频、音频、角色、图标、UI / 自定义规范、角色动画和宣发素材描述统一直接使用公共 `AutoGrowTextArea`,不经 `PlatformTextField` 的 textarea 分支;`AutoGrowTextArea` 负责尺寸、溢出及统一的 disabled 光标 / 透明度反馈,画布内生成面板统一由 `image-canvas-editor__generation-prompt` 承载边框、背景、padding、圆角和 focus chrome,独立修改图片弹窗继续使用 `image-canvas-editor__generate-prompt`,用途专属类只追加最小高度等差异,不得新建第三套 textarea chrome。Enter 继续输入换行而不提交表单。内部命名契约仍使用可选 `assetLabel`,最大 80 字符并在提交时 trim;历史状态或内部调用携带非空名称时,同一个名称必须贯穿 `assetLabel`、`canvasCompletion.title`、项目资源、账号素材和本地兜底图层,刷新后不得退回模板名。图标图集与角色动作请求同样兼容该字段,中间原图使用主名称加固定后缀;图标拆分素材按全连通域视觉顺序自动命名为 `素材 N`。
|
||||
- 生成面板不展示资源名称输入,默认使用原有自动编号;提示词输入保持统一可见边框。图片生成、快速编辑、重绘、修改图片、视频、音频、角色、图标、UI / 自定义规范、角色动画和宣发素材描述统一直接使用公共 `AutoGrowTextArea`,不经 `PlatformTextField` 的 textarea 分支;`AutoGrowTextArea` 负责尺寸、溢出及统一的 disabled 光标 / 透明度反馈,画布内生成面板统一由 `image-canvas-editor__generation-prompt` 承载边框、背景、padding、圆角和 focus chrome,独立修改图片弹窗继续使用 `image-canvas-editor__generate-prompt`,用途专属类只追加最小高度等差异,不得新建第三套 textarea chrome。两套 chrome 均通过 `--auto-grow-scrollbar-radius` 与内部滚动条共用圆角;WebKit 轨道按该圆角上下内缩,滑块在透明横向 gutter 内保持浏览器原生长度比例,拉至顶部或底部不得越过输入框边界。Enter 继续输入换行而不提交表单。内部命名契约仍使用可选 `assetLabel`,最大 80 字符并在提交时 trim;历史状态或内部调用携带非空名称时,同一个名称必须贯穿 `assetLabel`、`canvasCompletion.title`、项目资源、账号素材和本地兜底图层,刷新后不得退回模板名。图标图集与角色动作请求同样兼容该字段,中间原图使用主名称加固定后缀;图标拆分素材按全连通域视觉顺序自动命名为 `素材 N`。
|
||||
- 画布 Agent 会话按“SpacetimeDB 元数据 + OSS 消息正文”存储:`editor_agent_conversation` 只保存 `conversationId/projectId/ownerUserId/title/messagesObjectKey/deleted/createdAt/updatedAt` 等会话元数据;消息正文整体保存为私有 OSS JSON 文档 `editor-agent/{conversationId}.json`。消息文档单对象上限为 2 MiB,同一会话的消息追加和工具结果回填由 api-server 按 `conversationId` 串行化,避免“读 OSS → 改消息 → 写 OSS”并发覆盖。前端只通过 api-server BFF 读取和发送会话,不直接读写 SpacetimeDB,也不直接读写 OSS。
|
||||
- Agent 消息附件只允许引用当前工程画布资源或账号素材库图片,来源类型为 `canvas_resource` / `library_asset`,最多 9 张。附件请求可携带展示用 `imageSrc/thumbnailSrc/objectKey/width/height/label`,但持久化真相仍以后端校验后的 resource / asset 行和 OSS 对象为准;不得把 Data URL、signed URL 或 blob URL 当作会话长期事实。
|
||||
- 前端不直接订阅 SpacetimeDB,统一通过 api-server 的 `/api/editor/projects*` BFF 读写。
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
- 当测试是为防止历史回归,应在测试名或邻近注释中说明防的是什么行为,而不是记录实现步骤。
|
||||
- 同一用例既要验证定时器调度参数,又要断言确定的中间帧或中间状态时,必须 mock 定时器回调或使用可控假时钟;不得让真实墙上时间在异步交互期间推进被断言的状态,否则本地通过的用例会在较慢 CI 中偶发失败。
|
||||
- 测试 React effect 中注册的事件监听时,触发事件前先用可观测的 listener 调用确认注册已完成;异步请求已开始应用有界 `waitFor` 断言确认,不要用无界手工 Promise 等待一次性信号。解除挂起请求时将 Promise 收尾纳入异步 `act`,确保后续 React 更新在断言前已冲刷。
|
||||
- 公共 `AutoGrowTextArea` 使用 Lexical `contenteditable`,业务测试通过 `setPlainTextEditorValue` 写入文本、通过 `getPlainTextEditorHost` 断言外层样式;占位文案是独立渲染节点,不读取原生 `placeholder` 属性,也不对编辑根触发 textarea 专属的 `change` 事件。
|
||||
- 异步请求失败时,错误状态与调用方的草稿 / 附件恢复可能分属连续两次 React 更新;用例必须对最终恢复结果使用有界 `waitFor`,不能把错误文案刚出现的中间帧当成恢复已经完成。
|
||||
|
||||
## 试点调整
|
||||
|
||||
|
||||
Generated
+29
@@ -30,6 +30,8 @@
|
||||
"lexical": "^0.47.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"overlayscrollbars": "2.16.0",
|
||||
"overlayscrollbars-react": "0.5.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -15895,6 +15897,22 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/overlayscrollbars": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.16.0.tgz",
|
||||
"integrity": "sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/overlayscrollbars-react": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz",
|
||||
"integrity": "sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"overlayscrollbars": "^2.0.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -31502,6 +31520,17 @@
|
||||
"wcwidth": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"overlayscrollbars": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.16.0.tgz",
|
||||
"integrity": "sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg=="
|
||||
},
|
||||
"overlayscrollbars-react": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz",
|
||||
"integrity": "sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw==",
|
||||
"requires": {}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
|
||||
@@ -191,6 +191,8 @@
|
||||
"lexical": "^0.47.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"overlayscrollbars": "2.16.0",
|
||||
"overlayscrollbars-react": "0.5.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { act } from '@testing-library/react';
|
||||
import {
|
||||
$createLineBreakNode,
|
||||
$createParagraphNode,
|
||||
$createTextNode,
|
||||
$getRoot,
|
||||
getNearestEditorFromDOMNode,
|
||||
} from 'lexical';
|
||||
|
||||
export function setPlainTextEditorValue(
|
||||
contentEditable: HTMLElement,
|
||||
value: string,
|
||||
) {
|
||||
const editor = getNearestEditorFromDOMNode(contentEditable);
|
||||
if (!editor) {
|
||||
throw new Error('Expected a Lexical editor root');
|
||||
}
|
||||
|
||||
act(() => {
|
||||
editor.update(
|
||||
() => {
|
||||
const paragraph = $createParagraphNode();
|
||||
value.split('\n').forEach((line, index, lines) => {
|
||||
if (line) {
|
||||
paragraph.append($createTextNode(line));
|
||||
}
|
||||
if (index < lines.length - 1) {
|
||||
paragraph.append($createLineBreakNode());
|
||||
}
|
||||
});
|
||||
$getRoot().clear().append(paragraph);
|
||||
paragraph.selectEnd();
|
||||
},
|
||||
{ discrete: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function getPlainTextEditorHost(contentEditable: HTMLElement) {
|
||||
const host = contentEditable.closest<HTMLElement>(
|
||||
'.auto-grow-text-area',
|
||||
);
|
||||
if (!host) {
|
||||
throw new Error('Expected an auto-grow plain-text editor host');
|
||||
}
|
||||
return host;
|
||||
}
|
||||
@@ -1,288 +1,175 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AutoGrowTextArea } from './AutoGrowTextArea';
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
beforeEach(() => {
|
||||
Range.prototype.getBoundingClientRect = vi.fn(() => new DOMRect());
|
||||
Range.prototype.getClientRects = vi.fn(() => [] as unknown as DOMRectList);
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
vi.stubGlobal(
|
||||
'ClipboardEvent',
|
||||
class ClipboardEvent extends Event {
|
||||
clipboardData: DataTransfer;
|
||||
|
||||
constructor(
|
||||
type: string,
|
||||
init: EventInit & { clipboardData: DataTransfer },
|
||||
) {
|
||||
super(type, init);
|
||||
this.clipboardData = init.clipboardData;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function pastePlainText(editor: HTMLElement, value: string) {
|
||||
const clipboardData = {
|
||||
files: [],
|
||||
getData: (type: string) => (type === 'text/plain' ? value : ''),
|
||||
items: [],
|
||||
types: ['text/plain'],
|
||||
} as unknown as DataTransfer;
|
||||
fireEvent(
|
||||
editor,
|
||||
new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function ControlledEditor({
|
||||
initialValue = '',
|
||||
maxLength,
|
||||
}: {
|
||||
initialValue?: string;
|
||||
maxLength?: number;
|
||||
}) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
return (
|
||||
<>
|
||||
<AutoGrowTextArea
|
||||
aria-label="受控编辑器"
|
||||
value={value}
|
||||
maxLength={maxLength}
|
||||
onValueChange={setValue}
|
||||
/>
|
||||
<output aria-label="React 文本状态">{value}</output>
|
||||
<button type="button" onClick={() => setValue('外部\n更新')}>
|
||||
外部更新
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
describe('AutoGrowTextArea', () => {
|
||||
it('uses native field sizing when the browser supports it', () => {
|
||||
const supports = vi.fn().mockReturnValue(true);
|
||||
const resizeObserver = vi.fn();
|
||||
const onChange = vi.fn();
|
||||
vi.stubGlobal('CSS', { supports });
|
||||
vi.stubGlobal('ResizeObserver', resizeObserver);
|
||||
it('keeps React state authoritative and preserves exact line breaks', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ControlledEditor initialValue={'第一行\n第二行'} />);
|
||||
|
||||
render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="自动增长输入框"
|
||||
value=""
|
||||
onChange={onChange}
|
||||
/>,
|
||||
const editor = screen.getByRole('textbox', { name: '受控编辑器' });
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe(
|
||||
'第一行\n第二行',
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText(
|
||||
'自动增长输入框',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(supports).toHaveBeenCalledWith('field-sizing', 'content');
|
||||
expect(resizeObserver).not.toHaveBeenCalled();
|
||||
expect(input.getAttribute('rows')).toBe('1');
|
||||
expect(input.className).toContain('auto-grow-text-area');
|
||||
expect(input.className).toContain('[field-sizing:content]');
|
||||
expect(input.className).toContain('min-h-10');
|
||||
expect(input.className).toContain('max-h-32');
|
||||
expect(input.className).toContain('overflow-y-auto');
|
||||
expect(input.className).toContain('disabled:cursor-not-allowed');
|
||||
expect(input.className).toContain('disabled:opacity-60');
|
||||
expect(input.style.height).toBe('');
|
||||
|
||||
fireEvent.change(input, { target: { value: '新内容' } });
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
await user.click(screen.getByRole('button', { name: '外部更新' }));
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe(
|
||||
'外部\n更新',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to measured sizing, preserves css limits and remeasures width changes', () => {
|
||||
let resizeObserverCallback!: ResizeObserverCallback;
|
||||
const observe = vi.fn();
|
||||
const disconnect = vi.fn();
|
||||
vi.stubGlobal('CSS', { supports: vi.fn().mockReturnValue(false) });
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
vi.fn().mockImplementation((callback: ResizeObserverCallback) => {
|
||||
resizeObserverCallback = callback;
|
||||
return {
|
||||
observe,
|
||||
unobserve: vi.fn(),
|
||||
disconnect,
|
||||
};
|
||||
}),
|
||||
it('publishes edits through onValueChange and enforces maxLength', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ControlledEditor maxLength={4} />);
|
||||
|
||||
const editor = screen.getByRole('textbox', { name: '受控编辑器' });
|
||||
await user.click(editor);
|
||||
pastePlainText(editor, '12345');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe('1234'),
|
||||
);
|
||||
expect(editor.textContent).toBe('1234');
|
||||
});
|
||||
|
||||
it('supports undo and redo while keeping React state synchronized', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ControlledEditor />);
|
||||
|
||||
const editor = screen.getByRole('textbox', { name: '受控编辑器' });
|
||||
await user.click(editor);
|
||||
pastePlainText(editor, '草稿');
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe('草稿'),
|
||||
);
|
||||
|
||||
const { rerender, unmount } = render(
|
||||
await user.keyboard('{Control>}z{/Control}');
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe('');
|
||||
|
||||
await user.keyboard('{Control>}{Shift>}z{/Shift}{/Control}');
|
||||
expect(screen.getByLabelText('React 文本状态').textContent).toBe('草稿');
|
||||
});
|
||||
|
||||
it('exposes read-only and disabled accessibility without form controls', () => {
|
||||
const { container } = render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="自动增长输入框"
|
||||
value=""
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '80px',
|
||||
maxHeight: '128px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
aria-label="只读编辑器"
|
||||
value="只读内容"
|
||||
disabled
|
||||
readOnly
|
||||
onValueChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByLabelText(
|
||||
'自动增长输入框',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(observe).toHaveBeenCalledWith(input);
|
||||
let contentHeight = 92;
|
||||
Object.defineProperty(input, 'offsetHeight', {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
const styleHeight = Number.parseFloat(input.style.height);
|
||||
return Number.isNaN(styleHeight) ? 82 : styleHeight;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(input, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => input.offsetHeight - 2,
|
||||
});
|
||||
Object.defineProperty(input, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: () => contentHeight,
|
||||
});
|
||||
|
||||
rerender(
|
||||
const editor = screen.getByRole('textbox', { name: '只读编辑器' });
|
||||
expect(editor.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(editor.getAttribute('aria-readonly')).toBe('true');
|
||||
expect(editor.getAttribute('tabindex')).toBe('-1');
|
||||
expect(container.querySelector('textarea')).toBeNull();
|
||||
expect(container.querySelector('form')).toBeNull();
|
||||
});
|
||||
|
||||
it('uses a distinct internal viewport for overflow content', async () => {
|
||||
const { container } = render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="自动增长输入框"
|
||||
value="第一行\n第二行\n第三行\n第四行\n第五行"
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '80px',
|
||||
maxHeight: '128px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
aria-label="滚动编辑器"
|
||||
value={Array.from({ length: 20 }, (_, index) => `第 ${index + 1} 行`).join(
|
||||
'\n',
|
||||
)}
|
||||
onValueChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(input.style.height).toBe('94px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
|
||||
contentHeight = 180;
|
||||
act(() => {
|
||||
resizeObserverCallback(
|
||||
[
|
||||
{
|
||||
target: input,
|
||||
contentRect: { width: 180 },
|
||||
} as unknown as ResizeObserverEntry,
|
||||
],
|
||||
{} as ResizeObserver,
|
||||
const viewport = await waitFor(() => {
|
||||
const element = container.querySelector<HTMLElement>(
|
||||
'[data-overlayscrollbars-viewport]',
|
||||
);
|
||||
expect(element).not.toBeNull();
|
||||
return element!;
|
||||
});
|
||||
expect(input.style.height).toBe('128px');
|
||||
expect(input.style.overflowY).toBe('auto');
|
||||
|
||||
contentHeight = 30;
|
||||
rerender(
|
||||
<AutoGrowTextArea
|
||||
aria-label="自动增长输入框"
|
||||
value="短内容"
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '80px',
|
||||
maxHeight: '128px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
const content = container.querySelector<HTMLElement>(
|
||||
'[data-overlayscrollbars-content]',
|
||||
);
|
||||
expect(input.style.height).toBe('80px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
|
||||
unmount();
|
||||
expect(disconnect).toHaveBeenCalledTimes(1);
|
||||
expect(viewport).not.toBe(content);
|
||||
expect(
|
||||
viewport.contains(
|
||||
screen.getByRole('textbox', { name: '滚动编辑器' }),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('remeasures uncontrolled input changes and forwards the consumer input handler', () => {
|
||||
const onInput = vi.fn();
|
||||
vi.stubGlobal('CSS', { supports: vi.fn().mockReturnValue(false) });
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
})),
|
||||
);
|
||||
|
||||
render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="非受控自动增长输入框"
|
||||
defaultValue="初始内容"
|
||||
style={{
|
||||
boxSizing: 'border-box',
|
||||
minHeight: '40px',
|
||||
maxHeight: '128px',
|
||||
}}
|
||||
onInput={onInput}
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByLabelText(
|
||||
'非受控自动增长输入框',
|
||||
) as HTMLTextAreaElement;
|
||||
let contentHeight = 90;
|
||||
Object.defineProperty(input, 'offsetHeight', {
|
||||
configurable: true,
|
||||
get: () => {
|
||||
const styleHeight = Number.parseFloat(input.style.height);
|
||||
return Number.isNaN(styleHeight) ? 42 : styleHeight;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(input, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => input.offsetHeight - 2,
|
||||
});
|
||||
Object.defineProperty(input, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: () => contentHeight,
|
||||
});
|
||||
|
||||
fireEvent.input(input, {
|
||||
target: { value: '第一行\n第二行\n第三行\n第四行' },
|
||||
});
|
||||
expect(input.style.height).toBe('92px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
|
||||
contentHeight = 180;
|
||||
fireEvent.input(input, {
|
||||
target: { value: '很多行内容'.repeat(30) },
|
||||
});
|
||||
expect(input.style.height).toBe('128px');
|
||||
expect(input.style.overflowY).toBe('auto');
|
||||
|
||||
contentHeight = 20;
|
||||
fireEvent.input(input, { target: { value: '短内容' } });
|
||||
expect(input.style.height).toBe('40px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
expect(onInput).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('measures content-box height without double-counting padding or borders', () => {
|
||||
vi.stubGlobal('CSS', { supports: vi.fn().mockReturnValue(false) });
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
})),
|
||||
);
|
||||
|
||||
const { rerender } = render(
|
||||
<AutoGrowTextArea
|
||||
aria-label="内容盒输入框"
|
||||
value=""
|
||||
style={{
|
||||
boxSizing: 'content-box',
|
||||
minHeight: '40px',
|
||||
maxHeight: '128px',
|
||||
paddingTop: '10px',
|
||||
paddingBottom: '6px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const input = screen.getByLabelText(
|
||||
'内容盒输入框',
|
||||
) as HTMLTextAreaElement;
|
||||
let contentHeight = 120;
|
||||
Object.defineProperty(input, 'offsetHeight', {
|
||||
configurable: true,
|
||||
get: () => 122,
|
||||
});
|
||||
Object.defineProperty(input, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => 120,
|
||||
});
|
||||
Object.defineProperty(input, 'scrollHeight', {
|
||||
configurable: true,
|
||||
get: () => contentHeight,
|
||||
});
|
||||
|
||||
rerender(
|
||||
<AutoGrowTextArea
|
||||
aria-label="内容盒输入框"
|
||||
value="多行内容"
|
||||
style={{
|
||||
boxSizing: 'content-box',
|
||||
minHeight: '40px',
|
||||
maxHeight: '128px',
|
||||
paddingTop: '10px',
|
||||
paddingBottom: '6px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(input.style.height).toBe('104px');
|
||||
expect(input.style.overflowY).toBe('hidden');
|
||||
|
||||
contentHeight = 160;
|
||||
rerender(
|
||||
<AutoGrowTextArea
|
||||
aria-label="内容盒输入框"
|
||||
value="更多多行内容"
|
||||
style={{
|
||||
boxSizing: 'content-box',
|
||||
minHeight: '40px',
|
||||
maxHeight: '128px',
|
||||
paddingTop: '10px',
|
||||
paddingBottom: '6px',
|
||||
}}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(input.style.height).toBe('128px');
|
||||
expect(input.style.overflowY).toBe('auto');
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+61
-55
@@ -16,6 +16,7 @@ import type {
|
||||
EditorAgentMessage,
|
||||
EditorAgentMessageResponse,
|
||||
} from '@/packages/shared/src/contracts';
|
||||
import { setPlainTextEditorValue } from '@/src/components/common/AutoGrowTextArea.test-utils.ts';
|
||||
import type { EditorAgentConversationClient } from '@/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts';
|
||||
import { EDITOR_AGENT_PATIENCE_NOTICE_DELAY_MS } from '@/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts';
|
||||
import { ImageCanvasActionsProvider } from '@/src/components/image-editor/ImageCanvasActionsProvider.tsx';
|
||||
@@ -135,9 +136,10 @@ function createClient(): EditorAgentConversationClient {
|
||||
}
|
||||
|
||||
function enterAttachmentPrompt() {
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: ATTACHMENT_PROMPT },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
ATTACHMENT_PROMPT,
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
@@ -294,9 +296,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
name: '新建对话',
|
||||
}) as HTMLButtonElement;
|
||||
expect(newConversationButton.disabled).toBe(false);
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '应保留到新会话的草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'应保留到新会话的草稿',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加附件' }));
|
||||
const attachmentDialog = screen.getByRole('dialog', {
|
||||
name: '选择图片附件',
|
||||
@@ -332,25 +335,27 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
).toBe('conversation-2');
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
});
|
||||
expect(
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('应保留到新会话的草稿');
|
||||
expect(screen.getByLabelText('发送给画布 Agent').textContent).toBe(
|
||||
'应保留到新会话的草稿',
|
||||
);
|
||||
expect(
|
||||
screen.getByText('一二三四五六七八九十甲乙丙丁戊己庚辛壬癸子丑寅卯'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('option', { name: '角色参考' })).toBeTruthy();
|
||||
expect(screen.getByRole('option', { name: '新对话' })).toBeTruthy();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '尚未发送的草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'尚未发送的草稿',
|
||||
);
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
fireEvent.click(newConversationButton);
|
||||
expect(client.createConversation).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '参考附件做像素风' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'参考附件做像素风',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -491,9 +496,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
}) as HTMLButtonElement;
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '未发送内容' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'未发送内容',
|
||||
);
|
||||
expect(newConversationButton.disabled).toBe(true);
|
||||
fireEvent.click(newConversationButton);
|
||||
expect(client.createConversation).not.toHaveBeenCalled();
|
||||
@@ -516,17 +522,18 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '需要保留的草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'需要保留的草稿',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建对话' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('创建新会话失败')).toBeTruthy();
|
||||
});
|
||||
expect(
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('需要保留的草稿');
|
||||
expect(screen.getByLabelText('发送给画布 Agent').textContent).toBe(
|
||||
'需要保留的草稿',
|
||||
);
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
expect((screen.getByLabelText('当前对话') as HTMLSelectElement).value).toBe(
|
||||
'conversation-1',
|
||||
@@ -556,15 +563,13 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
});
|
||||
const draftInput = screen.getByLabelText(
|
||||
'发送给画布 Agent',
|
||||
) as HTMLTextAreaElement;
|
||||
fireEvent.change(draftInput, {
|
||||
target: { value: '旧项目草稿' },
|
||||
});
|
||||
) as HTMLElement;
|
||||
setPlainTextEditorValue(draftInput, '旧项目草稿');
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建对话' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(client.createConversation).toHaveBeenCalledWith('project-1', {});
|
||||
expect(draftInput.disabled).toBe(false);
|
||||
expect(draftInput.getAttribute('aria-disabled')).toBeNull();
|
||||
});
|
||||
const conversationSelect = screen.getByLabelText(
|
||||
'当前对话',
|
||||
@@ -578,12 +583,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
(screen.getByRole('button', { name: '添加附件' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(false);
|
||||
fireEvent.change(draftInput, {
|
||||
target: { value: '创建期间更新的草稿' },
|
||||
});
|
||||
fireEvent.submit(draftInput.closest('form') as HTMLFormElement);
|
||||
setPlainTextEditorValue(draftInput, '创建期间更新的草稿');
|
||||
fireEvent.click(sendButton);
|
||||
expect(client.sendMessage).not.toHaveBeenCalled();
|
||||
expect(draftInput.value).toBe('创建期间更新的草稿');
|
||||
expect(draftInput.textContent).toBe('创建期间更新的草稿');
|
||||
|
||||
await act(async () => {
|
||||
resolveCreate({
|
||||
@@ -596,7 +599,7 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
});
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(draftInput.value).toBe('创建期间更新的草稿');
|
||||
expect(draftInput.textContent).toBe('创建期间更新的草稿');
|
||||
expect(conversationSelect.value).toBe('conversation-2');
|
||||
expect(conversationSelect.disabled).toBe(false);
|
||||
expect(sendButton.disabled).toBe(false);
|
||||
@@ -659,13 +662,11 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
});
|
||||
const draftInput = screen.getByLabelText(
|
||||
'发送给画布 Agent',
|
||||
) as HTMLTextAreaElement;
|
||||
) as HTMLElement;
|
||||
const conversationSelect = screen.getByLabelText(
|
||||
'当前对话',
|
||||
) as HTMLSelectElement;
|
||||
fireEvent.change(draftInput, {
|
||||
target: { value: '切换期间必须保留的草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(draftInput, '切换期间必须保留的草稿');
|
||||
fireEvent.change(conversationSelect, {
|
||||
target: { value: 'conversation-2' },
|
||||
});
|
||||
@@ -677,9 +678,9 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
expect(client.getConversation).toHaveBeenCalledWith('conversation-2');
|
||||
expect(sendButton.disabled).toBe(true);
|
||||
});
|
||||
fireEvent.submit(draftInput.closest('form') as HTMLFormElement);
|
||||
fireEvent.click(sendButton);
|
||||
expect(client.sendMessage).not.toHaveBeenCalled();
|
||||
expect(draftInput.value).toBe('切换期间必须保留的草稿');
|
||||
expect(draftInput.textContent).toBe('切换期间必须保留的草稿');
|
||||
|
||||
await act(async () => {
|
||||
resolveConversationLoad({
|
||||
@@ -702,7 +703,7 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(await screen.findByText('背景会话内容')).toBeTruthy();
|
||||
expect(draftInput.value).toBe('切换期间必须保留的草稿');
|
||||
expect(draftInput.textContent).toBe('切换期间必须保留的草稿');
|
||||
expect(sendButton.disabled).toBe(false);
|
||||
});
|
||||
|
||||
@@ -729,9 +730,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
});
|
||||
vi.useFakeTimers();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '继续规划' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'继续规划',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await act(async () => {
|
||||
@@ -1316,7 +1318,7 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
}) as HTMLButtonElement;
|
||||
expect(sendButton.disabled).toBe(true);
|
||||
|
||||
fireEvent.submit(sendButton.closest('form')!);
|
||||
fireEvent.click(sendButton);
|
||||
|
||||
expect(client.sendMessage).not.toHaveBeenCalled();
|
||||
expect(screen.getByText('角色图层')).toBeTruthy();
|
||||
@@ -1368,15 +1370,18 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
fireEvent.click(
|
||||
within(attachmentDialog).getByRole('button', { name: '应用' }),
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '失败后恢复这条草稿' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'失败后恢复这条草稿',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
expect(await screen.findByText('Network error')).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('失败后恢复这条草稿');
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText('发送给画布 Agent').textContent).toBe(
|
||||
'失败后恢复这条草稿',
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('角色图层')).toBeTruthy();
|
||||
expect(
|
||||
within(
|
||||
@@ -1760,9 +1765,10 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
target: { value: '生成一张图片' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('发送给画布 Agent'),
|
||||
'生成一张图片',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }));
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
+7
-10
@@ -10,7 +10,6 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
type FormEvent,
|
||||
useEffect,
|
||||
useState,
|
||||
type WheelEvent as ReactWheelEvent,
|
||||
@@ -139,8 +138,7 @@ export function EditorAgentConversationPanelView({
|
||||
void createConversation().catch(() => undefined);
|
||||
};
|
||||
|
||||
const submitMessage = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const submitMessage = () => {
|
||||
if (isMessageSubmissionBlocked) {
|
||||
return;
|
||||
}
|
||||
@@ -328,10 +326,7 @@ export function EditorAgentConversationPanelView({
|
||||
{attachmentError}
|
||||
</div>
|
||||
) : null}
|
||||
<form
|
||||
className="border-t border-slate-200 bg-white/95 p-3"
|
||||
onSubmit={submitMessage}
|
||||
>
|
||||
<div className="border-t border-slate-200 bg-white/95 p-3">
|
||||
{attachments.length ? (
|
||||
<div className="mb-2 flex flex-wrap gap-1.5">
|
||||
{attachments.map((attachment) => (
|
||||
@@ -354,19 +349,21 @@ export function EditorAgentConversationPanelView({
|
||||
</button>
|
||||
<EditorAgentDraftTextarea
|
||||
value={draftText}
|
||||
onChange={setDraftText}
|
||||
onValueChange={setDraftText}
|
||||
onPaste={handleInputPaste}
|
||||
onSubmitRequest={submitMessage}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
type="button"
|
||||
className="inline-flex h-10 min-w-16 shrink-0 items-center justify-center gap-1.5 rounded-full bg-slate-900 px-3 text-sm font-semibold text-white disabled:opacity-45"
|
||||
disabled={isMessageSubmissionBlocked}
|
||||
onClick={submitMessage}
|
||||
>
|
||||
<Send className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
<AttachmentPicker
|
||||
open={attachmentPickerOpen}
|
||||
|
||||
+66
-40
@@ -1,60 +1,86 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import type { FormEvent } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { EditorAgentDraftTextarea } from './EditorAgentDraftTextarea.tsx';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal(
|
||||
'ResizeObserver',
|
||||
class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function DraftHarness({
|
||||
onPaste,
|
||||
onSubmitRequest,
|
||||
}: {
|
||||
onPaste?: (event: ClipboardEvent) => void;
|
||||
onSubmitRequest: () => void;
|
||||
}) {
|
||||
const [value, setValue] = useState('');
|
||||
return (
|
||||
<EditorAgentDraftTextarea
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
onPaste={onPaste}
|
||||
onSubmitRequest={onSubmitRequest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
describe('EditorAgentDraftTextarea', () => {
|
||||
it('forwards draft changes and paste events through the Agent wrapper', () => {
|
||||
const onChange = vi.fn();
|
||||
const onPaste = vi.fn();
|
||||
it('forwards controlled draft edits and paste events', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onPaste = vi.fn((event: ClipboardEvent) => event.preventDefault());
|
||||
render(<DraftHarness onPaste={onPaste} onSubmitRequest={vi.fn()} />);
|
||||
|
||||
render(
|
||||
<EditorAgentDraftTextarea
|
||||
value=""
|
||||
onChange={onChange}
|
||||
onPaste={onPaste}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText(
|
||||
'发送给画布 Agent',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(input.className).toContain('auto-grow-text-area');
|
||||
expect(input.className).toContain(
|
||||
'editor-agent-conversation__draft-input',
|
||||
);
|
||||
|
||||
fireEvent.change(input, { target: { value: '新草稿' } });
|
||||
expect(onChange).toHaveBeenCalledWith('新草稿');
|
||||
fireEvent.paste(input);
|
||||
const editor = screen.getByRole('textbox', {
|
||||
name: '发送给画布 Agent',
|
||||
});
|
||||
await user.click(editor);
|
||||
fireEvent.paste(editor, {
|
||||
clipboardData: {
|
||||
files: [],
|
||||
getData: () => '',
|
||||
items: [],
|
||||
types: [],
|
||||
},
|
||||
});
|
||||
expect(onPaste).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('submits on Enter and keeps line breaks or IME confirmation from submitting', () => {
|
||||
const onSubmit = vi.fn((event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
render(
|
||||
<form onSubmit={onSubmit}>
|
||||
<EditorAgentDraftTextarea value="草稿" onChange={vi.fn()} />
|
||||
</form>,
|
||||
it('submits on Enter but preserves Shift+Enter and IME confirmation', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSubmitRequest = vi.fn();
|
||||
const { container } = render(
|
||||
<DraftHarness onSubmitRequest={onSubmitRequest} />,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText('发送给画布 Agent');
|
||||
fireEvent.keyDown(input, { key: 'Enter', shiftKey: true });
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
const editor = screen.getByRole('textbox', {
|
||||
name: '发送给画布 Agent',
|
||||
});
|
||||
await user.click(editor);
|
||||
fireEvent.keyDown(editor, { key: 'Enter', shiftKey: true });
|
||||
expect(onSubmitRequest).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyDown(input, {
|
||||
fireEvent.keyDown(editor, {
|
||||
key: 'Enter',
|
||||
isComposing: false,
|
||||
isComposing: true,
|
||||
keyCode: 229,
|
||||
});
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
expect(onSubmitRequest).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
fireEvent.keyDown(editor, { key: 'Enter' });
|
||||
expect(onSubmitRequest).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelector('form')).toBeNull();
|
||||
expect(container.querySelector('textarea')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
import {
|
||||
type ClipboardEventHandler,
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
|
||||
import { AutoGrowTextArea } from '@/src/components/common/AutoGrowTextArea';
|
||||
|
||||
export function EditorAgentDraftTextarea({
|
||||
value,
|
||||
onChange,
|
||||
onValueChange,
|
||||
onPaste,
|
||||
onSubmitRequest,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onPaste?: ClipboardEventHandler<HTMLTextAreaElement>;
|
||||
onValueChange: (value: string) => void;
|
||||
onPaste?: (event: ClipboardEvent) => void;
|
||||
onSubmitRequest: () => void;
|
||||
}) {
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (
|
||||
event.key === 'Enter' &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing &&
|
||||
event.nativeEvent.keyCode !== 229
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.form?.requestSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoGrowTextArea
|
||||
className="editor-agent-conversation__draft-input min-w-0 flex-1 rounded-3xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-800 outline-none focus:border-slate-400"
|
||||
className="editor-agent-conversation__draft-input min-w-0 flex-1 border border-slate-200 bg-slate-50 text-sm text-slate-800 outline-none focus-within:border-slate-400"
|
||||
aria-label="发送给画布 Agent"
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.currentTarget.value)}
|
||||
onValueChange={onValueChange}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
onSubmitRequest={onSubmitRequest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
type ClipboardEvent as ReactClipboardEvent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
@@ -305,7 +304,7 @@ export function useConversationAttachments({
|
||||
);
|
||||
|
||||
const handleInputPaste = useCallback(
|
||||
(event: ReactClipboardEvent<HTMLTextAreaElement>) => {
|
||||
(event: ClipboardEvent) => {
|
||||
const imageFiles = extractClipboardImageFiles(event.clipboardData);
|
||||
if (!imageFiles.length || isPastingAttachmentRef.current) {
|
||||
return;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { createRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
|
||||
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
|
||||
import type {
|
||||
GenerateDialogState,
|
||||
@@ -83,9 +84,7 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '新的提示' },
|
||||
});
|
||||
setPlainTextEditorValue(screen.getByLabelText('生成提示词'), '新的提示');
|
||||
const panel = screen.getByRole('dialog', { name: '生成图片' });
|
||||
expect(
|
||||
within(panel).queryByRole('textbox', { name: '资源名称' }),
|
||||
@@ -216,7 +215,9 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
|
||||
name: '快速编辑提示词',
|
||||
});
|
||||
|
||||
expect(prompt.className).toContain('auto-grow-text-area');
|
||||
expect(prompt.closest('.auto-grow-text-area')?.className).toContain(
|
||||
'auto-grow-text-area',
|
||||
);
|
||||
expect(prompt.className).not.toContain('platform-text-field');
|
||||
fireEvent.keyDown(prompt, { key: 'Enter' });
|
||||
expect(submitQuickEdit).not.toHaveBeenCalled();
|
||||
|
||||
@@ -84,7 +84,7 @@ type ImageCanvasBasicGenerationComposerViewProps = {
|
||||
referenceAriaLabelFormatter?: ReferenceLabelFormatter;
|
||||
referenceRemoveLabelFormatter?: ReferenceLabelFormatter;
|
||||
showReferenceAddButton?: boolean;
|
||||
formClassName?: string;
|
||||
panelClassName?: string;
|
||||
footerClassName?: string;
|
||||
promptClassName?: string;
|
||||
referenceSlotClassName?: string;
|
||||
@@ -139,7 +139,7 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
referenceAriaLabelFormatter,
|
||||
referenceRemoveLabelFormatter,
|
||||
showReferenceAddButton = true,
|
||||
formClassName,
|
||||
panelClassName,
|
||||
footerClassName,
|
||||
promptClassName = 'image-canvas-editor__generation-prompt',
|
||||
referenceSlotClassName,
|
||||
@@ -176,8 +176,8 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
? '修改中'
|
||||
: submittingStatusLabel;
|
||||
const hasReferenceMenu = shouldIncludeReferences && onRequestUpload;
|
||||
const finalFormClassName =
|
||||
formClassName ??
|
||||
const finalPanelClassName =
|
||||
panelClassName ??
|
||||
'image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image';
|
||||
const finalFooterClassName =
|
||||
footerClassName ?? 'image-canvas-editor__generation-composer-footer';
|
||||
@@ -206,18 +206,12 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className={finalFormClassName}
|
||||
<div
|
||||
className={finalPanelClassName}
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label={resolvedDialogLabel}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{shouldIncludeReferences ? (
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
@@ -285,12 +279,12 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder={resolvedPromptPlaceholder}
|
||||
className={promptClassName}
|
||||
onChange={(event) =>
|
||||
onValueChange={(value) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
prompt: event.target.value,
|
||||
prompt: value,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
@@ -319,12 +313,12 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder="填写自定义画风,例如:90 年代复古像素风"
|
||||
className={`${promptClassName} image-canvas-editor__scene-custom-style`}
|
||||
onChange={(event) =>
|
||||
onValueChange={(value) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
sceneCustomStyle: event.target.value,
|
||||
sceneCustomStyle: value,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
@@ -358,6 +352,11 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
submitLabel={resolvedSubmitLabel}
|
||||
submitAriaLabel={resolvedSubmitAriaLabel}
|
||||
submitButtonClassName={submitButtonClassName}
|
||||
onSubmitRequest={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
styleControl={styleControl}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
@@ -385,7 +384,7 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
{dialog.errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
{hasReferenceMenu &&
|
||||
isGenerationReferenceMenuOpen &&
|
||||
generationReferenceButtonRef
|
||||
|
||||
@@ -4,9 +4,12 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
|
||||
import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView';
|
||||
import type { CanvasLayer, CharacterAnimationPanelState } from './ImageCanvasEditorTypes';
|
||||
|
||||
import type {
|
||||
CanvasLayer,
|
||||
CharacterAnimationPanelState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
|
||||
function createSourceLayer(): CanvasLayer {
|
||||
return {
|
||||
@@ -61,14 +64,11 @@ function CharacterAnimationPanelHarness({
|
||||
currentPanel
|
||||
? {
|
||||
...currentPanel,
|
||||
frameCount:
|
||||
frameCount === 48 ? 48 : frameCount === 40 ? 40 : 32,
|
||||
frameCount: frameCount === 48 ? 48 : frameCount === 40 ? 40 : 32,
|
||||
durationSeconds:
|
||||
frameCount === 48 ? 6 : frameCount === 40 ? 5 : 4,
|
||||
status:
|
||||
currentPanel.status === 'failed'
|
||||
? 'idle'
|
||||
: currentPanel.status,
|
||||
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
|
||||
errorMessage:
|
||||
currentPanel.status === 'failed'
|
||||
? undefined
|
||||
@@ -114,12 +114,12 @@ describe('ImageCanvasCharacterAnimationPanelView', () => {
|
||||
);
|
||||
|
||||
const description = screen.getByLabelText('动画描述');
|
||||
expect(description.className).toContain('auto-grow-text-area');
|
||||
expect(description.closest('.auto-grow-text-area')?.className).toContain(
|
||||
'auto-grow-text-area',
|
||||
);
|
||||
expect(description.className).not.toContain('platform-text-field');
|
||||
|
||||
fireEvent.change(description, {
|
||||
target: { value: `${'a'.repeat(4001)}` },
|
||||
});
|
||||
setPlainTextEditorValue(description, 'a'.repeat(4001));
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '动画参数 同图尺寸 · 4秒 · 480p' }),
|
||||
);
|
||||
@@ -128,7 +128,9 @@ describe('ImageCanvasCharacterAnimationPanelView', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '比例 16:9' }));
|
||||
|
||||
expect(screen.getByRole('menu', { name: '动画参数选项' })).toBeTruthy();
|
||||
expect(screen.getByLabelText('当前动画描述').textContent).toHaveLength(4000);
|
||||
expect(screen.getByLabelText('当前动画描述').textContent).toHaveLength(
|
||||
4000,
|
||||
);
|
||||
expect(screen.getByLabelText('当前分辨率').textContent).toBe('720p');
|
||||
expect(screen.getByLabelText('当前比例').textContent).toBe('16:9');
|
||||
expect(menu).toBeTruthy();
|
||||
@@ -142,9 +144,7 @@ describe('ImageCanvasCharacterAnimationPanelView', () => {
|
||||
});
|
||||
|
||||
it('clicks the parent animation panel to collapse the parameter menu', () => {
|
||||
render(
|
||||
<CharacterAnimationPanelHarness initialPanel={createPanel()} />,
|
||||
);
|
||||
render(<CharacterAnimationPanelHarness initialPanel={createPanel()} />);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '动画参数 同图尺寸 · 4秒 · 480p' }),
|
||||
@@ -211,15 +211,17 @@ describe('ImageCanvasCharacterAnimationPanelView', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成中' }));
|
||||
|
||||
const description = screen.getByLabelText(
|
||||
'动画描述',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(description.disabled).toBe(true);
|
||||
expect(description.className).toContain('disabled:cursor-not-allowed');
|
||||
expect(description.className).toContain('disabled:opacity-60');
|
||||
expect((screen.getByRole('button', { name: '待机' }) as HTMLButtonElement).disabled).toBe(
|
||||
true,
|
||||
);
|
||||
const description = screen.getByLabelText('动画描述');
|
||||
expect(description.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(
|
||||
description
|
||||
.closest('.auto-grow-text-area')
|
||||
?.getAttribute('data-disabled'),
|
||||
).toBe('true');
|
||||
expect(
|
||||
(screen.getByRole('button', { name: '待机' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
expect(submitCharacterAnimation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -161,18 +161,12 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__character-animation-panel image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--character-animation"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="角色动画生成面板"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!isGenerating) {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
<ImageCanvasReferenceSlot
|
||||
@@ -201,9 +195,7 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
disabled={isGenerating}
|
||||
placeholder="你希望角色做什么动作?"
|
||||
className="max-h-64 image-canvas-editor__generation-prompt image-canvas-editor__character-animation-textarea"
|
||||
onChange={(event) =>
|
||||
updatePanel({ promptText: event.target.value.slice(0, 4000) })
|
||||
}
|
||||
onValueChange={(value) => updatePanel({ promptText: value })}
|
||||
/>
|
||||
<div className="image-canvas-editor__character-animation-presets">
|
||||
{CHARACTER_ANIMATION_ACTION_PROMPTS.map((preset) => (
|
||||
@@ -244,12 +236,17 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="image-canvas-editor__character-animation-submit"
|
||||
disabled={isGenerating}
|
||||
onClick={() => {
|
||||
if (!isGenerating) {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isGenerating ? '生成中' : `生成${price}泥点`}
|
||||
</PlatformActionButton>
|
||||
@@ -281,7 +278,7 @@ export function ImageCanvasCharacterAnimationPanelView({
|
||||
{panel.errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
{isParameterMenuOpen
|
||||
? renderPanelPortal(
|
||||
<PlatformFloatingMenu
|
||||
|
||||
@@ -4,6 +4,10 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { createRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getPlainTextEditorHost,
|
||||
setPlainTextEditorValue,
|
||||
} from '../common/AutoGrowTextArea.test-utils';
|
||||
import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView';
|
||||
import type {
|
||||
GenerateDialogState,
|
||||
@@ -94,21 +98,26 @@ function CharacterGenerationHarness({
|
||||
|
||||
describe('ImageCanvasCharacterGenerationComposerView', () => {
|
||||
it('keeps the prompt as a bordered single text input with a question placeholder', () => {
|
||||
render(<CharacterGenerationHarness />);
|
||||
render(
|
||||
<CharacterGenerationHarness initialDialog={createDialog({ prompt: '' })} />,
|
||||
);
|
||||
|
||||
const panel = screen.getByRole('dialog', { name: '生成角色形象' });
|
||||
const prompt = screen.getByRole('textbox', { name: '角色设定' });
|
||||
const promptHost = getPlainTextEditorHost(prompt);
|
||||
|
||||
expect(
|
||||
Array.from(panel.querySelectorAll('.image-canvas-editor__field-title'))
|
||||
.map((node) => node.textContent)
|
||||
.join(''),
|
||||
).not.toContain('角色设定');
|
||||
expect(prompt.getAttribute('placeholder')).toBe('你希望角色如何设计?');
|
||||
expect(prompt.className).toContain('auto-grow-text-area');
|
||||
expect(prompt.className).not.toContain('platform-text-field');
|
||||
expect(prompt.className).toContain('image-canvas-editor__generation-prompt');
|
||||
expect(prompt.className).not.toContain(
|
||||
expect(screen.getByText('你希望角色如何设计?')).toBeTruthy();
|
||||
expect(promptHost.className).toContain('auto-grow-text-area');
|
||||
expect(promptHost.className).not.toContain('platform-text-field');
|
||||
expect(promptHost.className).toContain(
|
||||
'image-canvas-editor__generation-prompt',
|
||||
);
|
||||
expect(promptHost.className).not.toContain(
|
||||
'image-canvas-editor__generation-prompt--borderless',
|
||||
);
|
||||
});
|
||||
@@ -125,9 +134,10 @@ describe('ImageCanvasCharacterGenerationComposerView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('角色设定'), {
|
||||
target: { value: '新的角色设定' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('角色设定'),
|
||||
'新的角色设定',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
expect(screen.getByLabelText('当前角色设定').textContent).toBe(
|
||||
|
||||
@@ -111,18 +111,12 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__character-composer"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="生成角色形象"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
<ImageCanvasReferenceSlot
|
||||
@@ -255,25 +249,25 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<label className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
||||
<div className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
||||
<AutoGrowTextArea
|
||||
aria-label="角色设定"
|
||||
value={dialog.prompt}
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder="你希望角色如何设计?"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) =>
|
||||
onValueChange={(value) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'character'
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
prompt: event.target.value,
|
||||
prompt: value,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
@@ -300,10 +294,15 @@ export function ImageCanvasCharacterGenerationComposerView({
|
||||
})}
|
||||
submitLabel="生成"
|
||||
submitAriaLabel="生成"
|
||||
onSubmitRequest={() => {
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,18 +41,12 @@ export function ImageCanvasCropExpandPanelView({
|
||||
onSubmit,
|
||||
}: ImageCanvasCropExpandPanelViewProps) {
|
||||
return (
|
||||
<form
|
||||
<div
|
||||
className="image-canvas-editor__crop-expand-panel"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="裁扩图片"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (panel.status !== 'processing') {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__crop-expand-head">
|
||||
<strong>裁扩</strong>
|
||||
@@ -107,14 +101,19 @@ export function ImageCanvasCropExpandPanelView({
|
||||
取消
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
type="button"
|
||||
tone="primary"
|
||||
size="sm"
|
||||
disabled={panel.status === 'processing'}
|
||||
onClick={() => {
|
||||
if (panel.status !== 'processing') {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{panel.status === 'processing' ? '处理中' : '完成'}
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type ComponentProps, type ReactNode, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AuthUiContext } from '../auth/AuthUiContext';
|
||||
import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
|
||||
import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationModalView';
|
||||
import type { GenerateDialogState } from './ImageCanvasEditorTypes';
|
||||
|
||||
@@ -72,9 +73,7 @@ describe('ImageCanvasEditGenerationModalView', () => {
|
||||
expect(modal?.parentElement?.className).not.toContain(
|
||||
'platform-theme--dark',
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '新的修改提示' },
|
||||
});
|
||||
setPlainTextEditorValue(screen.getByLabelText('生成提示词'), '新的修改提示');
|
||||
fireEvent.click(screen.getByRole('button', { name: '修改3泥点' }));
|
||||
|
||||
expect(screen.getByLabelText('当前提示词').textContent).toBe(
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ImageCanvasEditGenerationModalView({
|
||||
submitLabel="修改"
|
||||
submitAriaLabel={`修改${editPrice}泥点`}
|
||||
submittingStatusLabel="修改中"
|
||||
formClassName="image-canvas-editor__generate-form"
|
||||
panelClassName="image-canvas-editor__generate-form"
|
||||
footerClassName="image-canvas-editor__generate-body"
|
||||
promptClassName="image-canvas-editor__generate-prompt"
|
||||
submitButtonClassName="image-canvas-editor__generate-submit"
|
||||
|
||||
@@ -12,6 +12,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
|
||||
import {
|
||||
ApiClientError,
|
||||
defaultEditorProjectLayers,
|
||||
@@ -554,9 +555,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(screen.queryByLabelText('图像生成占位图')).toBeNull();
|
||||
const generateDialog = screen.getByRole('dialog', { name: '生成图片' });
|
||||
expect(generateDialog).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByLabelText('生成提示词') as HTMLTextAreaElement).value,
|
||||
).toBe('刷新后恢复生成器');
|
||||
expect(screen.getByLabelText('生成提示词').textContent).toBe(
|
||||
'刷新后恢复生成器',
|
||||
);
|
||||
});
|
||||
|
||||
it('restores generated image composer from layer metadata when the dialog snapshot is missing', async () => {
|
||||
@@ -609,9 +610,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '生成图片' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByLabelText('生成提示词') as HTMLTextAreaElement).value,
|
||||
).toBe('元数据恢复生成器');
|
||||
expect(screen.getByLabelText('生成提示词').textContent).toBe(
|
||||
'元数据恢复生成器',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not open a generator on direct click for tagged uploaded images', async () => {
|
||||
@@ -781,11 +782,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
).value,
|
||||
).toBe('非对称对抗');
|
||||
expect(
|
||||
(
|
||||
within(publicationDialog).getByRole('textbox', {
|
||||
name: '运营海报一句话描述游戏',
|
||||
}) as HTMLTextAreaElement
|
||||
).value,
|
||||
within(publicationDialog).getByRole('textbox', {
|
||||
name: '运营海报一句话描述游戏',
|
||||
}).textContent,
|
||||
).toBe('找到钥匙,开门逃离马戏团');
|
||||
expect(within(publicationDialog).getByLabelText('首图参考')).toBeTruthy();
|
||||
expect(screen.getByLabelText('宣发素材生成占位图')).toBeTruthy();
|
||||
@@ -838,9 +837,11 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
.className,
|
||||
).toContain('image-canvas-editor__reference-chip');
|
||||
const generatePrompt = screen.getByLabelText('生成提示词');
|
||||
expect(generatePrompt.className).toContain('auto-grow-text-area');
|
||||
expect(generatePrompt.closest('.auto-grow-text-area')?.className).toContain(
|
||||
'auto-grow-text-area',
|
||||
);
|
||||
expect(generatePrompt.className).not.toContain('platform-text-field');
|
||||
expect(generatePrompt.className).toContain(
|
||||
expect(generatePrompt.closest('.auto-grow-text-area')?.className).toContain(
|
||||
'image-canvas-editor__generation-prompt',
|
||||
);
|
||||
expect(
|
||||
@@ -865,9 +866,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
).toContain('image-canvas-editor__generation-submit');
|
||||
expect(screen.getByRole('toolbar', { name: 'AI画布工具栏' })).toBeTruthy();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '一张明亮的拼图主视觉' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'一张明亮的拼图主视觉',
|
||||
);
|
||||
fireEvent.click(pixelArtToggle);
|
||||
fireEvent.click(
|
||||
within(generateDialog).getByRole('button', { name: '生成' }),
|
||||
@@ -890,9 +892,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
});
|
||||
const submittedGenerationInputs =
|
||||
generateEditorImageMock.mock.calls[0]?.[0]?.generationInputs;
|
||||
expect(JSON.stringify(submittedGenerationInputs)).not.toContain(
|
||||
'pixelArt',
|
||||
);
|
||||
expect(JSON.stringify(submittedGenerationInputs)).not.toContain('pixelArt');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
|
||||
@@ -979,9 +979,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
const draggedFrameCenterY =
|
||||
Number.parseFloat(draggedFrame.style.top) +
|
||||
Number.parseFloat(draggedFrame.style.height) / 2;
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '拖拽后的生成图' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'拖拽后的生成图',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1019,9 +1020,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
|
||||
const generateDialog = screen.getByRole('dialog', { name: '生成图片' });
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '生成中继续拖动的图片' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'生成中继续拖动的图片',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(generateDialog).getByRole('button', { name: '生成' }),
|
||||
);
|
||||
@@ -1098,9 +1100,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '准备删除的生成中图片' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'准备删除的生成中图片',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
const frame = screen.getByLabelText('图像生成占位图');
|
||||
@@ -1213,9 +1216,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '一张真实生成失败的图' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'一张真实生成失败的图',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
expect(screen.getByRole('status').textContent).toContain('生成中');
|
||||
@@ -1240,9 +1244,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '一张需要登录生成的图' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'一张需要登录生成的图',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1263,9 +1268,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
'1:1',
|
||||
'2K',
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '生成中的普通图片' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'生成中的普通图片',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
},
|
||||
dialogName: '生成图片',
|
||||
@@ -1284,9 +1290,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
screen.getByRole('menu', { name: '生成规范类型' }),
|
||||
).getByRole('menuitem', { name: '自定义规范' }),
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('自定义规范提示词'), {
|
||||
target: { value: '生成中的自定义规范图' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('自定义规范提示词'),
|
||||
'生成中的自定义规范图',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '生成规范' })).getByRole(
|
||||
'button',
|
||||
@@ -1306,9 +1313,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
'2:3',
|
||||
'2K',
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('角色设定'), {
|
||||
target: { value: '生成中的角色形象' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('角色设定'),
|
||||
'生成中的角色形象',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
},
|
||||
dialogName: '生成角色形象',
|
||||
@@ -1397,12 +1405,12 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
clientY: 120,
|
||||
},
|
||||
);
|
||||
fireEvent.change(
|
||||
setPlainTextEditorValue(
|
||||
within(screen.getByRole('dialog', { name: '生成图标素材' })).getByRole(
|
||||
'textbox',
|
||||
{ name: '素材描述' },
|
||||
),
|
||||
{ target: { value: '返回按钮' } },
|
||||
'返回按钮',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '生成图标素材' })).getByRole(
|
||||
@@ -1412,9 +1420,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('dialog', { name: '生成图标素材' }),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('dialog', { name: '生成图标素材' })).toBeNull();
|
||||
});
|
||||
const frame = screen.getByLabelText('图标素材生成占位图');
|
||||
expect(frame.className).toContain(
|
||||
@@ -1497,7 +1503,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
fireEvent.change(screen.getByLabelText('角色视角'), {
|
||||
target: { value: '左向三分之二侧身站姿' },
|
||||
});
|
||||
fireEvent.submit(specDialog);
|
||||
fireEvent.click(
|
||||
within(specDialog).getByRole('button', { name: '提交生成规范' }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
@@ -1689,9 +1697,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
const characterPanel = screen.getByRole('dialog', {
|
||||
name: '生成角色形象',
|
||||
});
|
||||
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
|
||||
target: { value: '高个子游侠' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
within(characterPanel).getByLabelText('角色设定'),
|
||||
'高个子游侠',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(characterPanel).getByRole('button', { name: '生成' }),
|
||||
);
|
||||
@@ -1784,9 +1793,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
});
|
||||
selectGenerationModel(characterPanel, 'gpt-image-2');
|
||||
selectGenerationDimensions(characterPanel, '2:3', '2K');
|
||||
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
|
||||
target: { value: '蓝衣剑士' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
within(characterPanel).getByLabelText('角色设定'),
|
||||
'蓝衣剑士',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(characterPanel).getByRole('button', { name: '生成' }),
|
||||
);
|
||||
@@ -1829,12 +1839,12 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
screen.getByRole('dialog', { name: '生成图标素材' }),
|
||||
'图标规范 2',
|
||||
);
|
||||
fireEvent.change(
|
||||
setPlainTextEditorValue(
|
||||
within(screen.getByRole('dialog', { name: '生成图标素材' })).getByRole(
|
||||
'textbox',
|
||||
{ name: '素材描述' },
|
||||
),
|
||||
{ target: { value: '返回按钮' } },
|
||||
'返回按钮',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '生成图标素材' })).getByRole(
|
||||
@@ -1910,9 +1920,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '生成中切换后仍保留位置' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'生成中切换后仍保留位置',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
const originalFrame = screen.getByLabelText('图像生成占位图');
|
||||
@@ -2078,16 +2089,16 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
iconPanel.querySelector('.image-canvas-editor__icon-spec-card'),
|
||||
).toBeTruthy();
|
||||
|
||||
fireEvent.change(
|
||||
setPlainTextEditorValue(
|
||||
within(iconPanel).getByRole('textbox', { name: '素材描述' }),
|
||||
{ target: { value: '返回按钮\n设置按钮\n提示按钮' } },
|
||||
'返回按钮\n设置按钮\n提示按钮',
|
||||
);
|
||||
|
||||
const iconPrompt = within(iconPanel).getByRole('textbox', {
|
||||
name: '素材描述',
|
||||
});
|
||||
expect(iconPrompt.tagName).toBe('TEXTAREA');
|
||||
expect(iconPrompt.className).toContain(
|
||||
expect(iconPrompt.getAttribute('contenteditable')).toBe('true');
|
||||
expect(iconPrompt.closest('.auto-grow-text-area')?.className).toContain(
|
||||
'image-canvas-editor__generation-prompt',
|
||||
);
|
||||
expect(iconPrompt.className).not.toContain(
|
||||
@@ -2118,9 +2129,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '发光蘑菇角色' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'发光蘑菇角色',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
const generatedImage = await screen.findByAltText(/画布图片:生成图片/u);
|
||||
@@ -2198,7 +2210,12 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
fireEvent.change(screen.getByLabelText('美术风格'), {
|
||||
target: { value: '糖果玻璃拟物' },
|
||||
});
|
||||
fireEvent.submit(screen.getByRole('dialog', { name: '生成规范' }));
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '生成规范' })).getByRole(
|
||||
'button',
|
||||
{ name: '提交生成规范' },
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
@@ -2248,10 +2265,16 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '自定义规范' }));
|
||||
fireEvent.change(screen.getByLabelText('自定义规范提示词'), {
|
||||
target: { value: ' 生成一张武器图标规范展板 ' },
|
||||
});
|
||||
fireEvent.submit(screen.getByRole('dialog', { name: '生成规范' }));
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('自定义规范提示词'),
|
||||
' 生成一张武器图标规范展板 ',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(screen.getByRole('dialog', { name: '生成规范' })).getByRole(
|
||||
'button',
|
||||
{ name: '提交生成规范' },
|
||||
),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
@@ -2480,9 +2503,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
const readyCharacterPanel = screen.getByRole('dialog', {
|
||||
name: '生成角色形象',
|
||||
});
|
||||
fireEvent.change(within(readyCharacterPanel).getByLabelText('角色设定'), {
|
||||
target: { value: '银发游侠,蓝色披风,弓箭手,适合像素风战棋。' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
within(readyCharacterPanel).getByLabelText('角色设定'),
|
||||
'银发游侠,蓝色披风,弓箭手,适合像素风战棋。',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(readyCharacterPanel).getByRole('button', { name: '生成' }),
|
||||
);
|
||||
@@ -2495,10 +2519,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
referenceImageSrcs: [
|
||||
'resource-character-spec',
|
||||
'resource-big-fish',
|
||||
],
|
||||
referenceImageSrcs: ['resource-character-spec', 'resource-big-fish'],
|
||||
projectId: 'editor-project-default',
|
||||
assetFolderId: 'project',
|
||||
assetKind: 'character',
|
||||
@@ -2633,11 +2654,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
within(iconPanel).getByRole('button', { name: '图标规范' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
(
|
||||
within(iconPanel).getByRole('textbox', {
|
||||
name: '素材描述',
|
||||
}) as HTMLTextAreaElement
|
||||
).value,
|
||||
within(iconPanel).getByRole('textbox', {
|
||||
name: '素材描述',
|
||||
}).textContent,
|
||||
).toBe('');
|
||||
|
||||
fireEvent.click(
|
||||
@@ -2677,11 +2696,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
screen.queryByText('请选择画布中的图标规范,按 Esc 退出'),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.change(
|
||||
setPlainTextEditorValue(
|
||||
within(iconPanel).getByRole('textbox', { name: '素材描述' }),
|
||||
{
|
||||
target: { value: '返回按钮\n设置按钮' },
|
||||
},
|
||||
'返回按钮\n设置按钮',
|
||||
);
|
||||
fireEvent.click(within(iconPanel).getByRole('button', { name: '生成' }));
|
||||
|
||||
@@ -2854,11 +2871,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
clientY: 100,
|
||||
},
|
||||
);
|
||||
fireEvent.change(
|
||||
setPlainTextEditorValue(
|
||||
within(iconPanel).getByRole('textbox', { name: '素材描述' }),
|
||||
{
|
||||
target: { value: '返回按钮' },
|
||||
},
|
||||
'返回按钮',
|
||||
);
|
||||
fireEvent.click(within(iconPanel).getByRole('button', { name: '生成' }));
|
||||
|
||||
@@ -3139,11 +3154,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
screen.queryByText('请选择画布中的图标规范,按 Esc 退出'),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.change(
|
||||
setPlainTextEditorValue(
|
||||
within(iconPanel).getByRole('textbox', { name: '素材描述' }),
|
||||
{
|
||||
target: { value: '背包按钮' },
|
||||
},
|
||||
'背包按钮',
|
||||
);
|
||||
fireEvent.click(within(iconPanel).getByRole('button', { name: '生成' }));
|
||||
|
||||
@@ -3344,21 +3357,23 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
).toBeTruthy();
|
||||
}
|
||||
fireEvent.click(within(panel).getByRole('button', { name: '待机' }));
|
||||
expect(
|
||||
(within(panel).getByLabelText('动画描述') as HTMLTextAreaElement).value,
|
||||
).toContain('待机');
|
||||
expect(within(panel).getByLabelText('动画描述').textContent).toContain(
|
||||
'待机',
|
||||
);
|
||||
const longPrompt = '走'.repeat(4100);
|
||||
fireEvent.change(within(panel).getByLabelText('动画描述'), {
|
||||
target: { value: longPrompt },
|
||||
});
|
||||
expect(
|
||||
(within(panel).getByLabelText('动画描述') as HTMLTextAreaElement).value,
|
||||
).toHaveLength(4000);
|
||||
setPlainTextEditorValue(
|
||||
within(panel).getByLabelText('动画描述'),
|
||||
longPrompt,
|
||||
);
|
||||
expect(within(panel).getByLabelText('动画描述').textContent).toHaveLength(
|
||||
4000,
|
||||
);
|
||||
const precisePrompt =
|
||||
'The elderly market woman gently shifts weight while the basket sways.';
|
||||
fireEvent.change(within(panel).getByLabelText('动画描述'), {
|
||||
target: { value: precisePrompt },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
within(panel).getByLabelText('动画描述'),
|
||||
precisePrompt,
|
||||
);
|
||||
expect(
|
||||
within(panel).getByLabelText(`生成文本:${precisePrompt}`),
|
||||
).toBeTruthy();
|
||||
@@ -3428,10 +3443,9 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
const remodelPanel = screen.getByRole('dialog', {
|
||||
name: '角色动画生成面板',
|
||||
});
|
||||
expect(
|
||||
(within(remodelPanel).getByLabelText('动画描述') as HTMLTextAreaElement)
|
||||
.value,
|
||||
).toBe(precisePrompt);
|
||||
expect(within(remodelPanel).getByLabelText('动画描述').textContent).toBe(
|
||||
precisePrompt,
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts UI design assets from the floating toolbar and keeps the spritesheet on canvas', async () => {
|
||||
@@ -3687,9 +3701,10 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
render(<ImageCanvasEditorView />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成图片' }));
|
||||
fireEvent.change(screen.getByLabelText('生成提示词'), {
|
||||
target: { value: '一张可修改的生成图' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('生成提示词'),
|
||||
'一张可修改的生成图',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import JSZip from 'jszip';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { usePlatformWalletStore } from '../../stores/usePlatformWalletStore';
|
||||
import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
|
||||
import type { EditorAgentConversationClient } from './EditorAgentConversation/useEditorAgentConversation';
|
||||
import {
|
||||
ApiClientError,
|
||||
@@ -895,11 +896,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
(
|
||||
within(quickEditDialog).getByLabelText(
|
||||
'快速编辑提示词',
|
||||
) as HTMLTextAreaElement
|
||||
).value,
|
||||
within(quickEditDialog).getByLabelText('快速编辑提示词').textContent,
|
||||
).toContain('对1号红色圈选框里的内容做以下修改');
|
||||
|
||||
dispatchPointerEvent(selectionCanvas, 'pointerdown', {
|
||||
@@ -977,11 +974,11 @@ describe('ImageCanvasEditorView', () => {
|
||||
clientY: 110,
|
||||
});
|
||||
|
||||
const resumedPrompt = screen.getByLabelText(
|
||||
'快速编辑提示词',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(resumedPrompt.value).toContain('对1号红色圈选框里的内容做以下修改');
|
||||
expect(resumedPrompt.value).not.toContain('2号红色圈选框');
|
||||
const resumedPrompt = screen.getByLabelText('快速编辑提示词');
|
||||
expect(resumedPrompt.textContent).toContain(
|
||||
'对1号红色圈选框里的内容做以下修改',
|
||||
);
|
||||
expect(resumedPrompt.textContent).not.toContain('2号红色圈选框');
|
||||
});
|
||||
|
||||
it('locks quick edit selection tools after the edit task starts', async () => {
|
||||
@@ -1002,9 +999,10 @@ describe('ImageCanvasEditorView', () => {
|
||||
const quickEditDialog = screen.getByRole('dialog', {
|
||||
name: '快速编辑图片',
|
||||
});
|
||||
fireEvent.change(within(quickEditDialog).getByLabelText('快速编辑提示词'), {
|
||||
target: { value: '快速修图' },
|
||||
});
|
||||
setPlainTextEditorValue(
|
||||
within(quickEditDialog).getByLabelText('快速编辑提示词'),
|
||||
'快速修图',
|
||||
);
|
||||
fireEvent.click(
|
||||
within(
|
||||
screen.getByRole('toolbar', { name: '快速编辑框选工具' }),
|
||||
@@ -1253,9 +1251,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
await waitFor(() => {
|
||||
expect(exportedBlob).toBeTruthy();
|
||||
});
|
||||
expect(downloadName).toMatch(
|
||||
/^导出项目-画布素材-\d{8}-\d{6}\.zip$/u,
|
||||
);
|
||||
expect(downloadName).toMatch(/^导出项目-画布素材-\d{8}-\d{6}\.zip$/u);
|
||||
|
||||
const zip = await JSZip.loadAsync(exportedBlob!);
|
||||
expect(zip.file('导出项目-画布素材/images/001-素材 A.png')).toBeTruthy();
|
||||
@@ -1613,7 +1609,9 @@ describe('ImageCanvasEditorView', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '改造' }));
|
||||
|
||||
const panel = screen.getByRole('dialog', { name: '生成游戏音效' });
|
||||
expect(within(panel).getByDisplayValue('金币跳出音')).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByRole('textbox', { name: 'prompt' }).textContent,
|
||||
).toBe('金币跳出音');
|
||||
expect(
|
||||
within(panel).getByRole('button', { name: '音效时长 8秒' }),
|
||||
).toBeTruthy();
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { setPlainTextEditorValue } from '../common/AutoGrowTextArea.test-utils';
|
||||
import { BACKGROUND_MUSIC_PROMPT_PRESETS } from './ImageCanvasBackgroundMusicPresetModel';
|
||||
import type { BackgroundMusicGenerationDialogState } from './ImageCanvasBackgroundMusicPromptModel';
|
||||
import type {
|
||||
@@ -242,7 +243,7 @@ function getBackgroundMusicPanel() {
|
||||
function getBackgroundMusicPromptTextarea() {
|
||||
return within(getBackgroundMusicPanel()).getByRole('textbox', {
|
||||
name: 'gpt_description_prompt',
|
||||
}) as HTMLTextAreaElement;
|
||||
});
|
||||
}
|
||||
|
||||
function getBackgroundMusicActionButton(name: string) {
|
||||
@@ -373,8 +374,6 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
expect(within(panel).queryByText('不得重新生成')).toBeNull();
|
||||
expect(within(panel).queryByText('gpt-image-2')).toBeNull();
|
||||
|
||||
fireEvent.submit(panel);
|
||||
|
||||
expect(onRetryPerfectPixelOperation).not.toHaveBeenCalled();
|
||||
expect(onSubmitImageGeneration).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -506,9 +505,11 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
const customStyle = within(panel).getByRole('textbox', {
|
||||
name: '自定义画风',
|
||||
});
|
||||
expect(customStyle.tagName).toBe('TEXTAREA');
|
||||
expect(customStyle.className).toContain('auto-grow-text-area');
|
||||
expect(customStyle.className).toContain(
|
||||
expect(customStyle.getAttribute('contenteditable')).toBe('true');
|
||||
expect(customStyle.closest('.auto-grow-text-area')?.className).toContain(
|
||||
'auto-grow-text-area',
|
||||
);
|
||||
expect(customStyle.closest('.auto-grow-text-area')?.className).toContain(
|
||||
'image-canvas-editor__generation-prompt',
|
||||
);
|
||||
expect(customStyle.className).not.toContain('platform-text-field');
|
||||
@@ -541,16 +542,14 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
|
||||
const error = within(panel).getByRole('alert');
|
||||
expect(error.textContent).toBe('请填写画面内容');
|
||||
expect(error.className).toContain(
|
||||
'image-canvas-editor__scene-field-error',
|
||||
);
|
||||
expect(error.className).toContain('image-canvas-editor__scene-field-error');
|
||||
expect(sceneContent.getAttribute('aria-invalid')).toBe('true');
|
||||
expect(sceneContent.getAttribute('aria-describedby')).toBe(error.id);
|
||||
expect(
|
||||
panel.querySelector('.image-canvas-editor__generate-status'),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.change(sceneContent, { target: { value: '雨夜中的海边车站' } });
|
||||
setPlainTextEditorValue(sceneContent, '雨夜中的海边车站');
|
||||
|
||||
expect(within(panel).queryByRole('alert')).toBeNull();
|
||||
expect(sceneContent.getAttribute('aria-invalid')).toBeNull();
|
||||
@@ -584,16 +583,14 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
|
||||
const error = within(panel).getByRole('alert');
|
||||
expect(error.textContent).toBe('请填写自定义画风');
|
||||
expect(error.className).toContain(
|
||||
'image-canvas-editor__scene-field-error',
|
||||
);
|
||||
expect(error.className).toContain('image-canvas-editor__scene-field-error');
|
||||
expect(customStyle.getAttribute('aria-invalid')).toBe('true');
|
||||
expect(customStyle.getAttribute('aria-describedby')).toBe(error.id);
|
||||
expect(
|
||||
panel.querySelector('.image-canvas-editor__generate-status'),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.change(customStyle, { target: { value: '90 年代复古像素风' } });
|
||||
setPlainTextEditorValue(customStyle, '90 年代复古像素风');
|
||||
|
||||
expect(within(panel).queryByRole('alert')).toBeNull();
|
||||
expect(customStyle.getAttribute('aria-invalid')).toBeNull();
|
||||
@@ -637,16 +634,16 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
within(panel).getByRole('button', { name: 'UI设计图标规范' }),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByRole('textbox', { name: 'UI设计要求' }).className,
|
||||
within(panel)
|
||||
.getByRole('textbox', { name: 'UI设计要求' })
|
||||
.closest('.auto-grow-text-area')?.className,
|
||||
).toContain('image-canvas-editor__generation-prompt');
|
||||
expect(
|
||||
panel.querySelector('.image-canvas-editor__generation-composer-footer'),
|
||||
).toBeTruthy();
|
||||
fireEvent.change(
|
||||
setPlainTextEditorValue(
|
||||
within(panel).getByRole('textbox', { name: 'UI设计要求' }),
|
||||
{
|
||||
target: { value: '主界面和结算弹窗' },
|
||||
},
|
||||
'主界面和结算弹窗',
|
||||
);
|
||||
expect(setGenerateDialog).toHaveBeenCalled();
|
||||
const menu = screen.getByRole('menu', { name: '参考图来源' });
|
||||
@@ -814,11 +811,9 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
).value,
|
||||
).toBe('非对称对抗');
|
||||
expect(
|
||||
(
|
||||
within(panel).getByRole('textbox', {
|
||||
name: '运营海报一句话描述游戏',
|
||||
}) as HTMLTextAreaElement
|
||||
).value,
|
||||
within(panel).getByRole('textbox', {
|
||||
name: '运营海报一句话描述游戏',
|
||||
}).textContent,
|
||||
).toBe('找到钥匙,开门逃离马戏团');
|
||||
expect(within(panel).getByLabelText('首图参考')).toBeTruthy();
|
||||
expect(
|
||||
@@ -1181,7 +1176,9 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
expect(screen.getByLabelText('当前音效时长').textContent).toBe('8');
|
||||
expect(screen.getByRole('button', { name: '音效时长 8秒' })).toBeTruthy();
|
||||
|
||||
fireEvent.submit(panel);
|
||||
fireEvent.click(
|
||||
within(panel).getByRole('button', { name: '生成游戏音效' }),
|
||||
);
|
||||
expect(onSubmitImageGeneration).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mode: 'audio-sound-effect',
|
||||
@@ -1266,7 +1263,7 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
});
|
||||
|
||||
const textarea = getBackgroundMusicPromptTextarea();
|
||||
expect(textarea.value).toBe(' 森林冒险背景音乐 ');
|
||||
expect(textarea.textContent).toBe(' 森林冒险背景音乐 ');
|
||||
expect(
|
||||
within(getBackgroundMusicPanel()).getByText('8 / 200'),
|
||||
).toBeTruthy();
|
||||
@@ -1312,8 +1309,8 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
});
|
||||
|
||||
const textarea = getBackgroundMusicPromptTextarea();
|
||||
expect(textarea.value).toBe(prompt);
|
||||
expect(textarea.value).toHaveLength(2001);
|
||||
expect(textarea.textContent).toBe(prompt);
|
||||
expect(textarea.textContent).toHaveLength(2001);
|
||||
expect(textarea.getAttribute('aria-invalid')).toBe('true');
|
||||
expect(
|
||||
within(getBackgroundMusicPanel()).getByText('2001 / 200'),
|
||||
@@ -1419,7 +1416,7 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
const dialog = createBackgroundMusicDialog({ prompt: '森林冒险' });
|
||||
renderBackgroundMusicComposer({ dialog, onSubmit });
|
||||
|
||||
fireEvent.submit(getBackgroundMusicPanel());
|
||||
fireEvent.click(getBackgroundMusicActionButton('生成游戏背景音乐'));
|
||||
expect(onSubmit).toHaveBeenCalledWith(dialog);
|
||||
|
||||
onSubmit.mockClear();
|
||||
@@ -1427,8 +1424,8 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
dialog: createBackgroundMusicDialog({ prompt: ' ' }),
|
||||
onSubmit,
|
||||
});
|
||||
fireEvent.submit(
|
||||
within(blocked.container).getByRole('dialog', {
|
||||
fireEvent.click(
|
||||
within(blocked.container).getByRole('button', {
|
||||
name: '生成游戏背景音乐',
|
||||
}),
|
||||
);
|
||||
@@ -1451,7 +1448,9 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
|
||||
const panel = getBackgroundMusicPanel();
|
||||
expect(panel.getAttribute('aria-busy')).toBe('true');
|
||||
expect(getBackgroundMusicPromptTextarea().readOnly).toBe(true);
|
||||
expect(
|
||||
getBackgroundMusicPromptTextarea().getAttribute('aria-readonly'),
|
||||
).toBe('true');
|
||||
expect(getBackgroundMusicActionButton('AI 补全').disabled).toBe(true);
|
||||
expect(getBackgroundMusicActionButton('一键简化').disabled).toBe(true);
|
||||
expect(getBackgroundMusicActionButton('撤销').disabled).toBe(true);
|
||||
@@ -1470,7 +1469,9 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
});
|
||||
|
||||
expect(getBackgroundMusicPanel().getAttribute('aria-busy')).toBe('true');
|
||||
expect(getBackgroundMusicPromptTextarea().readOnly).toBe(true);
|
||||
expect(
|
||||
getBackgroundMusicPromptTextarea().getAttribute('aria-readonly'),
|
||||
).toBe('true');
|
||||
expect(getBackgroundMusicActionButton('AI 补全').disabled).toBe(true);
|
||||
expect(getBackgroundMusicActionButton('展开').disabled).toBe(true);
|
||||
expect(
|
||||
@@ -1714,12 +1715,10 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
name: '生成游戏音效',
|
||||
});
|
||||
expect(
|
||||
(
|
||||
within(soundEffectPanel).getByRole('textbox', {
|
||||
name: 'prompt',
|
||||
}) as HTMLTextAreaElement
|
||||
).disabled,
|
||||
).toBe(false);
|
||||
within(soundEffectPanel)
|
||||
.getByRole('textbox', { name: 'prompt' })
|
||||
.getAttribute('aria-disabled'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
(
|
||||
within(soundEffectPanel).getByRole('button', {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user