feat: expose profile reset operation progress
This commit is contained in:
@@ -33,12 +33,15 @@ const installFixture = async (
|
||||
page: Page,
|
||||
options: {
|
||||
deferRequest?: boolean;
|
||||
deferInstall?: boolean;
|
||||
initialActions?: RuntimeAction[];
|
||||
afterRequestActions?: RuntimeAction[];
|
||||
pendingProfileReads?: number;
|
||||
} = {}
|
||||
) => {
|
||||
let requested = false;
|
||||
let installRequested = false;
|
||||
let installActive = false;
|
||||
let postRequestProfileReads = 0;
|
||||
const requestBodies: unknown[] = [];
|
||||
let releaseRequest = (): void => {};
|
||||
@@ -47,6 +50,12 @@ const installFixture = async (
|
||||
releaseRequest = resolve;
|
||||
})
|
||||
: Promise.resolve();
|
||||
let releaseInstall = (): void => {};
|
||||
const installGate = options.deferInstall
|
||||
? new Promise<void>((resolve) => {
|
||||
releaseInstall = resolve;
|
||||
})
|
||||
: Promise.resolve();
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('sammo-session-token', 'playwright-admin-session');
|
||||
});
|
||||
@@ -58,6 +67,12 @@ const installFixture = async (
|
||||
requestBodies.push(body);
|
||||
await requestGate;
|
||||
}
|
||||
if (operations.includes('admin.profiles.install')) {
|
||||
installRequested = true;
|
||||
requestBodies.push(body);
|
||||
await installGate;
|
||||
installActive = true;
|
||||
}
|
||||
const results = operations.map((operation) => {
|
||||
if (operation === 'me') {
|
||||
return response({
|
||||
@@ -75,7 +90,17 @@ const installFixture = async (
|
||||
return response({ enabled: true });
|
||||
}
|
||||
if (operation === 'admin.profiles.listScenarios') {
|
||||
return response([]);
|
||||
return response([
|
||||
{
|
||||
id: 1010,
|
||||
title: '【테스트】황건의 난',
|
||||
year: 184,
|
||||
npcCount: 42,
|
||||
npcExCount: 0,
|
||||
npcNeutralCount: 0,
|
||||
nations: [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (operation === 'admin.profiles.list') {
|
||||
const keepPending = requested && postRequestProfileReads++ < (options.pendingProfileReads ?? 0);
|
||||
@@ -83,11 +108,17 @@ const installFixture = async (
|
||||
{
|
||||
profileName: 'hwe:default',
|
||||
profile: 'hwe',
|
||||
scenario: 'default',
|
||||
scenario: '1010',
|
||||
apiPort: 15015,
|
||||
status: 'RUNNING',
|
||||
buildStatus: 'SUCCEEDED',
|
||||
meta: {},
|
||||
activeOperation: installActive
|
||||
? {
|
||||
id: '77777777-7777-4777-8777-777777777777',
|
||||
status: 'QUEUED',
|
||||
}
|
||||
: null,
|
||||
runtime: {
|
||||
profileName: 'hwe:default',
|
||||
apiRunning: true,
|
||||
@@ -123,6 +154,32 @@ const installFixture = async (
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operation === 'admin.profiles.install') {
|
||||
return response({
|
||||
ok: true,
|
||||
operationId: '77777777-7777-4777-8777-777777777777',
|
||||
});
|
||||
}
|
||||
if (operation === 'admin.operations.list') {
|
||||
return response(
|
||||
installRequested
|
||||
? [
|
||||
{
|
||||
id: '77777777-7777-4777-8777-777777777777',
|
||||
profileName: 'hwe:default',
|
||||
type: 'RESET',
|
||||
status: installActive ? 'QUEUED' : 'RUNNING',
|
||||
sourceMode: 'COMMIT',
|
||||
sourceRef: '0123456789abcdef0123456789abcdef01234567',
|
||||
payload: {},
|
||||
requestedBy: 'admin-user',
|
||||
createdAt: '2026-07-30T02:00:00.000Z',
|
||||
updatedAt: '2026-07-30T02:00:00.000Z',
|
||||
},
|
||||
]
|
||||
: []
|
||||
);
|
||||
}
|
||||
throw new Error(`Unhandled tRPC operation: ${operation}`);
|
||||
});
|
||||
await route.fulfill({
|
||||
@@ -131,7 +188,7 @@ const installFixture = async (
|
||||
body: JSON.stringify(results),
|
||||
});
|
||||
});
|
||||
return { releaseRequest, requestBodies };
|
||||
return { releaseRequest, releaseInstall, requestBodies };
|
||||
};
|
||||
|
||||
test('reports clock-shift acceptance separately from actual application', async ({ page }) => {
|
||||
@@ -215,3 +272,33 @@ test('renders an ignored terminal outcome without calling it applied', async ({
|
||||
await expect(page.getByText('지원하지 않는 요청')).toBeVisible();
|
||||
await expect(page.getByText(/적용됨|요청 완료/)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('keeps profile installation disabled while its queued operation is active', async ({ page }, testInfo) => {
|
||||
const fixture = await installFixture(page, { deferInstall: true });
|
||||
page.on('dialog', (dialog) => dialog.accept());
|
||||
await page.goto('admin');
|
||||
|
||||
const installButton = page.getByRole('button', { name: '설치 적용' });
|
||||
const click = installButton.click();
|
||||
await expect.poll(() => fixture.requestBodies.length).toBe(1);
|
||||
await expect(page.getByRole('button', { name: '등록 중…' })).toBeDisabled();
|
||||
fixture.releaseInstall();
|
||||
await click;
|
||||
|
||||
const operationLink = page.getByRole('link', { name: /77777777-7777-4777-8777-777777777777 상태 보기/ });
|
||||
await expect(operationLink).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '설치 작업 진행 중' })).toBeDisabled();
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const linkGeometry = await operationLink.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return { left: rect.left, right: rect.right, viewportWidth: window.innerWidth };
|
||||
});
|
||||
expect(linkGeometry.left).toBeGreaterThanOrEqual(0);
|
||||
expect(linkGeometry.right).toBeLessThanOrEqual(linkGeometry.viewportWidth);
|
||||
await page.screenshot({ path: testInfo.outputPath('admin-install-active-mobile.png'), fullPage: true });
|
||||
|
||||
await operationLink.click();
|
||||
await expect(page).toHaveURL(/\/gateway\/admin\/server-operations\?operationId=77777777/);
|
||||
await expect(page.getByTestId('operations-table')).toContainText('77777777-7777-4777-8777-777777777777');
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ type Operation = {
|
||||
sourceMode?: 'BRANCH' | 'COMMIT';
|
||||
sourceRef?: string;
|
||||
resolvedCommitSha?: string;
|
||||
completedAt?: string;
|
||||
error?: string;
|
||||
payload: Record<string, unknown>;
|
||||
requestedBy: string;
|
||||
createdAt: string;
|
||||
@@ -127,6 +129,22 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
||||
state.operations = [operation];
|
||||
return response(operation);
|
||||
}
|
||||
if (name === 'admin.operations.retry') {
|
||||
const operation: Operation = {
|
||||
id: '44444444-4444-4444-8444-444444444444',
|
||||
profileName: 'che:2',
|
||||
type: 'RESET',
|
||||
status: 'QUEUED',
|
||||
sourceMode: 'COMMIT',
|
||||
sourceRef: 'fedcba9876543210fedcba9876543210fedcba98',
|
||||
payload: { installOperationId: 'failed-generation' },
|
||||
requestedBy: 'admin',
|
||||
createdAt: '2026-07-25T04:00:00.000Z',
|
||||
updatedAt: '2026-07-25T04:00:00.000Z',
|
||||
};
|
||||
state.operations = [operation, ...state.operations];
|
||||
return response(operation);
|
||||
}
|
||||
throw new Error(`Unhandled tRPC operation: ${name}`);
|
||||
});
|
||||
await route.fulfill({
|
||||
@@ -190,7 +208,7 @@ test('separates branch and commit semantics and submits a reset from the dedicat
|
||||
await page.getByTestId('request-reset').hover();
|
||||
await page.getByTestId('request-reset').click();
|
||||
|
||||
await expect(page.getByText('초기화 작업을 시작했습니다.')).toBeVisible();
|
||||
await expect(page.getByText('초기화 작업을 등록했습니다.')).toBeVisible();
|
||||
await expect(page.getByTestId('operations-table')).toContainText('RESET');
|
||||
const resetRequest = state.requestBodies.find((entry) => entry.operation === 'admin.operations.requestReset');
|
||||
expect(JSON.stringify(resetRequest?.body)).toContain('"sourceMode":"COMMIT"');
|
||||
@@ -232,3 +250,66 @@ test('starts and stops all runtime roles through the operation controls', async
|
||||
expect(serializedRequests).toContain('"action":"START"');
|
||||
expect(serializedRequests).toContain('"action":"STOP"');
|
||||
});
|
||||
|
||||
test('renders a failed reset, retries it as a new operation, and reaches success', async ({ page }, testInfo) => {
|
||||
const longError =
|
||||
'선택한 커밋의 프로필 프로세스를 시작하지 못했습니다. 실패 원인을 확인한 뒤 동일 generation으로 재시도해 주세요.';
|
||||
const state: FixtureState = {
|
||||
operations: [
|
||||
{
|
||||
id: '55555555-5555-4555-8555-555555555555',
|
||||
profileName: 'che:2',
|
||||
type: 'RESET',
|
||||
status: 'FAILED',
|
||||
sourceMode: 'COMMIT',
|
||||
sourceRef: 'fedcba9876543210fedcba9876543210fedcba98',
|
||||
resolvedCommitSha: 'fedcba9876543210fedcba9876543210fedcba98',
|
||||
payload: { installOperationId: 'failed-generation' },
|
||||
requestedBy: 'admin',
|
||||
completedAt: '2026-07-25T03:30:00.000Z',
|
||||
error: longError,
|
||||
createdAt: '2026-07-25T03:00:00.000Z',
|
||||
updatedAt: '2026-07-25T03:30:00.000Z',
|
||||
},
|
||||
],
|
||||
runtimeRunning: false,
|
||||
requestBodies: [],
|
||||
};
|
||||
await installFixture(page, state);
|
||||
page.on('dialog', (dialog) => dialog.accept());
|
||||
|
||||
await page.goto('admin/server-operations');
|
||||
await expect(page.getByText('FAILED', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'fedcba987654', exact: true })).toBeVisible();
|
||||
const failure = page.getByText(longError);
|
||||
await expect(failure).toBeVisible();
|
||||
expect(await failure.evaluate((element) => getComputedStyle(element).color)).toBe('oklch(0.704 0.191 22.216)');
|
||||
|
||||
await page.getByRole('button', { name: '재시도' }).click();
|
||||
await expect(page.getByText('재시도 작업을 등록했습니다.')).toBeVisible();
|
||||
await expect(page.getByText('FAILED', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('QUEUED', { exact: true })).toBeVisible();
|
||||
await expect(page.getByTestId('operations-table').locator('tbody tr')).toHaveCount(2);
|
||||
|
||||
state.operations[0] = {
|
||||
...state.operations[0]!,
|
||||
status: 'SUCCEEDED',
|
||||
resolvedCommitSha: 'fedcba9876543210fedcba9876543210fedcba98',
|
||||
completedAt: '2026-07-25T04:05:00.000Z',
|
||||
updatedAt: '2026-07-25T04:05:00.000Z',
|
||||
};
|
||||
state.runtimeRunning = true;
|
||||
await page.getByTestId('refresh-operations').click();
|
||||
await expect(page.getByText('SUCCEEDED', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('RUNNING', { exact: true }).first()).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: testInfo.outputPath('failed-retry-succeeded-desktop.png'), fullPage: true });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const tableGeometry = await page.getByTestId('operations-table').evaluate((table) => {
|
||||
const tableRect = table.getBoundingClientRect();
|
||||
const scrollerRect = table.parentElement!.getBoundingClientRect();
|
||||
return { tableWidth: tableRect.width, scrollerWidth: scrollerRect.width };
|
||||
});
|
||||
expect(tableGeometry.tableWidth).toBeGreaterThan(tableGeometry.scrollerWidth);
|
||||
await page.screenshot({ path: testInfo.outputPath('failed-retry-succeeded-mobile.png'), fullPage: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user