1
0
forked from baron/baron-sso

chore: snapshot local state before dev merge

This commit is contained in:
2026-06-17 21:25:42 +09:00
parent b2808759d2
commit 49560e8a8c
107 changed files with 8958 additions and 939 deletions

View File

@@ -0,0 +1,402 @@
import { performance } from "node:perf_hooks";
import { expect, test } from "@playwright/test";
const tenantCount = 3500;
const userCount = 3500;
type TenantFixture = {
id: string;
name: string;
slug: string;
status: string;
type: string;
memberCount: number;
recursiveMemberCount: number;
createdAt: string;
updatedAt: string;
};
type UserFixture = {
id: string;
name: string;
email: string;
phone: string;
loginId: string;
role: string;
status: string;
tenantId: string;
tenantSlug: string;
tenantName: string;
department: string;
createdAt: string;
};
function buildTenants(): TenantFixture[] {
const baseTime = Date.UTC(2026, 0, 1, 0, 0, 0);
return Array.from({ length: tenantCount }, (_, index) => {
const sequence = index + 1;
const padded = String(sequence).padStart(4, "0");
const timestamp = new Date(baseTime + sequence * 1000).toISOString();
return {
id: `tenant-${padded}`,
name: `Tenant ${padded}`,
slug: sequence === 100 ? "full-dataset-needle-0100" : `tenant-${padded}`,
status: sequence % 17 === 0 ? "inactive" : "active",
type: sequence % 5 === 0 ? "ORGANIZATION" : "COMPANY",
memberCount: sequence % 13,
recursiveMemberCount: sequence % 29,
createdAt: timestamp,
updatedAt: timestamp,
};
});
}
function buildUsers(): UserFixture[] {
const baseTime = Date.UTC(2026, 0, 1, 0, 0, 0);
return Array.from({ length: userCount }, (_, index) => {
const sequence = index + 1;
const padded = String(sequence).padStart(4, "0");
const timestamp = new Date(baseTime + sequence * 1000).toISOString();
const email =
sequence === 100
? "full-dataset-user-needle-0100@example.com"
: `user-${padded}@example.com`;
return {
id: `user-${padded}`,
name: `User ${padded}`,
email,
phone: "010-1111-2222",
loginId: `user-${padded}`,
role: "user",
status: sequence % 19 === 0 ? "inactive" : "active",
tenantId: "tenant-main",
tenantSlug: "tenant-main",
tenantName: "Main Tenant",
department: "Platform",
createdAt: timestamp,
};
});
}
function compareTenantValues(
left: TenantFixture,
right: TenantFixture,
sortKey: string,
) {
const key = sortKey as keyof TenantFixture;
const leftValue = left[key] ?? "";
const rightValue = right[key] ?? "";
return String(leftValue).localeCompare(String(rightValue));
}
test.describe("Tenant list performance", () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem("locale", "ko");
window.localStorage.setItem("admin_session", "fake-token");
(
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
)._IS_TEST_MODE = true;
const authority = "http://localhost:5000/oidc";
const client_id = "adminfront";
const key = `oidc.user:${authority}:${client_id}`;
window.localStorage.setItem(
key,
JSON.stringify({
access_token: "fake-token",
token_type: "Bearer",
profile: { sub: "admin-user", name: "Admin", role: "super_admin" },
expires_at: Math.floor(Date.now() / 1000) + 36000,
}),
);
});
await page.route("**/oidc/**", async (route) => {
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
});
});
test("loads and searches the tenant list within the performance budget", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 });
const tenants = buildTenants();
await page.route("**/api/v1/**", async (route) => {
const url = new URL(route.request().url());
const headers = { "Access-Control-Allow-Origin": "*" };
if (url.pathname.endsWith("/user/me")) {
return route.fulfill({
json: {
id: "admin-user",
name: "Admin",
role: "super_admin",
manageableTenants: [],
},
headers,
});
}
if (
url.pathname.endsWith("/admin/tenants") &&
route.request().method() === "GET"
) {
const limit = Number(url.searchParams.get("limit") ?? "500");
const cursor = Number(url.searchParams.get("cursor") ?? "0");
const search = url.searchParams.get("search")?.trim().toLowerCase();
const sort = url.searchParams.get("sort") ?? "createdAt";
const direction = url.searchParams.get("direction") ?? "desc";
let filtered = tenants;
if (search) {
filtered = tenants.filter((tenant) =>
[tenant.id, tenant.name, tenant.slug, tenant.type].some((value) =>
value.toLowerCase().includes(search),
),
);
}
const sorted = [...filtered].sort((left, right) => {
const result = compareTenantValues(left, right, sort);
return direction === "asc" ? result : -result;
});
const pageItems = sorted.slice(cursor, cursor + limit);
const nextOffset = cursor + limit;
return route.fulfill({
json: {
items: pageItems,
total: sorted.length,
limit,
offset: 0,
nextCursor:
nextOffset < sorted.length ? String(nextOffset) : undefined,
},
headers,
});
}
return route.fulfill({ json: { items: [], total: 0 }, headers });
});
const loadStarted = performance.now();
await page.goto("/tenants");
await expect(page.getByTestId("tenant-internal-id-tenant-3500")).toBeVisible(
{ timeout: 15000 },
);
const loadMs = performance.now() - loadStarted;
const loadSnapshot = testInfo.outputPath("tenant-list-load.png");
await page.screenshot({ path: loadSnapshot, fullPage: true });
await expect(page.locator("tbody tr").first()).toContainText("Tenant 3500");
const searchInput = page.getByPlaceholder("이름 또는 슬러그, ID 검색");
const searchStarted = performance.now();
await searchInput.fill("full-dataset-needle-0100");
await expect(page.getByTestId("tenant-internal-id-tenant-0100")).toBeVisible(
{ timeout: 15000 },
);
const searchMs = performance.now() - searchStarted;
const searchSnapshot = testInfo.outputPath("tenant-list-search.png");
await page.screenshot({ path: searchSnapshot, fullPage: true });
await expect(page.locator("tbody")).toContainText(
"full-dataset-needle-0100",
);
await expect(page.getByTestId("tenant-internal-id-tenant-3500")).toHaveCount(
0,
);
console.log(
JSON.stringify({
metric: "tenant-list-performance",
loadMs: Math.round(loadMs),
searchMs: Math.round(searchMs),
loadSnapshot,
searchSnapshot,
}),
);
expect(loadMs).toBeLessThanOrEqual(1500);
expect(searchMs).toBeLessThanOrEqual(500);
});
});
test.describe("User list performance", () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem("locale", "ko");
window.localStorage.setItem("admin_session", "fake-token");
(
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
)._IS_TEST_MODE = true;
const authority = "http://localhost:5000/oidc";
const client_id = "adminfront";
const key = `oidc.user:${authority}:${client_id}`;
window.localStorage.setItem(
key,
JSON.stringify({
id_token: "fake-id-token",
access_token: "fake-token",
token_type: "Bearer",
scope: "openid profile email",
profile: {
sub: "admin-user",
name: "Admin",
email: "admin@test.com",
role: "super_admin",
},
expires_at: Math.floor(Date.now() / 1000) + 36000,
}),
);
});
await page.route("**/oidc/**", async (route) => {
if (route.request().url().includes("/.well-known/openid-configuration")) {
return route.fulfill({
json: {
issuer: "http://localhost:5000/oidc",
authorization_endpoint: "http://localhost:5000/oidc/auth",
token_endpoint: "http://localhost:5000/oidc/token",
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
jwks_uri: "http://localhost:5000/oidc/jwks",
},
});
}
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
});
});
test("loads and searches the user list within the performance budget", async ({
page,
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 900 });
const users = buildUsers();
await page.route("**/api/v1/**", async (route) => {
const url = new URL(route.request().url());
const headers = { "Access-Control-Allow-Origin": "*" };
if (url.pathname.endsWith("/user/me")) {
return route.fulfill({
json: {
id: "admin-user",
name: "Admin",
email: "admin@test.com",
role: "super_admin",
manageableTenants: [],
},
headers,
});
}
if (
url.pathname.endsWith("/admin/tenants") &&
route.request().method() === "GET"
) {
return route.fulfill({
json: {
items: [
{
id: "tenant-main",
slug: "tenant-main",
name: "Main Tenant",
type: "COMPANY",
config: { userSchema: [] },
},
],
total: 1,
limit: 500,
offset: 0,
},
headers,
});
}
if (
url.pathname.endsWith("/admin/users") &&
route.request().method() === "GET"
) {
const limit = Number(url.searchParams.get("limit") ?? "50");
const cursor = Number(url.searchParams.get("cursor") ?? "0");
const search = url.searchParams.get("search")?.trim().toLowerCase();
const filtered = search
? users.filter((user) =>
[user.id, user.name, user.email, user.loginId].some((value) =>
value.toLowerCase().includes(search),
),
)
: users;
const sorted = [...filtered].sort((left, right) =>
right.createdAt.localeCompare(left.createdAt),
);
const pageItems = sorted.slice(cursor, cursor + limit);
const nextOffset = cursor + limit;
return route.fulfill({
json: {
items: pageItems,
total: sorted.length,
limit,
offset: 0,
nextCursor:
nextOffset < sorted.length ? String(nextOffset) : undefined,
},
headers,
});
}
return route.fulfill({ json: { items: [], total: 0 }, headers });
});
const loadStarted = performance.now();
await page.goto("/users");
await expect(page.getByTestId("user-internal-id-user-3500")).toBeVisible({
timeout: 15000,
});
const loadMs = performance.now() - loadStarted;
const loadSnapshot = testInfo.outputPath("user-list-load.png");
await page.screenshot({ path: loadSnapshot, fullPage: true });
await expect(page.getByText("User 3500")).toBeVisible();
const searchInput = page.getByPlaceholder("이름 또는 이메일 검색");
const searchStarted = performance.now();
await searchInput.fill("full-dataset-user-needle-0100");
await expect(page.getByTestId("user-internal-id-user-0100")).toBeVisible({
timeout: 15000,
});
const searchMs = performance.now() - searchStarted;
const searchSnapshot = testInfo.outputPath("user-list-search.png");
await page.screenshot({ path: searchSnapshot, fullPage: true });
await expect(
page.getByText("full-dataset-user-needle-0100@example.com"),
).toBeVisible();
await expect(page.getByTestId("user-internal-id-user-3500")).toHaveCount(0);
console.log(
JSON.stringify({
metric: "user-list-performance",
loadMs: Math.round(loadMs),
searchMs: Math.round(searchMs),
loadSnapshot,
searchSnapshot,
}),
);
expect(loadMs).toBeLessThanOrEqual(1500);
expect(searchMs).toBeLessThanOrEqual(500);
});
});

