forked from baron/baron-sso
356 lines
10 KiB
TypeScript
356 lines
10 KiB
TypeScript
import { expect, test, type Page, type Route } from '@playwright/test';
|
|
|
|
type ProfileState = {
|
|
department: string;
|
|
getMeCount: number;
|
|
putBodies: Array<Record<string, unknown>>;
|
|
};
|
|
|
|
async function enableFlutterAccessibility(page: Page): Promise<void> {
|
|
const button = page.getByRole('button', { name: 'Enable accessibility' });
|
|
if (await button.count()) {
|
|
await button.click({ force: true });
|
|
await page.waitForTimeout(200);
|
|
}
|
|
}
|
|
|
|
type ProfileCoords = {
|
|
departmentEditX: number;
|
|
departmentEditY: number;
|
|
departmentInputX: number;
|
|
departmentInputY: number;
|
|
blurX: number;
|
|
blurY: number;
|
|
};
|
|
|
|
const desktopCoords: ProfileCoords = {
|
|
departmentEditX: 1170,
|
|
departmentEditY: 680,
|
|
departmentInputX: 110,
|
|
departmentInputY: 685,
|
|
blurX: 200,
|
|
blurY: 260,
|
|
};
|
|
|
|
const mobileCoords: ProfileCoords = {
|
|
departmentEditX: 350,
|
|
departmentEditY: 680,
|
|
departmentInputX: 110,
|
|
departmentInputY: 685,
|
|
blurX: 200,
|
|
blurY: 260,
|
|
};
|
|
|
|
function coordsFor(page: Page): ProfileCoords {
|
|
const viewport = page.viewportSize();
|
|
return (viewport?.width ?? 1280) <= 500 ? mobileCoords : desktopCoords;
|
|
}
|
|
|
|
function isMobileProject(page: Page): boolean {
|
|
const viewport = page.viewportSize();
|
|
return (viewport?.width ?? 1280) <= 500;
|
|
}
|
|
|
|
async function seedTokenLogin(page: Page): Promise<void> {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem('baron_auth_token', 'e30.e30.e30');
|
|
window.localStorage.setItem('baron_auth_provider', 'ory');
|
|
window.localStorage.removeItem('baron_auth_cookie_mode');
|
|
window.localStorage.removeItem('baron_auth_pending_provider');
|
|
});
|
|
}
|
|
|
|
async function fillAt(page: Page, x: number, y: number, value: string): Promise<void> {
|
|
const pane = page.locator('flt-glass-pane');
|
|
await pane.click({ position: { x, y }, force: true });
|
|
await page.waitForTimeout(100);
|
|
await page.keyboard.press('Control+A');
|
|
await page.keyboard.press('Backspace');
|
|
await page.keyboard.type(value);
|
|
}
|
|
|
|
async function openDepartmentEditor(page: Page): Promise<void> {
|
|
if (isMobileProject(page)) {
|
|
await enableFlutterAccessibility(page);
|
|
await page
|
|
.getByRole('group', { name: '소속 QA' })
|
|
.getByRole('button', { name: '편집' })
|
|
.click({ force: true });
|
|
await page.waitForTimeout(200);
|
|
return;
|
|
}
|
|
const coords = coordsFor(page);
|
|
await page.locator('flt-glass-pane').click({
|
|
position: { x: coords.departmentEditX, y: coords.departmentEditY },
|
|
force: true,
|
|
});
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
async function blurDepartmentEditor(page: Page): Promise<void> {
|
|
if (isMobileProject(page)) {
|
|
await page.getByRole('textbox', { name: '소속' }).blur();
|
|
await page.waitForTimeout(250);
|
|
return;
|
|
}
|
|
const coords = coordsFor(page);
|
|
await page.locator('flt-glass-pane').click({
|
|
position: { x: coords.blurX, y: coords.blurY },
|
|
force: true,
|
|
});
|
|
await page.waitForTimeout(250);
|
|
}
|
|
|
|
async function submitDepartmentEditor(page: Page): Promise<void> {
|
|
if (isMobileProject(page)) {
|
|
await page.getByRole('textbox', { name: '소속' }).press('Enter');
|
|
await page.waitForTimeout(250);
|
|
return;
|
|
}
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForTimeout(250);
|
|
}
|
|
|
|
async function fillDepartmentField(page: Page, value: string): Promise<void> {
|
|
if (isMobileProject(page)) {
|
|
await page.getByRole('textbox', { name: '소속' }).fill(value);
|
|
return;
|
|
}
|
|
const coords = coordsFor(page);
|
|
await fillAt(page, coords.departmentInputX, coords.departmentInputY, value);
|
|
}
|
|
|
|
async function mockProfileApis(page: Page, state: ProfileState): Promise<void> {
|
|
await page.route('**/api/v1/**', async (route: Route) => {
|
|
const request = route.request();
|
|
const requestUrl = new URL(request.url());
|
|
const path = requestUrl.pathname;
|
|
const method = request.method().toUpperCase();
|
|
|
|
if (path.endsWith('/api/v1/user/me') && method === 'GET') {
|
|
const authHeader = request.headers()['authorization'] ?? '';
|
|
if (!authHeader.startsWith('Bearer ')) {
|
|
await route.fulfill({
|
|
status: 401,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ error: 'unauthorized' }),
|
|
});
|
|
return;
|
|
}
|
|
state.getMeCount += 1;
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
id: 'e2e-user',
|
|
email: 'e2e@example.com',
|
|
name: 'E2E User',
|
|
phone: '+821012341234',
|
|
department: state.department,
|
|
affiliationType: 'employee',
|
|
companyCode: 'BARON',
|
|
tenant: {
|
|
id: 'tenant-1',
|
|
name: 'Baron',
|
|
slug: 'baron',
|
|
description: 'E2E tenant',
|
|
},
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path.endsWith('/api/v1/user/me') && method === 'PUT') {
|
|
const body = (request.postDataJSON() ?? {}) as Record<string, unknown>;
|
|
state.putBodies.push(body);
|
|
const nextDepartment = String(body.department ?? '').trim();
|
|
if (nextDepartment !== '') {
|
|
state.department = nextDepartment;
|
|
}
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
status: 'success',
|
|
updatedAt: '2026-02-24T00:00:00Z',
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path.endsWith('/api/v1/user/rp/linked')) {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ items: [] }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path.endsWith('/api/v1/audit/auth/timeline')) {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ items: [], next_cursor: '' }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path.endsWith('/api/v1/client-log')) {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ ok: true }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ ok: true }),
|
|
});
|
|
});
|
|
}
|
|
|
|
async function openProfilePage(page: Page): Promise<void> {
|
|
await page.goto('/ko/profile');
|
|
await expect(page).toHaveURL(/\/ko\/profile$/);
|
|
await page.waitForTimeout(1200);
|
|
}
|
|
|
|
async function waitForInitialProfileLoad(state: ProfileState): Promise<void> {
|
|
await expect.poll(() => state.getMeCount).toBeGreaterThan(0);
|
|
}
|
|
|
|
test.describe('UserFront WASM profile department editing', () => {
|
|
test.skip(({ isMobile }) => isMobile, 'Desktop only (hardcoded coordinates)');
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
await page.unroute('**/api/v1/**');
|
|
});
|
|
|
|
test('소속 수정 후 명시 저장하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({
|
|
page,
|
|
}) => {
|
|
const state: ProfileState = {
|
|
department: 'QA',
|
|
getMeCount: 0,
|
|
putBodies: [],
|
|
};
|
|
await seedTokenLogin(page);
|
|
await mockProfileApis(page, state);
|
|
await openProfilePage(page);
|
|
await waitForInitialProfileLoad(state);
|
|
|
|
await openDepartmentEditor(page);
|
|
await fillDepartmentField(page, 'QA-Updated');
|
|
await submitDepartmentEditor(page);
|
|
|
|
await expect.poll(() => state.putBodies.length).toBe(1);
|
|
expect(state.putBodies[0]?.department).toBe('QA-Updated');
|
|
expect(state.department).toBe('QA-Updated');
|
|
|
|
const getCountBeforeReload = state.getMeCount;
|
|
await page.reload();
|
|
await expect(page).toHaveURL(/\/ko\/profile$/);
|
|
await expect.poll(() => state.getMeCount).toBeGreaterThan(getCountBeforeReload);
|
|
});
|
|
|
|
test('소속 입력 후 즉시 새로고침해도 저장 요청이 중복 전송되지 않는다', async ({
|
|
page,
|
|
}) => {
|
|
const state: ProfileState = {
|
|
department: 'QA',
|
|
getMeCount: 0,
|
|
putBodies: [],
|
|
};
|
|
await seedTokenLogin(page);
|
|
await mockProfileApis(page, state);
|
|
await openProfilePage(page);
|
|
await waitForInitialProfileLoad(state);
|
|
|
|
await openDepartmentEditor(page);
|
|
await fillDepartmentField(page, 'QA-Repro');
|
|
|
|
await page.reload();
|
|
await expect(page).toHaveURL(/\/ko\/profile$/);
|
|
expect(state.putBodies.length).toBeLessThanOrEqual(1);
|
|
if (state.putBodies.length > 0) {
|
|
expect(state.putBodies[0]?.department).toBe('QA-Repro');
|
|
expect(state.department).toBe('QA-Repro');
|
|
return;
|
|
}
|
|
expect(state.department).toBe('QA');
|
|
});
|
|
|
|
test('소속에 기존값을 그대로 입력하고 포커스 아웃하면 저장 요청이 전송되지 않는다', async ({
|
|
page,
|
|
}) => {
|
|
const state: ProfileState = {
|
|
department: 'QA',
|
|
getMeCount: 0,
|
|
putBodies: [],
|
|
};
|
|
await seedTokenLogin(page);
|
|
await mockProfileApis(page, state);
|
|
await openProfilePage(page);
|
|
await waitForInitialProfileLoad(state);
|
|
|
|
await openDepartmentEditor(page);
|
|
await fillDepartmentField(page, 'QA');
|
|
await blurDepartmentEditor(page);
|
|
|
|
expect(state.putBodies).toHaveLength(0);
|
|
});
|
|
|
|
test('소속을 빈 값으로 입력하면 저장 요청이 전송되지 않는다', async ({ page }) => {
|
|
const state: ProfileState = {
|
|
department: 'QA',
|
|
getMeCount: 0,
|
|
putBodies: [],
|
|
};
|
|
await seedTokenLogin(page);
|
|
await mockProfileApis(page, state);
|
|
await openProfilePage(page);
|
|
await waitForInitialProfileLoad(state);
|
|
|
|
await openDepartmentEditor(page);
|
|
await fillDepartmentField(page, '');
|
|
await blurDepartmentEditor(page);
|
|
|
|
expect(state.putBodies).toHaveLength(0);
|
|
expect(state.department).toBe('QA');
|
|
});
|
|
|
|
test('소속을 저장한 뒤 새로고침 후 다시 저장해도 저장 요청이 누락되지 않는다', async ({ page }) => {
|
|
const state: ProfileState = {
|
|
department: 'QA',
|
|
getMeCount: 0,
|
|
putBodies: [],
|
|
};
|
|
await seedTokenLogin(page);
|
|
await mockProfileApis(page, state);
|
|
await openProfilePage(page);
|
|
await waitForInitialProfileLoad(state);
|
|
|
|
await openDepartmentEditor(page);
|
|
await fillDepartmentField(page, 'QA-1');
|
|
await submitDepartmentEditor(page);
|
|
await expect.poll(() => state.putBodies.length).toBe(1);
|
|
|
|
await page.reload();
|
|
await expect(page).toHaveURL(/\/ko\/profile$/);
|
|
await page.waitForTimeout(1200);
|
|
|
|
await openDepartmentEditor(page);
|
|
await fillDepartmentField(page, 'QA-2');
|
|
await submitDepartmentEditor(page);
|
|
await expect.poll(() => state.putBodies.length).toBe(2);
|
|
|
|
expect(state.putBodies[0]?.department).toBe('QA-1');
|
|
expect(state.putBodies[1]?.department).toBe('QA-2');
|
|
expect(state.department).toBe('QA-2');
|
|
});
|
|
});
|