This commit is contained in:
@@ -28,6 +28,7 @@ function updateView(state: string, checked: boolean, enabled = true, recoveryPen
|
||||
'QUEUED',
|
||||
'DOWNLOAD_QUEUED',
|
||||
'INSTALL_QUEUED',
|
||||
'PRECHECKING',
|
||||
'VERIFYING',
|
||||
'DOWNLOADING',
|
||||
'BACKING_UP',
|
||||
@@ -59,6 +60,14 @@ function updateView(state: string, checked: boolean, enabled = true, recoveryPen
|
||||
publishedAt: checked ? '2026-08-16T00:00:00Z' : null,
|
||||
checkedAt: checked ? '2026-08-16T00:01:00Z' : null,
|
||||
statusUpdatedAt: null,
|
||||
downloadedBytes: state === 'READY' ? 10_485_760 : state === 'DOWNLOADING' ? 5_242_880 : 0,
|
||||
totalBytes: ['DOWNLOAD_QUEUED', 'DOWNLOADING', 'VERIFYING', 'READY'].includes(state) ? 10_485_760 : null,
|
||||
bytesPerSecond: state === 'DOWNLOADING' ? 1_048_576 : 0,
|
||||
downloadPercent: state === 'READY' ? 100 : state === 'DOWNLOADING' ? 50 : 0,
|
||||
restartExpectedSeconds: state === 'RUNNING' ? 10 : null,
|
||||
events: checked
|
||||
? [{ occurredAt: '2026-08-16T00:01:00Z', level: 'INFO', stage: state, message: `状态进入 ${state}` }]
|
||||
: [],
|
||||
allowedActions:
|
||||
!enabled || recoveryPending || busy
|
||||
? []
|
||||
@@ -72,8 +81,11 @@ function updateView(state: string, checked: boolean, enabled = true, recoveryPen
|
||||
|
||||
interface FixtureOptions {
|
||||
canExecute?: boolean;
|
||||
checkResponseState?: string;
|
||||
downloadProgressReads?: number;
|
||||
failDownloadResponse?: boolean;
|
||||
failInstallResponse?: boolean;
|
||||
restartOfflineReads?: number;
|
||||
}
|
||||
|
||||
async function installFixture(
|
||||
@@ -87,9 +99,18 @@ async function installFixture(
|
||||
completeBusyAfterFirstRead = false,
|
||||
options: FixtureOptions = {},
|
||||
) {
|
||||
const { canExecute = true, failDownloadResponse = false, failInstallResponse = false } = options;
|
||||
const {
|
||||
canExecute = true,
|
||||
checkResponseState,
|
||||
downloadProgressReads = 0,
|
||||
failDownloadResponse = false,
|
||||
failInstallResponse = false,
|
||||
restartOfflineReads = 0,
|
||||
} = options;
|
||||
let checked = recoveryPending || Boolean(initialState);
|
||||
let state = recoveryPending ? 'FAILED' : initialState || 'IDLE';
|
||||
let state = recoveryPending ? 'RECOVERY_REQUIRED' : initialState || 'IDLE';
|
||||
let progressDeadline = 0;
|
||||
let restartDeadline = 0;
|
||||
const history: Array<Record<string, unknown>> = [];
|
||||
const recordHistory = (actionCode: string, targetVersion = '1.0.0-preview.2') => {
|
||||
history.unshift({
|
||||
@@ -131,13 +152,19 @@ async function installFixture(
|
||||
});
|
||||
}
|
||||
if (pathname === '/api/v1/admin/system-update' && request.method() === 'GET') {
|
||||
if (state === 'RUNNING' && Date.now() < restartDeadline) {
|
||||
return route.abort('connectionrefused');
|
||||
}
|
||||
if (state === 'RUNNING' && restartDeadline > 0) state = 'SUCCEEDED';
|
||||
const response = updateView(state, checked, enabled, recoveryPending);
|
||||
if (state === 'DOWNLOADING' && Date.now() >= progressDeadline) state = 'READY';
|
||||
if (completeBusyAfterFirstRead && state === 'INSTALLING') state = 'SUCCEEDED';
|
||||
return route.fulfill({ json: envelope(response) });
|
||||
}
|
||||
if (pathname === '/api/v1/admin/system-update/check' && request.method() === 'POST') {
|
||||
checked = true;
|
||||
recordHistory('SYSTEM_UPDATE_CHECK');
|
||||
if (checkResponseState) state = checkResponseState;
|
||||
const response = updateView(state, checked, enabled, recoveryPending);
|
||||
if (!canExecute) response.allowedActions = ['CHECK'];
|
||||
return route.fulfill({ json: envelope(response) });
|
||||
@@ -145,7 +172,8 @@ async function installFixture(
|
||||
if (pathname === '/api/v1/admin/system-update/download' && request.method() === 'POST') {
|
||||
downloadBodies.push(request.postDataJSON());
|
||||
recordHistory('SYSTEM_UPDATE_DOWNLOAD_REQUEST');
|
||||
state = 'READY';
|
||||
state = downloadProgressReads > 0 ? 'DOWNLOADING' : 'READY';
|
||||
progressDeadline = Date.now() + downloadProgressReads * 1000;
|
||||
if (failDownloadResponse) return route.abort('connectionreset');
|
||||
return route.fulfill({ json: envelope(updateView('DOWNLOAD_QUEUED', true, enabled)) });
|
||||
}
|
||||
@@ -153,7 +181,13 @@ async function installFixture(
|
||||
installBodies.push(request.postDataJSON());
|
||||
recordHistory('SYSTEM_UPDATE_REQUEST');
|
||||
const queued = updateView('INSTALL_QUEUED', true, enabled);
|
||||
state = failInstallResponse || completeAfterInstall ? 'SUCCEEDED' : 'INSTALL_QUEUED';
|
||||
restartDeadline = Date.now() + restartOfflineReads * 1000;
|
||||
state =
|
||||
restartOfflineReads > 0
|
||||
? 'RUNNING'
|
||||
: failInstallResponse || completeAfterInstall
|
||||
? 'SUCCEEDED'
|
||||
: 'INSTALL_QUEUED';
|
||||
if (failInstallResponse) return route.abort('connectionreset');
|
||||
return route.fulfill({ json: envelope(queued) });
|
||||
}
|
||||
@@ -189,6 +223,7 @@ test('system administrator checks, downloads, and explicitly installs a signed o
|
||||
expect(downloadBodies).toEqual([{ version: '1.0.0-preview.2' }]);
|
||||
const updateState = page.locator('.version-item').filter({ hasText: '更新状态' });
|
||||
await expect(updateState.getByText('下载已排队', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('已发现新版本,但当前账号没有下载权限', { exact: true })).toHaveCount(0);
|
||||
await expect(updateState.getByText('下载完成', { exact: true })).toBeVisible({ timeout: 8_000 });
|
||||
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toBeVisible();
|
||||
|
||||
@@ -198,6 +233,38 @@ test('system administrator checks, downloads, and explicitly installs a signed o
|
||||
expect(installBodies).toEqual([{ version: '1.0.0-preview.2', reason: '管理员确认立即更新并重启' }]);
|
||||
});
|
||||
|
||||
test('download dialog shows real byte progress and transfer speed without a false permission warning', async ({
|
||||
page,
|
||||
}) => {
|
||||
await installFixture(page, [], [], true, false, false, undefined, false, { downloadProgressReads: 8 });
|
||||
await page.goto('/governance/update');
|
||||
|
||||
await page.getByRole('button', { name: '获取版本', exact: true }).click();
|
||||
await page.getByRole('button', { name: '立即更新', exact: true }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: '系统更新' });
|
||||
await expect(dialog.getByText('50%', { exact: true })).toBeVisible({ timeout: 5_000 });
|
||||
await expect(dialog.getByText('1.0 MB/s', { exact: true })).toBeVisible();
|
||||
await expect(dialog.getByText('已发现新版本,但当前账号没有下载权限', { exact: true })).toHaveCount(0);
|
||||
await expect(dialog.getByRole('button', { name: '立即更新并重启', exact: true })).toBeVisible({ timeout: 12_000 });
|
||||
});
|
||||
|
||||
test('restart dialog keeps counting down and reconnects while the backend is temporarily offline', async ({ page }) => {
|
||||
await installFixture(page, [], [], true, false, false, 'READY', false, { restartOfflineReads: 8 });
|
||||
await page.goto('/governance/update');
|
||||
|
||||
await page.getByRole('button', { name: '立即更新并重启', exact: true }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: '系统更新' });
|
||||
await expect(dialog.getByText('服务正在重启,页面会持续重连,不需要手动刷新。', { exact: false })).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
await expect(dialog.getByText('应用服务连接中断,正在等待进程重启', { exact: true })).toBeVisible();
|
||||
await expect(dialog.getByText(/新版本已通过健康检查,页面将在 \d+ 秒后自动刷新。/)).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
});
|
||||
|
||||
test('rechecking a downloaded package prompts installation instead of downloading again', async ({ page }) => {
|
||||
await installFixture(page, [], [], true, false, false, 'READY');
|
||||
await page.goto('/governance/update');
|
||||
@@ -220,6 +287,19 @@ test('view-only administrators see a download permission message', async ({ page
|
||||
await expect(page.getByRole('button', { name: '立即更新并重启', exact: true })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('a check response that is already queued is treated as busy instead of missing permission', async ({ page }) => {
|
||||
await installFixture(page, [], [], true, false, false, undefined, false, {
|
||||
checkResponseState: 'DOWNLOAD_QUEUED',
|
||||
});
|
||||
await page.goto('/governance/update');
|
||||
|
||||
await page.getByRole('button', { name: '获取版本', exact: true }).click();
|
||||
|
||||
await expect(page.getByRole('dialog', { name: '系统更新' })).toBeVisible();
|
||||
await expect(page.getByText('发现新版本,但当前账号没有下载权限', { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByText('下载请求已排队', { exact: true }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('download and install continue polling after an accepted command loses its response', async ({ page }) => {
|
||||
await installFixture(page, [], [], true, false, false, undefined, false, {
|
||||
failDownloadResponse: true,
|
||||
@@ -252,6 +332,25 @@ test('accepted install continues to the refresh countdown after its response is
|
||||
});
|
||||
});
|
||||
|
||||
test('a lost install response stays pending while the application restarts', async ({ page }) => {
|
||||
await installFixture(page, [], [], true, false, false, 'READY', false, {
|
||||
failInstallResponse: true,
|
||||
restartOfflineReads: 4,
|
||||
});
|
||||
await page.goto('/governance/update');
|
||||
|
||||
await page.getByRole('button', { name: '立即更新并重启', exact: true }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: '系统更新' });
|
||||
await expect(dialog.getByText('立即更新并重启失败', { exact: false })).toHaveCount(0);
|
||||
await expect(dialog.getByText('服务正在重启,页面会持续重连,不需要手动刷新。', { exact: false })).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
await expect(dialog.getByText(/新版本已通过健康检查,页面将在 \d+ 秒后自动刷新。/)).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
test('brand logo returns the active identity to its workbench', async ({ page }) => {
|
||||
await installFixture(page, [], []);
|
||||
await page.goto('/governance/update');
|
||||
|
||||
Reference in New Issue
Block a user