Files
k88936 0797410eef
Project CI / Repository checks (push) Successful in 3m18s
Project CI / Frontend tests (push) Successful in 3m32s
Project CI / Backend tests (push) Successful in 4m24s
Project CI / Native shell tests (push) Successful in 15m34s
Feat/UI自动识别和布局编辑器简化版 (#170)
* 支持从项目, 本地电脑, 主站导入/引用素材.
* 页面复杂, 需要足够空间, 作为资源打开时会占满右侧
* 编辑结果作为一种新的美术资源.
* 新增素材按钮现在是空的实现, 临时在旁边加了一个新增UI设计的按钮

临时入口:
![shotmd-1787128060.jpg](/attachments/349a4fa8-7abe-4bbb-80e0-73b1717b6488)

简化:
* 组件实现了最基本的图片和文本
* 容器式布局未接入llm编辑
* 不同阶段统一维护一个状态State, 任何阶段都可以人工调整, 不设单独的步骤
* 多图的UI合并功能暂时不要求, 隐藏入口
* 因为生成工具尚未从主站迁移, 主站地图子画布未实现, 以tab栏的形式容纳素材的子画布需求搁置
* 未实现llm绑定字体功能
* 识别会替换现有UI树, 暂时不是增量的

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/170
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-08-20 19:07:25 +08:00

109 lines
3.2 KiB
TypeScript

// @vitest-environment jsdom
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ThemedModal } from '../src/components/modal/ThemedModal';
function ModalHarness({ noFocusableContent = false }) {
const [open, setOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
打开弹窗
</button>
<ThemedModal
open={open}
onClose={() => setOpen(false)}
ariaLabel="测试弹窗"
>
{noFocusableContent ? (
<p>没有可聚焦控件</p>
) : (
<>
<button type="button" onClick={() => setOpen(false)}>
取消
</button>
<button type="button">确认</button>
</>
)}
</ThemedModal>
</>
);
}
describe('ThemedModal', () => {
beforeEach(() => {
vi.spyOn(HTMLElement.prototype, 'getClientRects').mockImplementation(
() =>
[
{
width: 1,
height: 1,
top: 0,
right: 1,
bottom: 1,
left: 0,
x: 0,
y: 0,
toJSON: () => ({}),
},
] as unknown as DOMRectList,
);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('moves focus into the dialog, traps Tab, and restores the opener after close', async () => {
const user = userEvent.setup();
render(<ModalHarness />);
const opener = screen.getByRole('button', { name: '打开弹窗' });
await user.click(opener);
const cancel = await screen.findByRole('button', { name: '取消' });
const confirm = screen.getByRole('button', { name: '确认' });
await waitFor(() => expect(document.activeElement).toBe(cancel));
await user.tab();
expect(document.activeElement).toBe(confirm);
await user.tab();
expect(document.activeElement).toBe(cancel);
await user.tab({ shift: true });
expect(document.activeElement).toBe(confirm);
await user.keyboard('{Escape}');
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
expect(document.activeElement).toBe(opener);
});
it('focuses the dialog itself when it has no focusable content', async () => {
const user = userEvent.setup();
render(<ModalHarness noFocusableContent />);
await user.click(screen.getByRole('button', { name: '打开弹窗' }));
const dialog = await screen.findByRole('dialog', { name: '测试弹窗' });
await waitFor(() => expect(document.activeElement).toBe(dialog));
});
it('restores focus after a backdrop close', async () => {
const user = userEvent.setup();
render(<ModalHarness />);
const opener = screen.getByRole('button', { name: '打开弹窗' });
await user.click(opener);
const dialog = await screen.findByRole('dialog', { name: '测试弹窗' });
fireEvent.click(dialog.parentElement!);
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
expect(document.activeElement).toBe(opener);
});
});