1
0
forked from baron/baron-sso

ci: add code check badges and coverage reports

This commit is contained in:
2026-05-29 12:05:43 +09:00
parent c489c7c38f
commit a830242947
164 changed files with 9059 additions and 2012 deletions

View File

@@ -164,7 +164,9 @@ test.describe("Tenants Management", () => {
await expect(page.locator("table")).toContainText("Platform");
await expect(page.locator("table")).toContainText("Acme");
await page.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i).fill("");
await page
.getByPlaceholder(/테넌트 이름 또는 슬러그 검색|search/i)
.fill("");
await page
.locator("tbody tr")
.filter({ hasText: "Planning" })
@@ -538,7 +540,10 @@ test.describe("Tenants Management", () => {
test("should create a hanmac-family child tenant with org config", async ({
page,
}) => {
test.skip(true, "브라우저별 org picker 상호작용이 불안정하여 unit 테스트로 커버합니다.");
test.skip(
true,
"브라우저별 org picker 상호작용이 불안정하여 unit 테스트로 커버합니다.",
);
await page.setViewportSize({ width: 1280, height: 800 });
let createBody = "";
const tenants = [

View File

@@ -470,6 +470,193 @@ test.describe("User Management", () => {
.toMatchObject({ status: "preboarding" });
});
test("should center users table loading state and use compact headers", async ({
page,
}) => {
let resolveUsers: (() => void) | undefined;
const usersGate = new Promise<void>((resolve) => {
resolveUsers = resolve;
});
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
await usersGate;
return route.fulfill({
json: {
items: [],
total: 0,
limit: 50,
offset: 0,
},
});
});
await page.goto("/users");
const loadingCell = page.getByTestId("user-table-loading-cell");
await expect(loadingCell).toBeVisible();
await expect(loadingCell).toHaveCSS("display", "flex");
await expect(loadingCell).toHaveCSS("align-items", "center");
await expect(loadingCell).toHaveCSS("justify-content", "center");
const nameHeader = page.getByRole("columnheader", { name: /이름|Name/i });
await expect(nameHeader).toHaveClass(/h-9/);
await expect(nameHeader.locator("> div")).toHaveClass(/h-full/);
resolveUsers?.();
await expect(page.getByTestId("user-table-empty-cell")).toBeVisible();
});
test("should virtualize large user result rows in the users table", async ({
page,
}) => {
const manyUsers = Array.from({ length: 500 }, (_, index) => ({
id: `u-${index}`,
name: `User ${index}`,
email: `user${index}@test.com`,
phone: "010-1111-2222",
loginId: `user${index}`,
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
}));
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
return route.fulfill({
json: {
items: manyUsers,
total: manyUsers.length,
limit: manyUsers.length,
offset: 0,
},
});
});
await page.goto("/users");
await expect(page.getByText("User 0")).toBeVisible();
const renderedStatusControls = await page
.getByTestId(/^user-status-select-/)
.count();
expect(renderedStatusControls).toBeLessThan(manyUsers.length);
await expect(page.getByText("User 499")).toHaveCount(0);
await page.getByTestId("user-table-viewport").evaluate((element) => {
element.scrollTop = element.scrollHeight;
element.dispatchEvent(new Event("scroll", { bubbles: true }));
});
await expect(page.getByText("User 499")).toBeVisible();
});
test("should keep large user search rendering under 200ms", async ({
page,
}) => {
const manyUsers = Array.from({ length: 20_000 }, (_, index) => ({
id: `load-u-${index}`,
name: `Load User ${index}`,
email: `load-user-${index}@test.com`,
phone: "010-1111-2222",
loginId: `load-user-${index}`,
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
}));
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
const url = new URL(route.request().url());
const normalizedSearch = url.searchParams
.get("search")
?.trim()
.toLowerCase();
const items = normalizedSearch
? manyUsers.filter((user) =>
`${user.name} ${user.email}`
.toLowerCase()
.includes(normalizedSearch),
)
: manyUsers;
return route.fulfill({
json: {
items,
total: items.length,
limit: items.length,
offset: 0,
},
});
});
const initialStartedAt = performance.now();
await page.goto("/users");
await expect(page.getByText("Load User 0")).toBeVisible();
const initialMs = performance.now() - initialStartedAt;
const searchInput = page.getByPlaceholder("이름 또는 이메일 검색...");
await searchInput.fill("Load User 19999");
const searchMs = await page.evaluate(async () => {
const input = Array.from(document.querySelectorAll("input")).find(
(candidate) => candidate.placeholder === "이름 또는 이메일 검색...",
);
if (!input) {
throw new Error("User search input was not found.");
}
return await new Promise<number>((resolve, reject) => {
const startedAt = performance.now();
const timeout = window.setTimeout(() => {
observer.disconnect();
reject(new Error("Timed out waiting for large user search result."));
}, 1000);
const observer = new MutationObserver(() => {
const bodyText = document.body.textContent ?? "";
if (
bodyText.includes("Load User 19999") &&
!bodyText.includes("Load User 0")
) {
window.clearTimeout(timeout);
observer.disconnect();
resolve(performance.now() - startedAt);
}
});
observer.observe(document.body, {
childList: true,
characterData: true,
subtree: true,
});
input.dispatchEvent(
new KeyboardEvent("keydown", {
bubbles: true,
cancelable: true,
key: "Enter",
}),
);
});
});
await expect(page.getByText("Load User 19999")).toBeVisible();
await expect(page.getByText("Load User 0")).toHaveCount(0);
console.log(
`[perf] users initial render with ${manyUsers.length} rows: ${initialMs.toFixed(1)}ms`,
);
console.log(
`[perf] users search update with ${manyUsers.length} rows: ${searchMs.toFixed(1)}ms`,
);
expect(searchMs).toBeLessThan(200);
});
test("should expose internal user uuid in the users table", async ({
page,
}) => {