View File

@@ -0,0 +1,219 @@
import fs from "node:fs";
import path from "node:path";
import { performance } from "node:perf_hooks";
import { expect, test, type Route } from "@playwright/test";
const targetTenantId =
process.env.TENANT_PROFILE_PERF_TENANT_ID ??
"56cd0fd7-b62a-43c0-8db9-74a30468d7cb";
const actualApiBaseUrl =
process.env.TENANT_PROFILE_PERF_API_BASE_URL ?? "http://localhost:5173/api";
const normalizedActualApiBaseUrl = actualApiBaseUrl.replace(/\/$/, "");
const evidenceDir = path.resolve("e2e-evidence");
type ApiTiming = {
method: string;
url: string;
status: number;
durationMs: number;
};
type Measurement = {
sample: number;
configFieldsVisibleMs: number;
networkIdleMs: number;
orgUnitType: string | null;
visibility: string | null;
worksmobileSync: string | null;
apiTimings: ApiTiming[];
};
async function fulfillFromLocalApi(route: Route, targetUrl?: string) {
const request = route.request();
const corsHeaders = {
"access-control-allow-headers": "authorization,content-type,x-test-role",
"access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
"access-control-allow-origin": "*",
};
if (request.method() === "OPTIONS") {
await route.fulfill({ status: 204, headers: corsHeaders });
return;
}
const headers = { ...request.headers(), "x-test-role": "super_admin" };
delete headers.authorization;
delete headers.host;
const response = await route.fetch({ url: targetUrl, headers });
await route.fulfill({
response,
headers: { ...response.headers(), ...corsHeaders },
});
}
function percentile(values: number[], ratio: number) {
const sorted = [...values].sort((left, right) => left - right);
const index = Math.min(
sorted.length - 1,
Math.ceil(sorted.length * ratio) - 1,
);
return sorted[index] ?? 0;
}
test.describe("Tenant profile local performance evidence", () => {
test("loads org config fields through the local API within 500ms", async ({
page,
}, testInfo) => {
fs.mkdirSync(evidenceDir, { recursive: true });
await page.setViewportSize({ width: 1440, height: 900 });
await page.addInitScript(() => {
window.localStorage.setItem("locale", "ko");
window.localStorage.setItem("X-Mock-Role-Enabled", "true");
window.localStorage.setItem("X-Mock-Role", "super_admin");
window.localStorage.removeItem("admin_session");
for (const key of Object.keys(window.localStorage)) {
if (key.startsWith("oidc.user:")) {
window.localStorage.removeItem(key);
}
}
(
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
)._IS_TEST_MODE = true;
});
await page.route("**/oidc/**", async (route) => {
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
});
await page.route("**/api/**", async (route) => {
await fulfillFromLocalApi(route);
});
await page.route("http://playwright-mock/api/**", async (route) => {
const request = route.request();
const source = new URL(request.url());
const target = `${normalizedActualApiBaseUrl}${source.pathname.replace(
/^\/api/,
"",
)}${source.search}`;
await fulfillFromLocalApi(route, target);
});
const requestStartedAt = new Map<string, number>();
const apiTimings: ApiTiming[] = [];
page.on("request", (request) => {
const url = request.url();
if (url.includes("/api/v1/") || url.includes("playwright-mock/api")) {
requestStartedAt.set(request.url(), performance.now());
}
});
page.on("response", (response) => {
const request = response.request();
const startedAt = requestStartedAt.get(request.url());
if (startedAt === undefined) {
return;
}
const timing = {
method: request.method(),
url: response.url(),
status: response.status(),
durationMs: Math.round(performance.now() - startedAt),
};
apiTimings.push(timing);
});
page.on("requestfailed", (request) => {
const url = request.url();
if (url.includes("/api/v1/") || url.includes("playwright-mock/api")) {
console.log(
"api-request-failed",
JSON.stringify({
method: request.method(),
url,
failure: request.failure()?.errorText,
}),
);
}
});
const measurements: Measurement[] = [];
const sampleCount = 5;
for (let sample = 1; sample <= sampleCount; sample += 1) {
apiTimings.length = 0;
const startedAt = performance.now();
await page.goto(`/tenants/${targetTenantId}`, {
waitUntil: "domcontentloaded",
});
const orgUnitTypeSelect = page.getByTestId("tenant-org-unit-type-select");
await expect(orgUnitTypeSelect).toBeVisible({ timeout: 15000 });
await expect(page.locator("#tenant-visibility")).toBeVisible();
await expect(page.locator("#worksmobileExcluded")).toBeVisible();
const configFieldsVisibleMs = Math.round(performance.now() - startedAt);
await page.waitForLoadState("networkidle", { timeout: 15000 });
const networkIdleMs = Math.round(performance.now() - startedAt);
measurements.push({
sample,
configFieldsVisibleMs,
networkIdleMs,
orgUnitType: await orgUnitTypeSelect.inputValue(),
visibility: await page.locator("#tenant-visibility").inputValue(),
worksmobileSync: await page
.locator("#worksmobileExcluded")
.inputValue(),
apiTimings: [...apiTimings],
});
}
const screenshotPath = path.join(
evidenceDir,
"tenant-profile-performance-local.png",
);
await page.screenshot({ path: screenshotPath, fullPage: true });
const configTimes = measurements.map(
(measurement) => measurement.configFieldsVisibleMs,
);
const networkIdleTimes = measurements.map(
(measurement) => measurement.networkIdleMs,
);
const evidence = {
metric: "tenant-profile-local-performance",
tenantId: targetTenantId,
actualApiBaseUrl,
measuredAt: new Date().toISOString(),
browser: testInfo.project.name,
samples: measurements,
summary: {
configFieldsVisibleMs: {
min: Math.min(...configTimes),
max: Math.max(...configTimes),
p50: percentile(configTimes, 0.5),
p95: percentile(configTimes, 0.95),
},
networkIdleMs: {
min: Math.min(...networkIdleTimes),
max: Math.max(...networkIdleTimes),
p50: percentile(networkIdleTimes, 0.5),
p95: percentile(networkIdleTimes, 0.95),
},
},
screenshotPath,
};
const evidencePath = path.join(
evidenceDir,
"tenant-profile-performance-local.json",
);
fs.writeFileSync(evidencePath, `${JSON.stringify(evidence, null, 2)}\n`);
console.log(JSON.stringify(evidence, null, 2));
expect(evidence.summary.configFieldsVisibleMs.p95).toBeLessThanOrEqual(500);
});
});

