forked from baron/baron-sso
ci: add code check badges and coverage reports
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user