Files
Genarrative/apps/preview-deployer-web/src/PreviewDeployerApp.test.tsx
T
kdletters f5b4293e80
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m12s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m21s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 7m21s
Project CI / AI game creator shell Rust shard 1/4 (push) Failing after 7m26s
Project CI / Native shell tests (push) Failing after 1m50s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m15s
Project CI / AI game creator shell Rust crates (push) Successful in 2m19s
Project CI / Backend tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
预览控制面支持展示公网预览地址
- preview-deployer-server 新增可选配置 GENARRATIVE_PREVIEW_DEPLOYER_WEB_DOMAIN,并按「实例 ID + 预览域名」派生公开字段 webPublicUrl,卸载时与 webUrl/webPort 一起清空、加载状态文件时重新派生覆盖
- preview-deployer-web 在存在公网地址时把「打开公网预览」作为主入口,内网地址降级为次级链接,未配置时行为不变
- deploy/env/preview-deployer.env.example 补充 WEB_DOMAIN 键与留空语义说明
- 补充 cargo 与 vitest 用例覆盖公网地址派生、域名配置校验与页面展示
2026-09-17 20:29:42 +08:00

223 lines
6.9 KiB
TypeScript

/* @vitest-environment jsdom */
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import * as api from './api';
import { PreviewDeployerApp } from './PreviewDeployerApp';
vi.mock('./api');
afterEach(() => {
cleanup();
});
beforeEach(() => {
vi.mocked(api.getSession).mockResolvedValue({ authenticated: true });
vi.mocked(api.createSession).mockResolvedValue({ authenticated: true });
vi.mocked(api.deleteSession).mockResolvedValue(undefined);
vi.mocked(api.listDeployments).mockResolvedValue([]);
vi.mocked(api.searchBranches).mockResolvedValue([]);
vi.mocked(api.searchCommits).mockResolvedValue([]);
vi.mocked(api.createDeployment).mockResolvedValue({
id: null,
branch: 'master',
status: 'queued',
health: 'pending',
createdAt: '2026-08-15T00:00:00Z',
updatedAt: '2026-08-15T00:00:00Z',
});
vi.mocked(api.uninstallDeployment).mockResolvedValue({
id: '1',
branch: 'master',
status: 'uninstalling',
health: 'pending',
createdAt: '2026-08-15T00:00:00Z',
updatedAt: '2026-08-15T00:02:00Z',
});
});
test('requires an access token before showing deployments', async () => {
const user = userEvent.setup();
vi.mocked(api.getSession).mockResolvedValue({ authenticated: false });
render(<PreviewDeployerApp />);
expect(await screen.findByText('访问口令')).toBeTruthy();
await user.type(screen.getByPlaceholderText('请输入访问口令'), 'team-token');
await user.click(screen.getByRole('button', { name: '进入发布面板' }));
await waitFor(() => {
expect(api.createSession).toHaveBeenCalledWith('team-token');
});
expect(await screen.findByText('构建并发布一个分支')).toBeTruthy();
});
test('prefers the public preview domain when the control plane exposes one', async () => {
vi.mocked(api.listDeployments).mockResolvedValue([
{
id: '77',
branch: 'feature/public-preview',
resolvedCommit: '1234567890abcdef',
status: 'running',
health: 'healthy',
webPort: 8400,
webUrl: 'http://192.168.35.82:8400',
webPublicUrl:
'https://preview-63d38d3da6bc9b06.preview.genarrative.world',
createdAt: 1_787_270_400,
updatedAt: 1_787_270_460,
},
]);
render(<PreviewDeployerApp />);
const publicLink = await screen.findByRole('link', {
name: /打开公网预览/u,
});
expect(publicLink.getAttribute('href')).toBe(
'https://preview-63d38d3da6bc9b06.preview.genarrative.world',
);
});
test('submits a branch with an optional commit hash', async () => {
const user = userEvent.setup();
render(<PreviewDeployerApp />);
const branchInput = await screen.findByPlaceholderText('master');
await user.clear(branchInput);
await user.type(branchInput, 'feature/preview');
await user.type(
screen.getByPlaceholderText('留空则构建分支最新提交'),
'aa5221abc',
);
await user.click(screen.getByRole('button', { name: '开始构建' }));
await waitFor(() => {
expect(api.createDeployment).toHaveBeenCalledWith({
branch: 'feature/preview',
commitHash: 'aa5221abc',
});
});
});
test('shows health and web url, then confirms uninstall', async () => {
vi.mocked(api.listDeployments).mockResolvedValue([
{
id: '42',
branch: 'feature/demo',
resolvedCommit: '1234567890abcdef',
status: 'running',
health: 'healthy',
webPort: 8400,
webUrl: 'http://192.168.35.82:8400',
createdAt: 1_787_270_400,
updatedAt: 1_787_270_460,
},
]);
render(<PreviewDeployerApp />);
expect(await screen.findByText('健康')).toBeTruthy();
expect(screen.getByText('端口 8400')).toBeTruthy();
expect(screen.getByText('构建 #42')).toBeTruthy();
expect(
screen.getByRole('link', { name: /打开 Web/u }).getAttribute('href'),
).toBe('http://192.168.35.82:8400');
fireEvent.click(screen.getByRole('button', { name: '卸载' }));
expect(screen.getByRole('dialog')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '确认卸载' }));
await waitFor(() => {
expect(api.uninstallDeployment).toHaveBeenCalledWith('42');
});
});
test('shows debounced branch results and selects one with the keyboard', async () => {
const user = userEvent.setup();
vi.mocked(api.searchBranches).mockResolvedValue([
{ name: 'feature/search-one', commitHash: '111111111111' },
{ name: 'feature/search-two', commitHash: '222222222222' },
]);
render(<PreviewDeployerApp />);
const branchInput = await screen.findByRole('combobox', { name: '分支名' });
await user.clear(branchInput);
await user.type(branchInput, 'feature/search');
expect(
await screen.findByRole('option', { name: /feature\/search-one/u }),
).toBeTruthy();
expect(api.searchBranches).toHaveBeenLastCalledWith(
'feature/search',
expect.any(AbortSignal),
);
await user.keyboard('{ArrowDown}{ArrowDown}{Enter}');
expect((branchInput as HTMLInputElement).value).toBe('feature/search-two');
});
test('searches commits in the current branch and selects a result', async () => {
const user = userEvent.setup();
vi.mocked(api.searchCommits).mockResolvedValue([
{
commitHash: 'aa5221abcdef0123456789',
shortHash: 'aa5221a',
subject: '补充预览搜索',
},
]);
render(<PreviewDeployerApp />);
const commitInput = await screen.findByRole('combobox', {
name: 'Commit Hash',
});
await user.type(commitInput, 'aa5221a');
const option = await screen.findByRole('option', {
name: /aa5221a.*补充预览搜索/u,
});
expect(api.searchCommits).toHaveBeenCalledWith(
'master',
'aa5221a',
expect.any(AbortSignal),
);
await user.click(option);
expect((commitInput as HTMLInputElement).value).toBe(
'aa5221abcdef0123456789',
);
});
test('clears commit and ignores stale commit responses when branch changes', async () => {
const user = userEvent.setup();
let resolveOldSearch: ((items: api.CommitRef[]) => void) | undefined;
vi.mocked(api.searchCommits).mockImplementation(
() =>
new Promise<api.CommitRef[]>((resolve) => {
resolveOldSearch = resolve;
}),
);
render(<PreviewDeployerApp />);
const branchInput = await screen.findByRole('combobox', { name: '分支名' });
const commitInput = screen.getByRole('combobox', { name: 'Commit Hash' });
await user.type(commitInput, 'abcdef1');
await waitFor(() => expect(api.searchCommits).toHaveBeenCalled());
await user.clear(branchInput);
await user.type(branchInput, 'feature/new');
expect((commitInput as HTMLInputElement).value).toBe('');
resolveOldSearch?.([
{
commitHash: 'abcdef1234567890',
shortHash: 'abcdef1',
subject: '旧分支提交',
},
]);
await Promise.resolve();
expect(screen.queryByText('旧分支提交')).toBeNull();
});