Merge branch 'main' into refactor/frontend-time-format-options-20260813
This commit is contained in:
@@ -26,6 +26,9 @@ type FixtureState = {
|
||||
status: OperationStatus;
|
||||
sourceMode?: 'BRANCH' | 'COMMIT';
|
||||
sourceRef?: string;
|
||||
resolvedCommitSha?: string;
|
||||
completedAt?: string;
|
||||
error?: string;
|
||||
payload: Record<string, unknown>;
|
||||
requestedBy: string;
|
||||
createdAt: string;
|
||||
@@ -38,6 +41,9 @@ type FixtureState = {
|
||||
profileLogsEmpty?: boolean;
|
||||
gatewayLogPollCount?: number;
|
||||
gatewayLogsEmpty?: boolean;
|
||||
gatewayStateFailuresAfterRequest?: number;
|
||||
gatewayStateFailuresRemaining?: number;
|
||||
gatewayStateFailureCount?: number;
|
||||
capabilities?: Array<{ permission: string; scope: 'GLOBAL' | 'PROFILE'; scopes: string[] }>;
|
||||
profileListDelayMs?: number;
|
||||
profileNavigationDelayMs?: number;
|
||||
@@ -139,6 +145,19 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
||||
await route.abort('failed');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
names.includes('admin.releases.gatewayState') &&
|
||||
(state.gatewayStateFailuresRemaining ?? 0) > 0
|
||||
) {
|
||||
state.gatewayStateFailuresRemaining = (state.gatewayStateFailuresRemaining ?? 0) - 1;
|
||||
state.gatewayStateFailureCount = (state.gatewayStateFailureCount ?? 0) + 1;
|
||||
await route.fulfill({
|
||||
status: 502,
|
||||
contentType: 'application/json',
|
||||
body: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (names.includes('admin.operations.logs') && !state.profileLogProgress) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
@@ -337,6 +356,7 @@ const installFixture = async (page: Page, state: FixtureState) => {
|
||||
updatedAt: '2026-08-01T02:00:00.000Z',
|
||||
};
|
||||
state.gatewayOperations = [releaseOperation];
|
||||
state.gatewayStateFailuresRemaining = state.gatewayStateFailuresAfterRequest;
|
||||
return response(releaseOperation);
|
||||
}
|
||||
if (name === 'admin.operations.requestRuntime') {
|
||||
@@ -815,7 +835,13 @@ test('scenario-only operator resets the current version without Git or Gateway c
|
||||
});
|
||||
|
||||
test('controls gateway deployment and rollback through the external controller queue', async ({ page }, testInfo) => {
|
||||
const state: FixtureState = { operations: [], gatewayOperations: [], runtimeRunning: true, requestBodies: [] };
|
||||
const state: FixtureState = {
|
||||
operations: [],
|
||||
gatewayOperations: [],
|
||||
runtimeRunning: true,
|
||||
requestBodies: [],
|
||||
gatewayStateFailuresAfterRequest: 1,
|
||||
};
|
||||
await installFixture(page, state);
|
||||
page.on('dialog', (dialog) => dialog.accept());
|
||||
|
||||
@@ -828,6 +854,9 @@ test('controls gateway deployment and rollback through the external controller q
|
||||
await page.getByTestId('request-gateway-deploy').click();
|
||||
|
||||
await expect(page.getByText(/Gateway 배포 작업을 등록했습니다/).first()).toBeVisible();
|
||||
expect(state.gatewayStateFailureCount).toBe(1);
|
||||
await expect(page.getByTestId('server-operations-page')).not.toContainText('Unexpected end of JSON input');
|
||||
await expect(page.getByTestId('action-toast').filter({ hasText: 'Unexpected end of JSON input' })).toHaveCount(0);
|
||||
await expect(page.getByTestId('gateway-release-table')).toContainText('DEPLOY');
|
||||
await expect(page.getByTestId('gateway-release-log-panel')).toBeVisible();
|
||||
await expect(page.getByTestId('gateway-release-log')).toContainText('Gateway 구성 요소를 빌드합니다.');
|
||||
@@ -853,6 +882,131 @@ test('controls gateway deployment and rollback through the external controller q
|
||||
expect(state.requestBodies.some((entry) => entry.operation === 'admin.releases.requestGatewayRollback')).toBe(true);
|
||||
});
|
||||
|
||||
test('moves long Gateway release errors out of the table column into an expandable detail row', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const longError = [
|
||||
'Gateway release did not become ready before the timeout.',
|
||||
'Error: gateway-frontend readiness check failed after 30 attempts',
|
||||
' at waitForGatewayReadiness (/srv/core/release-controller/dist/releaseController.js:842:19)',
|
||||
'controller-output-without-breaks-'.repeat(12),
|
||||
].join('\n');
|
||||
const operationId = '88888888-8888-4888-8888-888888888888';
|
||||
const state: FixtureState = {
|
||||
operations: [],
|
||||
gatewayOperations: [
|
||||
{
|
||||
id: operationId,
|
||||
type: 'DEPLOY',
|
||||
status: 'FAILED',
|
||||
sourceMode: 'COMMIT',
|
||||
sourceRef: 'cccccccccccccccccccccccccccccccccccccccc',
|
||||
resolvedCommitSha: 'cccccccccccccccccccccccccccccccccccccccc',
|
||||
completedAt: '2026-08-01T02:03:00.000Z',
|
||||
error: longError,
|
||||
payload: {},
|
||||
requestedBy: 'admin',
|
||||
createdAt: '2026-08-01T02:00:00.000Z',
|
||||
updatedAt: '2026-08-01T02:03:00.000Z',
|
||||
},
|
||||
],
|
||||
gatewayLogsEmpty: true,
|
||||
runtimeRunning: true,
|
||||
requestBodies: [],
|
||||
};
|
||||
await installFixture(page, state);
|
||||
|
||||
await page.goto('admin/releases');
|
||||
const table = page.getByTestId('gateway-release-table');
|
||||
await expect(table.getByRole('columnheader')).toHaveCount(6);
|
||||
await expect(table.getByRole('columnheader', { name: '오류', exact: true })).toHaveCount(0);
|
||||
await expect(table.getByRole('columnheader', { name: '상세', exact: true })).toBeVisible();
|
||||
|
||||
const errorToggle = page.getByTestId('gateway-release-error-toggle');
|
||||
await expect(errorToggle).toHaveText('오류 보기');
|
||||
await expect(errorToggle).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(page.getByTestId('gateway-release-error-detail')).toBeHidden();
|
||||
|
||||
await errorToggle.focus();
|
||||
const focusedToggleStyle = await errorToggle.evaluate((element) => {
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
outlineStyle: style.outlineStyle,
|
||||
outlineWidth: style.outlineWidth,
|
||||
color: style.color,
|
||||
};
|
||||
});
|
||||
expect(focusedToggleStyle.outlineStyle).not.toBe('none');
|
||||
expect(parseFloat(focusedToggleStyle.outlineWidth)).toBeGreaterThanOrEqual(2);
|
||||
|
||||
await errorToggle.hover();
|
||||
await errorToggle.click();
|
||||
await expect(errorToggle).toHaveText('오류 닫기');
|
||||
await expect(errorToggle).toHaveAttribute('aria-expanded', 'true');
|
||||
const errorDetail = page.getByTestId('gateway-release-error-detail');
|
||||
await expect(errorDetail).toContainText('Gateway release did not become ready');
|
||||
await expect(errorDetail).toContainText('controller-output-without-breaks');
|
||||
const desktopGeometry = await table.evaluate((element) => {
|
||||
const headings = Array.from(element.querySelectorAll('thead th'));
|
||||
const detail = element.querySelector('[data-testid="gateway-release-error-detail"]');
|
||||
const detailCell = detail?.querySelector('td');
|
||||
const errorText = detail?.querySelector('pre');
|
||||
return {
|
||||
tableWidth: element.getBoundingClientRect().width,
|
||||
scrollerWidth: element.parentElement?.getBoundingClientRect().width ?? 0,
|
||||
tableLayout: getComputedStyle(element).tableLayout,
|
||||
columnCount: headings.length,
|
||||
detailColSpan: detailCell?.getAttribute('colspan'),
|
||||
detailWidth: detailCell?.getBoundingClientRect().width ?? 0,
|
||||
errorWhiteSpace: errorText ? getComputedStyle(errorText).whiteSpace : '',
|
||||
errorOverflowWrap: errorText ? getComputedStyle(errorText).overflowWrap : '',
|
||||
};
|
||||
});
|
||||
expect(desktopGeometry).toMatchObject({
|
||||
tableLayout: 'fixed',
|
||||
columnCount: 6,
|
||||
detailColSpan: '6',
|
||||
errorWhiteSpace: 'pre-wrap',
|
||||
});
|
||||
expect(desktopGeometry.tableWidth).toBeGreaterThanOrEqual(680);
|
||||
expect(desktopGeometry.detailWidth).toBeGreaterThanOrEqual(desktopGeometry.tableWidth - 1);
|
||||
await page.screenshot({ path: testInfo.outputPath('gateway-release-error-expanded-desktop.png'), fullPage: true });
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await expect(table.getByRole('columnheader')).toHaveCount(4);
|
||||
await expect(table.getByRole('columnheader', { name: '소스', exact: true })).toHaveCount(0);
|
||||
await expect(table.getByRole('columnheader', { name: '해석 커밋', exact: true })).toHaveCount(0);
|
||||
const mobileGeometry = await table.evaluate((element) => {
|
||||
const scroller = element.parentElement!;
|
||||
const scrollerRect = scroller.getBoundingClientRect();
|
||||
const detailRect = element
|
||||
.querySelector('[data-testid="gateway-release-error-detail"]')!
|
||||
.getBoundingClientRect();
|
||||
return {
|
||||
tableWidth: element.getBoundingClientRect().width,
|
||||
scrollerX: scrollerRect.x,
|
||||
scrollerWidth: scrollerRect.width,
|
||||
scrollerScrollWidth: scroller.scrollWidth,
|
||||
detailWidth: detailRect.width,
|
||||
viewportWidth: document.documentElement.clientWidth,
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
};
|
||||
});
|
||||
expect(mobileGeometry.tableWidth).toBeLessThanOrEqual(mobileGeometry.scrollerWidth + 1);
|
||||
expect(mobileGeometry.detailWidth).toBeGreaterThanOrEqual(mobileGeometry.tableWidth - 1);
|
||||
expect(mobileGeometry.scrollerScrollWidth).toBeLessThanOrEqual(mobileGeometry.scrollerWidth + 1);
|
||||
expect(mobileGeometry.scrollerX).toBeGreaterThanOrEqual(0);
|
||||
expect(mobileGeometry.scrollerX + mobileGeometry.scrollerWidth).toBeLessThanOrEqual(
|
||||
mobileGeometry.viewportWidth
|
||||
);
|
||||
expect(mobileGeometry.documentScrollWidth).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
|
||||
await page.screenshot({ path: testInfo.outputPath('gateway-release-error-expanded-mobile.png'), fullPage: true });
|
||||
|
||||
await errorToggle.click();
|
||||
await expect(errorToggle).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(page.getByTestId('gateway-release-error-detail')).toBeHidden();
|
||||
});
|
||||
|
||||
test('explains terminal releases created before controller progress logging', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
operations: [],
|
||||
|
||||
Reference in New Issue
Block a user