import { expect, test } from "@playwright/test"; test.describe("Tenants Management", () => { test.beforeEach(async ({ page }) => { // Authenticate await page.addInitScript(() => { const authority = "http://localhost:5000/oidc"; const client_id = "adminfront"; const key = `oidc.user:${authority}:${client_id}`; const authData = { access_token: "fake-token", token_type: "Bearer", profile: { sub: "admin-user", name: "Admin User", email: "admin@example.com", }, expires_at: Math.floor(Date.now() / 1000) + 3600, }; window.localStorage.setItem(key, JSON.stringify(authData)); }); // Mock OIDC config to avoid redirects await page.route( "**/oidc/.well-known/openid-configuration", async (route) => { await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } }); }, ); // Mock user profile await page.route("**/api/v1/user/me", async (route) => { await route.fulfill({ json: { id: "admin-user", name: "Admin User", email: "admin@example.com", role: "super_admin", }, }); }); // Default mock for tenants to avoid proxy leaks await page.route("**/api/v1/admin/tenants**", async (route) => { if (route.request().method() === "GET") { await route.fulfill({ json: { items: [], total: 0, limit: 100, offset: 0 }, }); } else { await route.continue(); } }); }); test("should list tenants", async ({ page }) => { await page.route("**/api/v1/admin/tenants**", async (route) => { await route.fulfill({ json: { items: [ { id: "1", name: "Tenant A", slug: "tenant-a", status: "active", type: "COMPANY", updatedAt: new Date().toISOString(), }, ], total: 1, limit: 1000, offset: 0, }, }); }); await page.goto("/tenants"); await expect(page.locator("h2")).toContainText("테넌트 목록"); await expect(page.locator("table")).toContainText("Tenant A"); }); test("should create a new tenant", async ({ page }) => { // Mock GET for list (empty) and for parents await page.route("**/api/v1/admin/tenants**", async (route) => { if (route.request().method() === "GET") { await route.fulfill({ json: { items: [], total: 0, limit: 100, offset: 0 }, }); } else if (route.request().method() === "POST") { await route.fulfill({ json: { id: "2", name: "New Tenant", slug: "new-tenant", status: "active", type: "COMPANY", }, }); } }); await page.goto("/tenants/new"); await page.fill("input >> nth=0", "New Tenant"); await page.fill("input >> nth=1", "new-tenant"); await page.fill("textarea", "Description"); await page.click('button:has-text("생성")'); await expect(page).toHaveURL(/\/tenants$/); }); test("should show validation error on empty name", async ({ page }) => { await page.goto("/tenants/new"); const submitBtn = page.locator('button:has-text("생성")'); await expect(submitBtn).toBeDisabled(); await page.fill("input >> nth=0", "Valid Name"); await expect(submitBtn).not.toBeDisabled(); }); test("should show organization hierarchy and member list distinction", async ({ page, }) => { // Mock parent tenant and its children const mockTenants = [ { id: "parent-1", name: "Parent Org", slug: "parent-slug", status: "active", type: "COMPANY", memberCount: 5, parentId: null, }, { id: "child-1", name: "Child Team", slug: "child-slug", status: "active", type: "USER_GROUP", memberCount: 3, parentId: "parent-1", }, ]; await page.route("**/api/v1/admin/tenants**", async (route) => { await route.fulfill({ json: { items: mockTenants, total: 2, limit: 1000, offset: 0, }, }); }); // Mock members for parent and child await page.route( "**/api/v1/admin/users?*companyCode=parent-slug*", async (route) => { await route.fulfill({ json: { items: [{ id: "u1", name: "User One", email: "u1@parent.com" }], total: 1, }, }); }, ); await page.route( "**/api/v1/admin/users?*companyCode=child-slug*", async (route) => { await route.fulfill({ json: { items: [{ id: "u2", name: "User Two", email: "u2@child.com" }], total: 1, }, }); }, ); await page.goto("/tenants/parent-1/organization"); // Wait for the table to appear await expect(page.locator("table")).toBeVisible(); // Check if hierarchy shows correctly await expect(page.locator("table")).toContainText("Parent Org"); await expect(page.locator("table")).toContainText("Child Team"); // Check if member counts (Direct/Total) are displayed // Parent should have Direct 5, Total 8 const parentRow = page.locator("tr", { hasText: "Parent Org" }); await expect(parentRow).toContainText("5"); // Direct await expect(parentRow).toContainText("8"); // Total (5 + 3) // Check for either English or Korean labels const hasDirectLabel = await parentRow.evaluate( (el) => el.textContent?.includes("Direct") || el.textContent?.includes("소속"), ); const hasTotalLabel = await parentRow.evaluate( (el) => el.textContent?.includes("Total") || el.textContent?.includes("전체"), ); expect(hasDirectLabel).toBe(true); expect(hasTotalLabel).toBe(true); // Open Member List Dialog - Click the members count button const memberButton = parentRow .getByRole("button") .filter({ hasText: /Direct|소속/ }); await memberButton.click(); // Check Tabs in Member List Dialog // Use regex to match either language, ignoring the count suffix await expect( page .locator('button[role="tab"]') .filter({ hasText: /소속 멤버|Direct Members/ }), ).toBeVisible(); await expect( page .locator('button[role="tab"]') .filter({ hasText: /하위 조직 멤버|Descendant Members/ }), ).toBeVisible(); // Direct Members Tab should show parent's user await expect(page.locator("role=dialog")).toContainText("u1@parent.com"); // Switch to Descendant Members Tab await page.click( 'button[role="tab"]:has-text("하위 조직 멤버"), button[role="tab"]:has-text("Descendant Members")', ); await expect(page.locator("role=dialog")).toContainText("u2@child.com"); }); });