forked from baron/baron-sso
ci: add code check badges and coverage reports
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
import { expect, type Page, type Route, test } from "@playwright/test";
|
||||
|
||||
type ProfileState = {
|
||||
department: string;
|
||||
@@ -7,7 +7,7 @@ type ProfileState = {
|
||||
};
|
||||
|
||||
async function enableFlutterAccessibility(page: Page): Promise<void> {
|
||||
const button = page.getByRole('button', { name: 'Enable accessibility' });
|
||||
const button = page.getByRole("button", { name: "Enable accessibility" });
|
||||
if (await button.count()) {
|
||||
await button.click({ force: true }).catch(async () => {
|
||||
await page
|
||||
@@ -59,26 +59,31 @@ function isMobileProject(page: Page): boolean {
|
||||
|
||||
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');
|
||||
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');
|
||||
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 replaceFocusedText(page, value);
|
||||
}
|
||||
|
||||
async function replaceFocusedText(page: Page, value: string): Promise<void> {
|
||||
await page.keyboard.press('End');
|
||||
await page.keyboard.press("End");
|
||||
for (let index = 0; index < 64; index += 1) {
|
||||
await page.keyboard.press('Backspace');
|
||||
await page.keyboard.press("Backspace");
|
||||
}
|
||||
if (value !== '') {
|
||||
if (value !== "") {
|
||||
await page.keyboard.insertText(value);
|
||||
}
|
||||
await page.waitForTimeout(100);
|
||||
@@ -89,8 +94,12 @@ type BoxCenter = {
|
||||
y: number;
|
||||
};
|
||||
|
||||
async function resolveLocatorCenter(locator: ReturnType<Page['locator']>): Promise<BoxCenter | null> {
|
||||
const handle = await locator.elementHandle({ timeout: 1_000 }).catch(() => null);
|
||||
async function resolveLocatorCenter(
|
||||
locator: ReturnType<Page["locator"]>,
|
||||
): Promise<BoxCenter | null> {
|
||||
const handle = await locator
|
||||
.elementHandle({ timeout: 1_000 })
|
||||
.catch(() => null);
|
||||
if (!handle) {
|
||||
return null;
|
||||
}
|
||||
@@ -115,11 +124,14 @@ async function resolveLocatorCenter(locator: ReturnType<Page['locator']>): Promi
|
||||
};
|
||||
}
|
||||
|
||||
async function clickGlassPaneAt(page: Page, center: BoxCenter | null): Promise<boolean> {
|
||||
async function clickGlassPaneAt(
|
||||
page: Page,
|
||||
center: BoxCenter | null,
|
||||
): Promise<boolean> {
|
||||
if (!center) {
|
||||
return false;
|
||||
}
|
||||
await page.locator('flt-glass-pane').click({
|
||||
await page.locator("flt-glass-pane").click({
|
||||
position: center,
|
||||
force: true,
|
||||
});
|
||||
@@ -128,22 +140,25 @@ async function clickGlassPaneAt(page: Page, center: BoxCenter | null): Promise<b
|
||||
}
|
||||
|
||||
async function departmentTextboxIsOpen(page: Page): Promise<boolean> {
|
||||
return (await page.getByRole('textbox', { name: '소속' }).count()) > 0;
|
||||
return (await page.getByRole("textbox", { name: "소속" }).count()) > 0;
|
||||
}
|
||||
|
||||
async function openDepartmentEditor(page: Page): Promise<void> {
|
||||
const accessibleEditor = page
|
||||
.getByRole('group', { name: '소속 QA' })
|
||||
.getByRole('button', { name: '편집' });
|
||||
const textbox = page.getByRole('textbox', { name: '소속' });
|
||||
.getByRole("group", { name: "소속 QA" })
|
||||
.getByRole("button", { name: "편집" });
|
||||
const textbox = page.getByRole("textbox", { name: "소속" });
|
||||
if ((await accessibleEditor.count()) > 0) {
|
||||
const editorCenter = await resolveLocatorCenter(accessibleEditor);
|
||||
await accessibleEditor
|
||||
.evaluate((element) => {
|
||||
if (element instanceof HTMLElement) {
|
||||
element.click();
|
||||
}
|
||||
}, { timeout: 1_000 })
|
||||
.evaluate(
|
||||
(element) => {
|
||||
if (element instanceof HTMLElement) {
|
||||
element.click();
|
||||
}
|
||||
},
|
||||
{ timeout: 1_000 },
|
||||
)
|
||||
.catch(() => undefined);
|
||||
await page.waitForTimeout(200);
|
||||
if (await departmentTextboxIsOpen(page)) {
|
||||
@@ -153,14 +168,16 @@ async function openDepartmentEditor(page: Page): Promise<void> {
|
||||
if (await departmentTextboxIsOpen(page)) {
|
||||
return;
|
||||
}
|
||||
await accessibleEditor.click({ force: true, timeout: 1_000 }).catch(() => undefined);
|
||||
await accessibleEditor
|
||||
.click({ force: true, timeout: 1_000 })
|
||||
.catch(() => undefined);
|
||||
await page.waitForTimeout(200);
|
||||
if (await departmentTextboxIsOpen(page)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isMobileProject(page)) {
|
||||
throw new Error('Department editor accessibility button was not found.');
|
||||
throw new Error("Department editor accessibility button was not found.");
|
||||
}
|
||||
const coords = coordsFor(page);
|
||||
const viewport = page.viewportSize();
|
||||
@@ -180,17 +197,17 @@ async function openDepartmentEditor(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
async function blurDepartmentEditor(page: Page): Promise<void> {
|
||||
const textbox = page.getByRole('textbox', { name: '소속' });
|
||||
const textbox = page.getByRole("textbox", { name: "소속" });
|
||||
if ((await textbox.count()) > 0) {
|
||||
await textbox.blur();
|
||||
await page.waitForTimeout(250);
|
||||
return;
|
||||
}
|
||||
if (isMobileProject(page)) {
|
||||
throw new Error('Department textbox was not found.');
|
||||
throw new Error("Department textbox was not found.");
|
||||
}
|
||||
const coords = coordsFor(page);
|
||||
await page.locator('flt-glass-pane').click({
|
||||
await page.locator("flt-glass-pane").click({
|
||||
position: { x: coords.blurX, y: coords.blurY },
|
||||
force: true,
|
||||
});
|
||||
@@ -198,21 +215,21 @@ async function blurDepartmentEditor(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
async function submitDepartmentEditor(page: Page): Promise<void> {
|
||||
const textbox = page.getByRole('textbox', { name: '소속' });
|
||||
const textbox = page.getByRole("textbox", { name: "소속" });
|
||||
if ((await textbox.count()) > 0) {
|
||||
await textbox.press('Enter');
|
||||
await textbox.press("Enter");
|
||||
await page.waitForTimeout(250);
|
||||
return;
|
||||
}
|
||||
if (isMobileProject(page)) {
|
||||
throw new Error('Department textbox was not found.');
|
||||
throw new Error("Department textbox was not found.");
|
||||
}
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press("Enter");
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
async function fillDepartmentField(page: Page, value: string): Promise<void> {
|
||||
const textbox = page.getByRole('textbox', { name: '소속' });
|
||||
const textbox = page.getByRole("textbox", { name: "소속" });
|
||||
if (!isMobileProject(page)) {
|
||||
if ((await textbox.count()) > 0) {
|
||||
await textbox.click({ force: true });
|
||||
@@ -230,92 +247,92 @@ async function fillDepartmentField(page: Page, value: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
if (isMobileProject(page)) {
|
||||
throw new Error('Department textbox was not found.');
|
||||
throw new Error("Department textbox was not found.");
|
||||
}
|
||||
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) => {
|
||||
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 ')) {
|
||||
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' }),
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ error: "unauthorized" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
state.getMeCount += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id: 'e2e-user',
|
||||
email: 'e2e@example.com',
|
||||
name: 'E2E User',
|
||||
phone: '+821012341234',
|
||||
id: "e2e-user",
|
||||
email: "e2e@example.com",
|
||||
name: "E2E User",
|
||||
phone: "+821012341234",
|
||||
department: state.department,
|
||||
affiliationType: 'employee',
|
||||
companyCode: 'BARON',
|
||||
affiliationType: "employee",
|
||||
companyCode: "BARON",
|
||||
tenant: {
|
||||
id: 'tenant-1',
|
||||
name: 'Baron',
|
||||
slug: 'baron',
|
||||
description: 'E2E tenant',
|
||||
id: "tenant-1",
|
||||
name: "Baron",
|
||||
slug: "baron",
|
||||
description: "E2E tenant",
|
||||
},
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/user/me') && method === 'PUT') {
|
||||
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 !== '') {
|
||||
const nextDepartment = String(body.department ?? "").trim();
|
||||
if (nextDepartment !== "") {
|
||||
state.department = nextDepartment;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
status: 'success',
|
||||
updatedAt: '2026-02-24T00:00:00Z',
|
||||
status: "success",
|
||||
updatedAt: "2026-02-24T00:00:00Z",
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/user/rp/linked')) {
|
||||
if (path.endsWith("/api/v1/user/rp/linked")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ items: [] }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/audit/auth/timeline')) {
|
||||
if (path.endsWith("/api/v1/audit/auth/timeline")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ items: [], next_cursor: '' }),
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ items: [], next_cursor: "" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/client-log')) {
|
||||
if (path.endsWith("/api/v1/client-log")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
@@ -323,14 +340,14 @@ async function mockProfileApis(page: Page, state: ProfileState): Promise<void> {
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function openProfilePage(page: Page): Promise<void> {
|
||||
await page.goto('/ko/profile');
|
||||
await page.goto("/ko/profile");
|
||||
await expect(page).toHaveURL(/\/ko\/profile$/);
|
||||
await enableFlutterAccessibility(page);
|
||||
await page.waitForTimeout(1200);
|
||||
@@ -340,22 +357,22 @@ 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.describe("UserFront WASM profile department editing", () => {
|
||||
test.skip(({ isMobile }) => isMobile, "Desktop only (hardcoded coordinates)");
|
||||
test.skip(
|
||||
({ browserName }) => browserName === 'webkit',
|
||||
'WebKit headless does not consistently open Flutter profile edit controls; Chromium and Firefox cover this flow.',
|
||||
({ browserName }) => browserName === "webkit",
|
||||
"WebKit headless does not consistently open Flutter profile edit controls; Chromium and Firefox cover this flow.",
|
||||
);
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await page.unroute('**/api/v1/**');
|
||||
await page.unroute("**/api/v1/**");
|
||||
});
|
||||
|
||||
test('소속 수정 후 명시 저장하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({
|
||||
test("소속 수정 후 명시 저장하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
department: "QA",
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
@@ -365,24 +382,26 @@ test.describe('UserFront WASM profile department editing', () => {
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillDepartmentField(page, 'QA-Updated');
|
||||
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');
|
||||
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);
|
||||
await expect
|
||||
.poll(() => state.getMeCount)
|
||||
.toBeGreaterThan(getCountBeforeReload);
|
||||
});
|
||||
|
||||
test('소속 입력 후 즉시 새로고침해도 저장 요청이 중복 전송되지 않는다', async ({
|
||||
test("소속 입력 후 즉시 새로고침해도 저장 요청이 중복 전송되지 않는다", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
department: "QA",
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
@@ -392,24 +411,24 @@ test.describe('UserFront WASM profile department editing', () => {
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillDepartmentField(page, 'QA-Repro');
|
||||
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');
|
||||
expect(state.putBodies[0]?.department).toBe("QA-Repro");
|
||||
expect(state.department).toBe("QA-Repro");
|
||||
return;
|
||||
}
|
||||
expect(state.department).toBe('QA');
|
||||
expect(state.department).toBe("QA");
|
||||
});
|
||||
|
||||
test('소속에 기존값을 그대로 입력하고 포커스 아웃하면 저장 요청이 전송되지 않는다', async ({
|
||||
test("소속에 기존값을 그대로 입력하고 포커스 아웃하면 저장 요청이 전송되지 않는다", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
department: "QA",
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
@@ -419,15 +438,17 @@ test.describe('UserFront WASM profile department editing', () => {
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillDepartmentField(page, 'QA');
|
||||
await fillDepartmentField(page, "QA");
|
||||
await blurDepartmentEditor(page);
|
||||
|
||||
expect(state.putBodies).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('소속을 빈 값으로 입력하면 저장 요청이 전송되지 않는다', async ({ page }) => {
|
||||
test("소속을 빈 값으로 입력하면 저장 요청이 전송되지 않는다", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
department: "QA",
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
@@ -437,16 +458,18 @@ test.describe('UserFront WASM profile department editing', () => {
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillDepartmentField(page, '');
|
||||
await fillDepartmentField(page, "");
|
||||
await blurDepartmentEditor(page);
|
||||
|
||||
expect(state.putBodies).toHaveLength(0);
|
||||
expect(state.department).toBe('QA');
|
||||
expect(state.department).toBe("QA");
|
||||
});
|
||||
|
||||
test('소속을 저장한 뒤 새로고침 후 다시 저장해도 저장 요청이 누락되지 않는다', async ({ page }) => {
|
||||
test("소속을 저장한 뒤 새로고침 후 다시 저장해도 저장 요청이 누락되지 않는다", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
department: "QA",
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
@@ -456,7 +479,7 @@ test.describe('UserFront WASM profile department editing', () => {
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillDepartmentField(page, 'QA-1');
|
||||
await fillDepartmentField(page, "QA-1");
|
||||
await submitDepartmentEditor(page);
|
||||
await expect.poll(() => state.putBodies.length).toBe(1);
|
||||
|
||||
@@ -464,16 +487,18 @@ test.describe('UserFront WASM profile department editing', () => {
|
||||
await page.reload();
|
||||
await expect(page).toHaveURL(/\/ko\/profile$/);
|
||||
await enableFlutterAccessibility(page);
|
||||
await expect.poll(() => state.getMeCount).toBeGreaterThan(getCountBeforeReload);
|
||||
await expect
|
||||
.poll(() => state.getMeCount)
|
||||
.toBeGreaterThan(getCountBeforeReload);
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillDepartmentField(page, 'QA-2');
|
||||
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');
|
||||
expect(state.putBodies[0]?.department).toBe("QA-1");
|
||||
expect(state.putBodies[1]?.department).toBe("QA-2");
|
||||
expect(state.department).toBe("QA-2");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user