View File

@@ -527,6 +527,93 @@ test.describe("Tenants Management", () => {
);
});
test("should bulk update selected tenant status type and visibility", async ({
page,
}) => {
await page.setViewportSize({ width: 1100, height: 760 });
const updatePayloads: Record<string, unknown>[] = [];
await page.route("**/api/v1/admin/tenants**", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (request.method() === "PUT") {
updatePayloads.push(request.postDataJSON());
return route.fulfill({
json: {
id: url.pathname.split("/").at(-1),
name: "Updated Tenant",
slug: "updated-tenant",
status: "inactive",
type: "ORGANIZATION",
config: { visibility: "public" },
},
headers: { "Access-Control-Allow-Origin": "*" },
});
}
if (request.method() !== "GET") {
return route.continue();
}
return route.fulfill({
json: {
items: [
{
id: "tenant-a",
name: "Tenant A",
slug: "tenant-a",
status: "active",
type: "COMPANY",
config: { visibility: "internal" },
updatedAt: new Date().toISOString(),
},
{
id: "tenant-b",
name: "Tenant B",
slug: "tenant-b",
status: "active",
type: "COMPANY",
config: { visibility: "internal" },
updatedAt: new Date().toISOString(),
},
],
total: 2,
limit: 500,
offset: 0,
},
headers: { "Access-Control-Allow-Origin": "*" },
});
});
await page.goto("/tenants");
for (const tenantId of ["tenant-a", "tenant-b"]) {
await page
.getByTestId(`tenant-internal-id-${tenantId}`)
.locator("xpath=ancestor::tr")
.getByRole("checkbox")
.click();
}
await page.getByTestId("tenant-bulk-status-select").click();
await page.getByRole("option", { name: /비활성|inactive/i }).click();
await page.getByTestId("tenant-bulk-type-select").click();
await page.getByRole("option", { name: /Organization|정규 조직/i }).click();
await page.getByTestId("tenant-bulk-visibility-select").click();
await page.getByRole("option", { name: "공개", exact: true }).click();
await page.getByTestId("tenant-bulk-apply-btn").click();
await expect.poll(() => updatePayloads).toHaveLength(2);
for (const payload of updatePayloads) {
expect(payload).toMatchObject({
status: "inactive",
type: "ORGANIZATION",
config: { visibility: "public" },
});
}
});
test("switches tree and flat views, searches UUID, and selects descendants", async ({
page,
}) => {

View File

@@ -754,7 +754,7 @@ test.describe("User Management", () => {
expect(exportUrl).toContain("includeIds=false");
});
test("should show contact info in one row, hide roles, and change user status", async ({
test("should hide role controls from the users table and change user status", async ({
page,
}) => {
let updatePayload: Record<string, unknown> | undefined;
@@ -781,13 +781,311 @@ test.describe("User Management", () => {
const table = page.locator("table");
await expect(
table.getByRole("columnheader", { name: /ROLE|역할/i }),
).toBeVisible();
).toHaveCount(0);
await page.getByTestId("user-status-select-u-1").click();
await page.getByRole("option", { name: /입사대기|Preboarding/ }).click();
await expect
.poll(() => updatePayload)
.toMatchObject({ status: "preboarding" });
await table.locator('input[name="user-list-select-u-1"]').check();
await expect(page.getByTestId("bulk-permission-select")).toHaveCount(0);
});
test("should keep system role assignment out of the permissions screen", async ({
page,
}) => {
let bulkPayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/system\/relations$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
return route.fulfill({
json: {
items: [
{
userId: "u-1",
name: "John Doe",
email: "john@test.com",
relations: ["overview_viewers"],
},
],
},
});
});
await page.route(/\/admin\/users\/bulk$/, async (route) => {
if (route.request().method() !== "PUT") {
return route.fallback();
}
bulkPayload = route.request().postDataJSON();
return route.fulfill({
json: { results: [{ userId: "u-1", success: true }] },
});
});
await page.goto("/permissions-direct");
await expect(
page.getByTestId("permission-assignment-row-u-1-overview_viewers"),
).toBeVisible();
await expect(
page.getByTestId("permissions-direct-super-admin-select"),
).toHaveCount(0);
expect(bulkPayload).toBeUndefined();
});
test("should support bulk page and target action grants while keeping permissions direct protected", async ({
page,
}) => {
const relationWrites: Array<Record<string, unknown>> = [];
const relationDeletes: Array<Record<string, unknown>> = [];
await page.route(/\/admin\/system\/relations$/, async (route) => {
const method = route.request().method();
if (method === "GET") {
return route.fulfill({
json: {
items: [
{
userId: "u-1",
name: "John Doe",
email: "john@test.com",
relations: ["overview_viewers"],
},
{
userId: "u-2",
name: "Jane Manager",
email: "jane@test.com",
relations: [],
},
],
},
});
}
if (method === "POST") {
relationWrites.push(route.request().postDataJSON());
return route.fulfill({ json: { success: true } });
}
if (method === "DELETE") {
relationDeletes.push(route.request().postDataJSON());
return route.fulfill({ json: { success: true } });
}
return route.fallback();
});
await page.route(/\/admin\/tenants\/t-1\/relations$/, async (route) => {
const method = route.request().method();
if (method === "GET") {
return route.fulfill({
json: {
items: [
{
userId: "u-1",
name: "John Doe",
email: "john@test.com",
relations: ["profile_viewers"],
},
],
},
});
}
if (method === "POST") {
relationWrites.push(route.request().postDataJSON());
return route.fulfill({ json: { success: true } });
}
if (method === "DELETE") {
relationDeletes.push(route.request().postDataJSON());
return route.fulfill({ json: { success: true } });
}
return route.fallback();
});
await page.goto("/permissions-direct");
await expect(page.getByRole("tab", { name: /상세 권한/ })).toBeVisible();
await expect(
page.getByRole("option", { name: /권한 부여.*수정/ }),
).toHaveCount(0);
await expect(page.getByTestId("permission-target-org-picker-frame")).toBeVisible();
await expect(page.getByTestId("permission-target-org-picker-frame")).toHaveAttribute(
"src",
/rootTenantId%3Dall|rootTenantId=all/,
);
const pickerBox = await page
.getByTestId("permission-target-org-picker-frame")
.boundingBox();
const queueBox = await page.getByTestId("permission-target-queue").boundingBox();
expect(pickerBox?.x ?? Number.POSITIVE_INFINITY).toBeLessThan(
queueBox?.x ?? Number.NEGATIVE_INFINITY,
);
await page.getByTestId("bulk-relation-mode").selectOption("target-action");
await expect(
page.getByTestId("bulk-relation-operation"),
).toHaveCount(0);
await page.getByTestId("permission-action-tenant-picker-open").click();
await page.getByTestId("permission-action-tenant-search").fill("Test");
await page.getByTestId("permission-action-tenant-result-t-1").click();
await expect(page.getByTestId("bulk-relation-target-tenant")).toHaveValue(
"t-1",
);
await expect(
page.getByTestId("permission-target-tenant-scope"),
).toHaveCount(0);
await expect(
page.getByTestId("permission-target-org-picker-frame"),
).not.toHaveAttribute("src", /tenantId%3Dt-1|tenantId=t-1/);
await page.evaluate(() => {
window.postMessage(
{
type: "orgfront:picker:confirm",
payload: {
selections: [
{
type: "user",
id: "u-2",
name: "Jane Manager",
email: "jane@test.com",
rootTenantName: "한맥가족",
leafTenantName: "기술기획",
},
{
type: "user",
id: "u-3",
name: "Org Picked User",
email: "picked@test.com",
rootTenantName: "Commercial",
leafTenantName: "디자인팀",
},
],
},
},
"*",
);
});
await expect(page.getByTestId("permission-target-queue")).toContainText(
"Jane Manager",
);
await expect(page.getByTestId("permission-target-queue")).toContainText(
"Org Picked User",
);
await expect(page.getByTestId("permission-target-queue")).toContainText(
"한맥가족 / 기술기획",
);
await page.getByTestId("bulk-relation-target").selectOption("profile");
await page.getByTestId("bulk-relation-action").selectOption("manage");
await page
.getByRole("button", { name: /선택 사용자에게 권한 부여/ })
.click();
await expect.poll(() => relationWrites).toContainEqual(
{ userId: "u-2", relation: "tenants_managers" },
);
await expect.poll(() => relationWrites).toContainEqual(
{ userId: "u-2", relation: "profile_managers" },
);
await expect.poll(() => relationWrites).toContainEqual(
{ userId: "u-3", relation: "profile_managers" },
);
await page.getByTestId("permission-assignment-search").fill("John");
await expect(page.getByTestId("permission-assignment-row-u-1-profile_viewers")).toBeVisible();
await expect(
page.getByTestId("permission-assignment-row-u-2-profile_managers"),
).toHaveCount(0);
await page.getByTestId("permission-assignment-search").fill("");
await page.getByTestId("permission-assignment-sort").selectOption("relation");
await page
.getByTestId("permission-assignment-level-u-1-profile_viewers")
.selectOption("write");
await expect.poll(() => relationWrites).toContainEqual({
userId: "u-1",
relation: "profile_managers",
});
await page
.getByTestId("permission-assignment-remove-u-1-profile_viewers")
.click();
await expect.poll(() => relationDeletes).toContainEqual({
userId: "u-1",
relation: "profile_viewers",
});
});
test("should grant super admin role from the last tab only for super admins", async ({
page,
}) => {
let bulkPayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/system\/relations$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
return route.fulfill({
json: {
items: [
{
userId: "u-1",
name: "John Doe",
email: "john@test.com",
relations: ["overview_viewers"],
},
],
},
});
});
await page.route(/\/admin\/users\/bulk$/, async (route) => {
if (route.request().method() !== "PUT") {
return route.fallback();
}
bulkPayload = route.request().postDataJSON();
return route.fulfill({
json: { results: [{ userId: "u-1", success: true }] },
});
});
await page.goto("/permissions-direct");
const tabs = page.getByRole("tab");
await expect(tabs.last()).toHaveText(/Super Admin 역할/);
await tabs.last().click();
await page.getByTestId("super-admin-role-user-u-1").check();
await page.getByRole("button", { name: /Super Admin 부여/ }).click();
await expect.poll(() => bulkPayload).toEqual({
userIds: ["u-1"],
role: "super_admin",
});
});
test("should hide the super admin role tab from non super admins", async ({
page,
}) => {
await page.route(/\/user\/me$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
return route.fulfill({
json: {
id: "operator-user",
name: "Operator",
email: "operator@test.com",
role: "user",
manageableTenants: [],
},
});
});
await page.goto("/permissions-direct");
await expect(
page.getByRole("tab", { name: /Super Admin 역할/ }),
).toHaveCount(0);
await expect(
page.getByText(/이 작업을 수행할 권한이 없습니다/),
).toBeVisible();
});
test("should center users table loading state and use compact headers", async ({
@@ -1222,8 +1520,17 @@ test.describe("User Management", () => {
await page.goto("/users/u-1");
await expect(
page.getByRole("tab", { name: /한맥가족 구성원/i }),
page.getByRole("tab", { name: /^한맥가족$/i }),
).toHaveAttribute("data-state", "active");
await expect(
page.getByRole("tab", { name: /외부 기업 회원/i }),
).toHaveCount(0);
await expect(
page.getByRole("tab", { name: /^Commercial$/i }),
).toBeVisible();
await expect(page.getByRole("tab", { name: /^공공기관$/i })).toBeVisible();
await expect(page.getByRole("tab", { name: /^교육기관$/i })).toBeVisible();
await expect(page.getByRole("tab", { name: /^개인$/i })).toBeVisible();
await expect(page.getByLabel(/한맥 가족 구성원으로 등록/i)).toHaveCount(0);
await expect(page.getByTestId("detail-appointment-row-0")).toBeVisible();
await expect(