1
0
forked from baron/baron-sso
Files
baron-sso/adminfront/tests/users.spec.ts

1025 lines
32 KiB
TypeScript

import { expect, test } from "@playwright/test";
test.describe("User Management", () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const authority = "http://localhost:5000/oidc";
const client_id = "adminfront";
const key = `oidc.user:${authority}:${client_id}`;
const authData = {
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,
};
window.localStorage.setItem(key, JSON.stringify(authData));
window.localStorage.setItem("admin_session", "fake-token");
window.localStorage.setItem("locale", "ko");
window.localStorage.setItem("oidc.state", "dummy");
(
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
)._IS_TEST_MODE = true;
});
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" } });
});
await page.route(/.*\/api\/v1\/.*/, async (route) => {
const url = route.request().url();
const method = route.request().method();
if (url.includes("/user/me")) {
return route.fulfill({
json: {
id: "admin-user",
name: "Admin",
email: "admin@test.com",
role: "super_admin",
manageableTenants: [],
},
});
}
if (url.match(/\/admin\/tenants(\?.*)?$/) && method === "GET") {
return route.fulfill({
json: {
items: [
{
id: "t-1",
slug: "test-tenant",
name: "Test Tenant",
config: {
userSchema: [
{
key: "loginId",
label: "Login ID",
type: "text",
isLoginId: true,
},
],
},
},
{
id: "03dbe16b-e47b-4f72-927b-782807d67a35",
slug: "tech-planning",
name: "기술기획",
type: "USER_GROUP",
parentId: "hanmac-company-id",
config: {},
},
{
id: "hanmac-family-id",
slug: "hanmac-family",
name: "한맥가족",
type: "COMPANY_GROUP",
config: {},
},
{
id: "hanmac-company-id",
slug: "hanmac-company",
name: "한맥기술",
type: "COMPANY",
parentId: "hanmac-family-id",
config: {},
},
{
id: "hanmac-team-id",
slug: "hanmac-team",
name: "한맥팀",
type: "USER_GROUP",
parentId: "hanmac-company-id",
config: {},
},
{
id: "system-id",
slug: "system",
name: "System Tenant",
type: "SYSTEM",
config: {},
},
{
id: "external-tenant-id",
slug: "external-tenant",
name: "External Tenant",
type: "COMPANY",
config: {},
},
],
total: 7,
limit: 100,
offset: 0,
},
});
}
if (url.match(/\/admin\/tenants$/) && method === "POST") {
const postData = route.request().postDataJSON();
return route.fulfill({
status: 201,
json: {
id: "personal-tenant-id",
slug: postData?.slug || "personal",
name: postData?.name || "Personal",
type: postData?.type || "PERSONAL",
config: {},
},
});
}
if (url.match(/\/admin\/tenants\/t-1$/) && method === "GET") {
return route.fulfill({
json: {
id: "t-1",
slug: "test-tenant",
name: "Test Tenant",
config: {
userSchema: [
{
key: "loginId",
label: "Login ID",
type: "text",
isLoginId: true,
},
],
},
},
});
}
if (
url.match(/\/admin\/tenants\/03dbe16b-e47b-4f72-927b-782807d67a35$/) &&
method === "GET"
) {
return route.fulfill({
json: {
id: "03dbe16b-e47b-4f72-927b-782807d67a35",
slug: "tech-planning",
name: "기술기획",
type: "USER_GROUP",
config: {},
},
});
}
if (url.match(/\/admin\/users\/u-1$/) && method === "GET") {
return route.fulfill({
json: {
id: "u-1",
name: "John Doe",
email: "john@test.com",
loginId: "johndoe",
tenantSlug: "test-tenant",
tenant: { id: "t-1", name: "Test Tenant", slug: "test-tenant" },
role: "user",
status: "active",
metadata: { "t-1": { loginId: "johndoe" } },
},
});
}
if (url.includes("/password/policy")) {
return route.fulfill({
json: {
minLength: 12,
lowercase: true,
uppercase: true,
number: true,
nonAlphanumeric: true,
},
});
}
if (url.includes("/rp-history")) {
return route.fulfill({
json: [],
});
}
if (url.match(/\/admin\/users(\?.*)?$/) && method === "POST") {
// Parse request payload to simulate validation checks
const postData = route.request().postDataJSON();
if (postData && postData.metadata?.loginId === "existing_user") {
// Simulate a backend conflict error (409) for an existing loginId
return route.fulfill({
status: 409,
json: {
error: "이미 존재하는 로그인 ID 입니다.",
},
});
}
// Mock successful user creation
return route.fulfill({
json: {
id: "new-user-id",
name: "New User",
email: "newuser@test.com",
loginId: postData?.metadata?.loginId || "newuser123",
},
});
}
if (url.match(/\/admin\/users\/u-1$/) && method === "PUT") {
const postData = route.request().postData();
console.log("PUT /admin/users/u-1 payload:", postData);
// Force 409 error for this specific conflict string
if (postData?.includes("johndoe_conflict")) {
return route.fulfill({
status: 409,
json: {
error: "이미 존재하는 로그인 ID 입니다.",
},
});
}
// Mock successful user update
return route.fulfill({
json: {
id: "u-1",
name: "John Doe Updated",
email: "john@test.com",
loginId: "johndoe_updated",
status: "inactive",
},
});
}
if (url.match(/\/admin\/users(\?.*)?$/) && method === "GET") {
return route.fulfill({
json: {
items: [
{
id: "u-1",
name: "John Doe",
email: "john@test.com",
phone: "010-1111-2222",
loginId: "johndoe",
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
},
],
total: 1,
limit: 50,
offset: 0,
},
});
}
return route.fulfill({ json: { items: [], total: 0 } });
});
});
test("should successfully edit a user's Login ID", async ({ page }) => {
await page.goto("/users/u-1");
// "테넌트 프로필" 탭 클릭
await page
.getByRole("tab", { name: /테넌트 프로필|Tenants|Tenant Profile/i })
.click();
// Wait for the form to load with the existing login ID
const loginIdInput = page.locator(
'input[name*="metadata"][name*="loginId"]',
);
await expect(loginIdInput).toBeVisible();
await expect(loginIdInput).toHaveValue("johndoe");
// Change the Login ID
await loginIdInput.fill("johndoe_updated");
// Submit the form using Enter key
await loginIdInput.press("Enter");
// Check for success message
await expect(page.getByText(/저장/i).first()).toBeVisible();
});
test("should show conflict error when updating to an existing Login ID", async ({
page,
}) => {
// Intercept ANY PUT request to this user and return 409
await page.route(/\/admin\/users\/u-1/, async (route) => {
if (route.request().method() === "PUT") {
return route.fulfill({
status: 409,
contentType: "application/json",
body: JSON.stringify({ error: "이미 존재하는 로그인 ID 입니다." }),
});
}
return route.fallback();
});
await page.goto("/users/u-1");
// "테넌트 프로필" 탭 클릭
await page
.getByRole("tab", { name: /테넌트 프로필|Tenants|Tenant Profile/i })
.click();
const loginIdInput = page.locator(
'input[name*="metadata"][name*="loginId"]',
);
await expect(loginIdInput).toBeVisible();
await expect(loginIdInput).toHaveValue("johndoe");
// Use a value similar to the successful edit test
await loginIdInput.fill("johndoe_conflict");
// Submit the form using Enter key
await loginIdInput.press("Enter");
// Check for the specific error
await expect(
page.getByText(/이미 존재하는 로그인 ID 입니다/i).first(),
).toBeVisible();
});
test("should successfully create a new user with a Login ID", async ({
page,
}) => {
await page.goto("/users/new");
// Ensure the page title is loaded
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
const categoryTabs = page.getByRole("tab");
await expect(categoryTabs).toHaveText([
"한맥가족 구성원",
"외부 기업 회원",
"개인 회원",
]);
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.getByRole("tab", { name: /한맥가족 구성원/i }),
).toHaveAttribute("data-state", "active");
await expect(page.getByLabel(/한맥 가족 구성원으로 등록/i)).toHaveCount(0);
// Select Tenant first (important for schema fields to show up)
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
await page.selectOption("select#tenantSlug", "test-tenant");
// Fill required fields
await page.locator('input[name="name"]').fill("New User");
await page.locator('input[name="email"]').fill("newuser@test.com");
// Fill Login ID
const loginIdInput = page.locator('input[id*="metadata"][id*="loginId"]');
await loginIdInput.fill("newuser123");
// Submit the form
const createButton = page.getByRole("button", { name: /생성/i });
await createButton.click();
// Assuming successful creation redirects back to the user list
await expect(page).toHaveURL(/.*\/users$/, { timeout: 10000 });
});
test("should export users through the authenticated API client", async ({
page,
}) => {
let authorizationHeader: string | undefined;
let exportUrl = "";
await page.route(/\/admin\/users\/export(\?.*)?$/, async (route) => {
authorizationHeader = route.request().headers().authorization;
exportUrl = route.request().url();
return route.fulfill({
status: 200,
headers: {
"content-type": "text/csv; charset=utf-8",
"content-disposition": 'attachment; filename="users.csv"',
},
body: "email,name\njohn@test.com,John Doe\n",
});
});
await page.goto("/users");
const [download] = await Promise.all([
page.waitForEvent("download"),
(async () => {
await page.getByTestId("user-data-mgmt-btn").click();
await page.getByTestId("user-export-menu-item").click();
})(),
]);
expect(download.suggestedFilename()).toBe("users.csv");
expect(authorizationHeader).toBe("Bearer fake-token");
expect(exportUrl).toContain("includeIds=false");
});
test("should show contact info in one row, hide roles, and change user status", async ({
page,
}) => {
let updatePayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/users\/u-1$/, async (route) => {
if (route.request().method() === "PUT") {
updatePayload = route.request().postDataJSON();
return route.fulfill({
json: {
id: "u-1",
name: "John Doe",
email: "john@test.com",
phone: "010-1111-2222",
loginId: "johndoe",
status: "preboarding",
createdAt: "2026-04-01T00:00:00Z",
},
});
}
return route.fallback();
});
await page.goto("/users");
const table = page.locator("table");
await expect(
table.getByRole("columnheader", { name: /ROLE|역할/i }),
).toBeVisible();
await page.getByTestId("user-status-select-u-1").click();
await page.getByRole("option", { name: /입사대기|Preboarding/ }).click();
await expect
.poll(() => updatePayload)
.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(500);
});
test("should expose internal user uuid in the users table", async ({
page,
}) => {
const internalUserId = "4d20c735-05d0-42d4-9479-0e9be74fd987";
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() !== "GET") {
return route.fallback();
}
return route.fulfill({
json: {
items: [
{
id: internalUserId,
name: "UUID User",
email: "uuid-user@test.com",
phone: "010-2222-3333",
loginId: "uuid_login_id",
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
},
],
total: 1,
limit: 50,
offset: 0,
},
});
});
await page.goto("/users");
await expect(page.locator("table")).toContainText(internalUserId);
});
test("should require a tenant appointment before creating a Hanmac family user", async ({
page,
}) => {
let createPayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() === "POST") {
createPayload = route.request().postDataJSON();
return route.fulfill({
status: 201,
json: {
id: "new-user-id",
name: "Family User",
email: "family@test.com",
},
});
}
return route.fallback();
});
await page.goto("/users/new");
await expect(
page.getByRole("tab", { name: /한맥가족 구성원/i }),
).toHaveAttribute("data-state", "active");
await expect(page.getByLabel(/한맥 가족 구성원으로 등록/i)).toHaveCount(0);
await expect(page.locator("select#role")).toBeVisible();
await expect(page.locator("input#department")).toHaveCount(0);
await expect(page.getByText(/대표 소속/i)).toHaveCount(0);
await page.getByTestId("add-appointment-btn").click();
await expect(page.getByTestId("appointment-row-0")).toBeVisible();
await expect(
page.getByTestId("appointment-tenant-owner-line-0"),
).toBeVisible();
await expect(page.getByTestId("appointment-position-line-0")).toBeVisible();
await page.getByRole("switch", { name: /대표 조직/i }).click();
await page.getByLabel(/^직무$/i).fill("플랫폼 운영");
await page.getByLabel(/^직급$/i).fill("책임");
await page.getByLabel(/^직책$/i).fill("팀장");
await page.locator('input[name="name"]').fill("Family User");
await page.locator('input[name="email"]').fill("family@test.com");
await page.getByRole("button", { name: /생성/i }).click();
await expect(
page.getByText(
/한맥 가족 구성원은 소속 테넌트를 하나 이상 선택해 주세요\./,
),
).toBeVisible();
expect(createPayload).toBeUndefined();
});
test("should hide Hanmac family subtree and system tenants when creating a non-family user", async ({
page,
}) => {
await page.goto("/users/new");
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
const tenantOptionValues = await page
.locator("select#tenantSlug option")
.evaluateAll((options) =>
options.map((option) => (option as HTMLOptionElement).value),
);
expect(tenantOptionValues).toContain("test-tenant");
expect(tenantOptionValues).toContain("external-tenant");
expect(tenantOptionValues).not.toContain("");
expect(tenantOptionValues).not.toContain("system");
expect(tenantOptionValues).not.toContain("hanmac-family");
expect(tenantOptionValues).not.toContain("hanmac-company");
expect(tenantOptionValues).not.toContain("hanmac-team");
expect(tenantOptionValues).not.toContain("tech-planning");
});
test("should create a personal user with the fixed global Personal tenant when it is not listed", async ({
page,
}) => {
let tenantPayload: Record<string, unknown> | undefined;
let createPayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/tenants$/, async (route) => {
if (route.request().method() === "POST") {
tenantPayload = route.request().postDataJSON();
return route.fulfill({
status: 201,
json: {
id: "personal-tenant-id",
slug: "personal",
name: "Personal",
type: "PERSONAL",
config: {},
},
});
}
return route.fallback();
});
await page.route(/\/admin\/users(\?.*)?$/, async (route) => {
if (route.request().method() === "POST") {
createPayload = route.request().postDataJSON();
return route.fulfill({
status: 201,
json: {
id: "personal-user-id",
name: "Personal User",
email: "personal@test.com",
},
});
}
return route.fallback();
});
await page.goto("/users/new");
await page.getByRole("tab", { name: /개인 회원/i }).click();
await expect(page.getByTestId("personal-tenant-summary")).toContainText(
/Personal/i,
);
await page.locator('input[name="name"]').fill("Personal User");
await page.locator('input[name="email"]').fill("personal@test.com");
await page.getByRole("button", { name: /생성/i }).click();
expect(tenantPayload).toBeUndefined();
await expect
.poll(() => createPayload)
.toMatchObject({
tenantSlug: "personal",
metadata: {
personalTenantId: "9607eb7b-04d2-42ab-80fe-780fe21c7e8f",
},
});
});
test("should show Hanmac family appointments layout on user detail", async ({
page,
}) => {
await page.route(/\/admin\/users\/u-1$/, async (route) => {
if (route.request().method() === "GET") {
return route.fulfill({
json: {
id: "u-1",
name: "Family User",
email: "family@test.com",
phone: "010-1111-2222",
loginId: "familyuser",
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
updatedAt: "2026-04-01T00:00:00Z",
metadata: {
additionalAppointments: [
{
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
tenantSlug: "tech-planning",
tenantName: "기술기획",
isPrimary: true,
isOwner: true,
grade: "책임",
jobTitle: "플랫폼 운영",
position: "팀장",
},
],
},
},
});
}
return route.fallback();
});
await page.goto("/users/u-1");
await expect(
page.getByRole("tab", { name: /한맥가족 구성원/i }),
).toHaveAttribute("data-state", "active");
await expect(page.getByLabel(/한맥 가족 구성원으로 등록/i)).toHaveCount(0);
await expect(page.getByTestId("detail-appointment-row-0")).toBeVisible();
await expect(
page.getByTestId("detail-appointment-tenant-owner-line-0"),
).toContainText(/기술기획|대표 조직|조직장/);
await expect(
page.getByTestId("detail-appointment-row-0").getByRole("switch", {
name: /대표 조직/i,
}),
).toBeChecked();
await expect(
page.getByTestId("detail-appointment-row-0").getByRole("switch", {
name: /대표 조직/i,
}),
).toBeDisabled();
await expect(
page.getByTestId("detail-appointment-position-line-0"),
).toBeVisible();
});
test("should save selected Hanmac representative appointment from user detail", async ({
page,
}) => {
let updatePayload: Record<string, unknown> | undefined;
await page.route(/\/admin\/users\/u-1$/, async (route) => {
if (route.request().method() === "GET") {
return route.fulfill({
json: {
id: "u-1",
name: "Family User",
email: "family@test.com",
phone: "010-1111-2222",
loginId: "familyuser",
role: "user",
status: "active",
createdAt: "2026-04-01T00:00:00Z",
updatedAt: "2026-04-01T00:00:00Z",
metadata: {
additionalAppointments: [
{
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
tenantSlug: "tech-planning",
tenantName: "기술기획",
isOwner: true,
grade: "책임",
jobTitle: "플랫폼 운영",
position: "팀장",
},
{
tenantId: "hanmac-team-id",
tenantSlug: "hanmac-team",
tenantName: "한맥팀",
isOwner: false,
grade: "선임",
jobTitle: "개발",
position: "파트장",
},
],
},
},
});
}
if (route.request().method() === "PUT") {
updatePayload = route.request().postDataJSON();
return route.fulfill({
json: {
id: "u-1",
name: "Family User",
email: "family@test.com",
status: "active",
},
});
}
return route.fallback();
});
await page.goto("/users/u-1");
await page
.getByTestId("detail-appointment-row-1")
.getByRole("switch", { name: /대표 조직/i })
.click();
await expect(
page.getByTestId("detail-appointment-row-0").getByRole("switch", {
name: /대표 조직/i,
}),
).not.toBeChecked();
await expect(
page.getByTestId("detail-appointment-row-1").getByRole("switch", {
name: /대표 조직/i,
}),
).toBeChecked();
await page.locator("form").evaluate((form) => {
(form as HTMLFormElement).requestSubmit();
});
await expect
.poll(() => updatePayload)
.toMatchObject({
tenantSlug: "hanmac-team",
primaryTenantId: "hanmac-team-id",
primaryTenantName: "한맥팀",
metadata: {
primaryTenantId: "hanmac-team-id",
primaryTenantName: "한맥팀",
primaryTenantSlug: "hanmac-team",
additionalAppointments: [
{
tenantId: "03dbe16b-e47b-4f72-927b-782807d67a35",
isPrimary: false,
},
{ tenantId: "hanmac-team-id", isPrimary: true },
],
},
});
expect(updatePayload?.primaryTenantIsOwner).toBeUndefined();
expect(
(updatePayload?.metadata as Record<string, unknown>)
?.primaryTenantIsOwner,
).toBeUndefined();
const appointments = (updatePayload?.metadata as Record<string, unknown>)
?.additionalAppointments as Array<Record<string, unknown>>;
expect(appointments[1].isOwner).toBeUndefined();
});
test("should show conflict error when creating with an existing Login ID", async ({
page,
}) => {
await page.goto("/users/new");
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
await page.getByRole("tab", { name: /외부 기업 회원/i }).click();
// Select Tenant first (important for schema fields to show up)
await page.selectOption("select#tenantSlug", "test-tenant");
// Fill required fields
await page.locator('input[name="name"]').fill("New User");
await page.locator('input[name="email"]').fill("newuser@test.com");
// Fill Login ID that triggers the mock conflict error
const loginIdInput = page.locator('input[id*="metadata"][id*="loginId"]');
await loginIdInput.fill("existing_user");
// Submit the form
const createButton = page.getByRole("button", { name: /생성/i });
await createButton.click();
// Check for the specific conflict error message from the backend mock
await expect(
page.getByText(/이미 존재하는 로그인 ID 입니다/i),
).toBeVisible();
});
});