09fb8bc076
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m52s
Project CI / Backend tests (pull_request) Failing after 13s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m5s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
- 运行画面右下角新增「全屏预览」:新增 `useElementFullscreen`,只把画面那一格(`.game-run-preview`)送进全屏,按钮态只认 `fullscreenchange`,宿主没有 Fullscreen API 时整枚入口不渲染。 - 底部信息栏改为「有内容才存在」:运行页选中资源时自动展开,可手动收起到只剩一行开合按钮,清空选中后整栏不渲染;只有一栏内容时卡片铺满整行。 - 「数值微调」在没有登记表前不渲染区域标题与卡片,随这一栏删除其 label / input 样式声明。 - 新增 `tests/runPreviewFullscreen.test.tsx`;AGC 子集补信息栏开合用例,并把「没有内容仍渲染两张空卡片」的旧断言改成整栏不渲染。 - 同步 PRD §3.4、技术方案运行表现层条目与 project-memory 决策记录。
143 lines
5.0 KiB
TypeScript
143 lines
5.0 KiB
TypeScript
/** @vitest-environment jsdom */
|
||
import { fireEvent, render, screen } from '@testing-library/react';
|
||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||
|
||
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||
import ProjectDevelopmentView from '../src/view/project-development';
|
||
|
||
const PREVIEW_URL = 'http://127.0.0.1:4173/';
|
||
|
||
function installInvoke() {
|
||
window.__TAURI__ = {
|
||
core: {
|
||
invoke: vi.fn(async (command: string, args?: Record<string, unknown>) => {
|
||
if (command === 'read_local_project_resource_graph') {
|
||
return {
|
||
resourceIds: [],
|
||
referenceEdges: [],
|
||
taskFlows: [],
|
||
categories: [],
|
||
diagnostics: [],
|
||
};
|
||
}
|
||
if (command === 'read_local_project_resource_canvas_layout') {
|
||
return {
|
||
schemaVersion: 'game-creator-resource-layout.v1',
|
||
projectId: args?.expectedProjectId,
|
||
mode: args?.mode,
|
||
revision: 0,
|
||
positions: [],
|
||
updatedAt: 0,
|
||
};
|
||
}
|
||
throw new Error(`unexpected invoke ${command}`);
|
||
}),
|
||
},
|
||
} as unknown as typeof window.__TAURI__;
|
||
}
|
||
|
||
function renderRunView() {
|
||
installInvoke();
|
||
const manifest = createGameCreationAppManifest(
|
||
'run-preview-fullscreen',
|
||
'运行页全屏预览',
|
||
);
|
||
render(
|
||
<ProjectDevelopmentView
|
||
projectName={manifest.name}
|
||
projectPath="/tmp/run-preview-fullscreen"
|
||
manifest={manifest}
|
||
attachments={[]}
|
||
recentRunStatus={null}
|
||
recentRunStopReason={null}
|
||
preview={{ status: 'running', url: PREVIEW_URL, port: 4173 }}
|
||
supervisor={<div>项目总控</div>}
|
||
onHomeOpen={vi.fn()}
|
||
onProjectsOpen={vi.fn()}
|
||
onNotice={vi.fn()}
|
||
/>,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* jsdom 完全没有 Fullscreen API(`document.fullscreenEnabled` / `requestFullscreen` /
|
||
* `fullscreenElement` 都是 undefined),所以这一组用例必须自己把宿主那一份补出来:
|
||
* 全屏状态、元素身份与 `fullscreenchange` 都按规范最小实现,只用于验按钮的真实行为。
|
||
*/
|
||
function installFullscreenHost() {
|
||
let fullscreenElement: Element | null = null;
|
||
const requestFullscreen = vi.fn(function (this: Element) {
|
||
// 这枚桩就是要记录调用方传进来的 `this`:全屏元素身份正是本用例的断言对象(画面那一格)。
|
||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||
fullscreenElement = this;
|
||
document.dispatchEvent(new Event('fullscreenchange'));
|
||
return Promise.resolve();
|
||
});
|
||
const exitFullscreen = vi.fn(() => {
|
||
fullscreenElement = null;
|
||
document.dispatchEvent(new Event('fullscreenchange'));
|
||
return Promise.resolve();
|
||
});
|
||
Object.defineProperty(document, 'fullscreenElement', {
|
||
configurable: true,
|
||
get: () => fullscreenElement,
|
||
});
|
||
Object.defineProperty(document, 'exitFullscreen', {
|
||
configurable: true,
|
||
value: exitFullscreen,
|
||
});
|
||
Object.defineProperty(Element.prototype, 'requestFullscreen', {
|
||
configurable: true,
|
||
writable: true,
|
||
value: requestFullscreen,
|
||
});
|
||
return { requestFullscreen, exitFullscreen };
|
||
}
|
||
|
||
afterEach(() => {
|
||
document.body.innerHTML = '';
|
||
Reflect.deleteProperty(document, 'fullscreenElement');
|
||
Reflect.deleteProperty(document, 'exitFullscreen');
|
||
Reflect.deleteProperty(Element.prototype, 'requestFullscreen');
|
||
vi.clearAllMocks();
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
describe('运行页「全屏预览」', () => {
|
||
it('点画面右下角那枚按钮就把画面那一格送进全屏,再点退出', async () => {
|
||
const { requestFullscreen, exitFullscreen } = installFullscreenHost();
|
||
renderRunView();
|
||
|
||
const button = await screen.findByRole('button', { name: '全屏预览' });
|
||
// 按钮住在画面那一格里(右下角由样式给),全屏的也是那一格——不是整块工作台。
|
||
const stage = document.querySelector('.game-run-preview');
|
||
expect(stage).not.toBeNull();
|
||
expect(button.closest('.game-run-preview')).toBe(stage);
|
||
expect(button.getAttribute('aria-pressed')).toBe('false');
|
||
|
||
fireEvent.click(button);
|
||
expect(requestFullscreen).toHaveBeenCalledTimes(1);
|
||
// 全屏元素就是画面那一格:按钮自己也算得出来(状态来自 fullscreenchange,不是乐观值)。
|
||
expect(document.fullscreenElement).toBe(stage);
|
||
const exitButton = await screen.findByRole('button', {
|
||
name: '退出全屏预览',
|
||
});
|
||
expect(exitButton.getAttribute('aria-pressed')).toBe('true');
|
||
|
||
fireEvent.click(exitButton);
|
||
expect(exitFullscreen).toHaveBeenCalledTimes(1);
|
||
expect(
|
||
(await screen.findByRole('button', { name: '全屏预览' })).getAttribute(
|
||
'aria-pressed',
|
||
),
|
||
).toBe('false');
|
||
});
|
||
|
||
it('宿主没有 Fullscreen API 时不渲染这枚按钮,而不是留一个点了没反应的入口', async () => {
|
||
renderRunView();
|
||
|
||
await screen.findByTitle('运行页全屏预览 游戏运行画面');
|
||
expect(screen.queryByRole('button', { name: '全屏预览' })).toBeNull();
|
||
});
|
||
});
|