import { expect, test } from "@playwright/test"; test.describe("Worksmobile tenant management", () => { test.beforeEach(async ({ page }) => { await page.addInitScript(() => { window.localStorage.setItem("locale", "ko"); window.localStorage.setItem("admin_session", "fake-token"); window.localStorage.setItem("RoleSwitcher-Collapsed", "true"); ( 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}`; const authData = { access_token: "fake-token", token_type: "Bearer", profile: { sub: "admin-user", name: "Admin", role: "super_admin" }, expires_at: Math.floor(Date.now() / 1000) + 36000, }; window.localStorage.setItem(key, JSON.stringify(authData)); }); await page.route("**/oidc/**", async (route) => { await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } }); }); }); test("opens Worksmobile in the current tab and filters comparison rows", async ({ page, }) => { const comparisonRequests: boolean[] = []; const syncRequests: string[] = []; await page.route("**/api/v1/**", async (route) => { const url = new URL(route.request().url()); const method = route.request().method(); 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/hanmac-family-id") && method === "GET" ) { return route.fulfill({ json: { id: "hanmac-family-id", name: "한맥 가족", slug: "hanmac-family", type: "COMPANY_GROUP", status: "active", parentId: null, }, headers, }); } if ( url.pathname.endsWith("/admin/tenants/hanmac-family-id/worksmobile") && method === "GET" ) { return route.fulfill({ json: { tenant: { id: "hanmac-family-id", name: "한맥 가족", slug: "hanmac-family", type: "COMPANY_GROUP", status: "active", memberCount: 0, createdAt: "2026-05-04T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z", }, config: { enabled: true, tokenConfigured: true, }, recentJobs: [], }, headers, }); } if ( url.pathname.endsWith( "/admin/tenants/hanmac-family-id/worksmobile/comparison", ) && method === "GET" ) { const includeMatched = url.searchParams.get("includeMatched") === "true"; comparisonRequests.push(includeMatched); return route.fulfill({ json: { users: includeMatched ? [ { resourceType: "USER", baronId: "user-matched", baronName: "홍길동", baronPrimaryOrgId: "team-tech", baronPrimaryOrgName: "기술기획", worksmobileId: "works-user-matched", externalKey: "user-matched", worksmobileName: "홍길동", status: "matched", worksmobilePrimaryOrgId: "works-team-tech", worksmobilePrimaryOrgName: "WORKS 기술기획", }, { resourceType: "USER", baronId: "user-missing", baronName: "김누락", status: "missing_in_worksmobile", }, { resourceType: "USER", worksmobileId: "works-user-only", externalKey: "works-user-only", worksmobileName: "박웍스", status: "missing_in_baron", }, ] : [ { resourceType: "USER", baronId: "user-missing", baronName: "김누락", status: "missing_in_worksmobile", }, { resourceType: "USER", worksmobileId: "works-user-only", externalKey: "works-user-only", worksmobileName: "박웍스", status: "missing_in_baron", }, ], groups: [ { resourceType: "GROUP", baronId: "group-missing", baronName: "Baron 전용 조직", baronParentId: "parent-tech", baronParentName: "기술본부", status: "missing_in_worksmobile", }, { resourceType: "GROUP", worksmobileId: "works-group-only", externalKey: "works-group-only", worksmobileName: "WORKS 전용 조직", worksmobileParentId: "works-parent-tech", worksmobileParentName: "WORKS 기술본부", status: "missing_in_baron", }, ], }, headers, }); } if ( url.pathname.endsWith( "/admin/tenants/hanmac-family-id/worksmobile/users/user-missing/sync", ) && method === "POST" ) { syncRequests.push("user-missing"); return route.fulfill({ json: { id: "job-user-missing", resourceId: "user-missing" }, headers, }); } return route.fulfill({ json: { items: [], total: 0 }, headers }); }); await page.goto("/tenants/hanmac-family-id"); await page.getByRole("link", { name: "Worksmobile" }).click(); await expect(page).toHaveURL(/\/tenants\/hanmac-family-id\/worksmobile$/); await expect(page.getByText("Baron / Works 비교")).toBeVisible(); await expect(page.getByText("domainMappings")).not.toBeVisible(); await expect(page.getByText("SCIM token")).not.toBeVisible(); await expect(page.getByText("김누락")).toBeVisible(); await expect(page.getByText("박웍스")).toBeVisible(); await expect(page.getByText("WORKS 전용 조직")).toBeVisible(); await expect(page.getByText("기술본부", { exact: true })).toBeVisible(); await expect(page.getByText("parent-tech", { exact: true })).toBeVisible(); await expect(page.getByText("WORKS 기술본부")).toBeVisible(); await expect(page.getByText("works-parent-tech")).toBeVisible(); await expect(page.getByText("홍길동")).not.toBeVisible(); expect(comparisonRequests[0]).toBe(true); const filterButtons = page .getByRole("button", { name: /바론에만 있음|웍스에만 있음|양쪽 다 있음/, }) .allTextContents(); await expect .poll(() => filterButtons) .toEqual(["바론에만 있음", "웍스에만 있음", "양쪽 다 있음"]); await page.getByRole("button", { name: "웍스에만 있음" }).click(); await expect(page.getByText("박웍스")).not.toBeVisible(); await expect(page.getByText("김누락")).toBeVisible(); await expect(page.getByText("홍길동")).not.toBeVisible(); await page.getByRole("button", { name: "양쪽 다 있음" }).click(); await expect(page.getByText("홍길동")).toHaveCount(2); await expect(page.getByText("기술기획", { exact: true })).toBeVisible(); await expect(page.getByText("team-tech", { exact: true })).toBeVisible(); await expect(page.getByText("WORKS 기술기획")).toBeVisible(); await expect(page.getByText("works-team-tech")).toBeVisible(); await expect(page.getByText("김누락")).toBeVisible(); await expect(page.getByText("박웍스")).not.toBeVisible(); await page.getByRole("button", { name: "바론에만 있음" }).click(); await expect(page.getByText("홍길동")).toHaveCount(2); await expect(page.getByText("김누락")).not.toBeVisible(); await expect(page.getByText("박웍스")).not.toBeVisible(); await page.getByRole("button", { name: "웍스에만 있음" }).click(); await expect(page.getByText("홍길동")).toHaveCount(2); await expect(page.getByText("김누락")).not.toBeVisible(); await expect(page.getByText("박웍스")).toBeVisible(); await page.getByRole("button", { name: "양쪽 다 있음" }).click(); await expect(page.getByText("김누락")).not.toBeVisible(); await expect(page.getByText("박웍스")).toBeVisible(); await expect(page.getByText("홍길동")).not.toBeVisible(); await page.getByRole("button", { name: "바론에만 있음" }).click(); await expect(page.getByText("김누락")).toBeVisible(); await expect(page.getByText("박웍스")).toBeVisible(); await expect(page.getByText("홍길동")).not.toBeVisible(); await page .getByRole("row", { name: /김누락/ }) .getByRole("checkbox") .check(); await page .getByRole("button", { name: "선택 구성원 WORKS에 생성" }) .click(); await expect.poll(() => syncRequests).toEqual(["user-missing"]); }); test("shows a toast when selected WORKS creation fails", async ({ page }) => { await page.route("**/api/v1/**", async (route) => { const url = new URL(route.request().url()); const method = route.request().method(); const headers = { "Access-Control-Allow-Origin": "*" }; if (url.pathname.endsWith("/user/me")) { return route.fulfill({ json: { id: "admin-user", name: "Admin", role: "super_admin" }, headers, }); } if ( url.pathname.endsWith("/admin/tenants/hanmac-family-id") && method === "GET" ) { return route.fulfill({ json: { id: "hanmac-family-id", name: "한맥 가족", slug: "hanmac-family", parentId: null, }, headers, }); } if ( url.pathname.endsWith("/admin/tenants/hanmac-family-id/worksmobile") && method === "GET" ) { return route.fulfill({ json: { tenant: { id: "hanmac-family-id", name: "한맥 가족", slug: "hanmac-family", parentId: null, }, config: {}, recentJobs: [], }, headers, }); } if ( url.pathname.endsWith( "/admin/tenants/hanmac-family-id/worksmobile/comparison", ) && method === "GET" ) { return route.fulfill({ json: { users: [ { resourceType: "USER", baronId: "user-fail", baronName: "실패 사용자", status: "missing_in_worksmobile", }, ], groups: [], }, headers, }); } if ( url.pathname.endsWith( "/admin/tenants/hanmac-family-id/worksmobile/users/user-fail/sync", ) && method === "POST" ) { return route.fulfill({ status: 500, json: { error: "WORKS API rejected user creation" }, headers, }); } return route.fulfill({ json: { items: [], total: 0 }, headers }); }); await page.goto("/tenants/hanmac-family-id/worksmobile"); await page .getByRole("row", { name: /실패 사용자/ }) .getByRole("checkbox") .check(); await page .getByRole("button", { name: "선택 구성원 WORKS에 생성" }) .click(); await expect(page.getByText("WORKS 생성 작업 등록 실패")).toBeVisible(); await expect( page.getByText(/WORKS API rejected user creation/), ).toBeVisible(); }); test("keeps wide comparison columns inside table scroll and blocks immutable WORKS accounts", async ({ page, }) => { await page.setViewportSize({ width: 900, height: 700 }); await page.route("**/api/v1/**", async (route) => { const url = new URL(route.request().url()); const method = route.request().method(); const headers = { "Access-Control-Allow-Origin": "*" }; if (url.pathname.endsWith("/user/me")) { return route.fulfill({ json: { id: "admin-user", name: "Admin", role: "super_admin" }, headers, }); } if ( url.pathname.endsWith("/admin/tenants/hanmac-family-id") && method === "GET" ) { return route.fulfill({ json: { id: "hanmac-family-id", name: "한맥 가족", slug: "hanmac-family", parentId: null, }, headers, }); } if ( url.pathname.endsWith("/admin/tenants/hanmac-family-id/worksmobile") && method === "GET" ) { return route.fulfill({ json: { tenant: { id: "hanmac-family-id", name: "한맥 가족", slug: "hanmac-family", parentId: null, }, config: { adminTenantId: "works-tenant-1", }, recentJobs: [], }, headers, }); } if ( url.pathname.endsWith( "/admin/tenants/hanmac-family-id/worksmobile/comparison", ) && method === "GET" ) { return route.fulfill({ json: { users: [ { resourceType: "USER", worksmobileId: "works-user-with-extra-long-identifier-for-scroll-check", externalKey: "external-key-with-extra-long-identifier", worksmobileName: "긴 WORKS 사용자", worksmobileEmail: "long-works-user-name-for-scroll@samaneng.com", worksmobileDomainId: 300285955, worksmobileDomainName: "samaneng.com", worksmobilePrimaryOrgId: "works-primary-org-with-extra-long-identifier", worksmobilePrimaryOrgName: "긴 WORKS 조직", status: "missing_in_baron", }, { resourceType: "USER", worksmobileId: "works-cyhan", worksmobileName: "변경 불가 계정", worksmobileEmail: "cyhan@samaneng.com", worksmobileDomainId: 300285955, worksmobileDomainName: "samaneng.com", status: "missing_in_baron", }, ], groups: [], }, headers, }); } return route.fulfill({ json: { items: [], total: 0 }, headers }); }); await page.goto("/tenants/hanmac-family-id/worksmobile"); await expect(page.getByText("긴 WORKS 사용자")).toBeVisible(); const userColumnButton = page .getByRole("heading", { name: "구성원" }) .locator("xpath=ancestor::div[contains(@class, 'space-y-2')][1]") .getByRole("button", { name: "컬럼 설정" }); await userColumnButton.click(); const dialog = page.getByRole("dialog", { name: "구성원 컬럼 설정" }); await dialog.getByLabel("Baron ID").check(); await dialog.getByLabel("WORKS ID").check(); await dialog.getByLabel("external_key").check(); await dialog.getByRole("button", { name: "닫기" }).click(); const pageOverflow = await page.evaluate(() => ({ documentScrollWidth: document.documentElement.scrollWidth, bodyScrollWidth: document.body.scrollWidth, viewportWidth: document.documentElement.clientWidth, })); expect( Math.max(pageOverflow.documentScrollWidth, pageOverflow.bodyScrollWidth), ).toBeLessThanOrEqual(pageOverflow.viewportWidth + 1); const userTableScroll = await page .locator("table") .first() .evaluate((table) => { const container = table.parentElement?.parentElement as HTMLElement; return { clientWidth: container.clientWidth, overflowX: window.getComputedStyle(container).overflowX, scrollWidth: container.scrollWidth, }; }); expect(userTableScroll.overflowX).toBe("auto"); expect(userTableScroll.scrollWidth).toBeGreaterThan( userTableScroll.clientWidth, ); const immutableRow = page.getByRole("row", { name: /cyhan@samaneng\.com/, }); await expect(immutableRow.getByRole("checkbox")).toBeDisabled(); await expect( immutableRow.getByRole("button", { name: /비밀번호 관리/ }), ).toBeDisabled(); }); });