첫 커밋: 로컬 프로젝트 업로드
This commit is contained in:
604
baron-sso/userfront-e2e/tests/auth-routing.spec.ts
Normal file
604
baron-sso/userfront-e2e/tests/auth-routing.spec.ts
Normal file
@@ -0,0 +1,604 @@
|
||||
import { expect, type Page, type Route, test } from "@playwright/test";
|
||||
|
||||
type MockOptions = {
|
||||
sessionStatus?: number;
|
||||
captureApprove?: (pendingRef: string | null) => void;
|
||||
captureUserMe?: () => void;
|
||||
captureVerify?: (path: string, body: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
async function seedTokenLogin(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("baron_auth_token", "e30.e30.e30");
|
||||
window.localStorage.setItem("baron_auth_provider", "ory");
|
||||
window.localStorage.removeItem("baron_auth_cookie_mode");
|
||||
window.localStorage.removeItem("baron_auth_pending_provider");
|
||||
});
|
||||
}
|
||||
|
||||
async function seedSessionTokenLogin(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
window.sessionStorage.setItem("baron_auth_token", "e30.e30.e30");
|
||||
window.sessionStorage.setItem("baron_auth_provider", "ory");
|
||||
window.sessionStorage.removeItem("baron_auth_cookie_mode");
|
||||
window.sessionStorage.removeItem("baron_auth_pending_provider");
|
||||
window.localStorage.removeItem("baron_auth_token");
|
||||
window.localStorage.removeItem("baron_auth_provider");
|
||||
window.localStorage.removeItem("baron_auth_cookie_mode");
|
||||
window.localStorage.removeItem("baron_auth_pending_provider");
|
||||
});
|
||||
}
|
||||
|
||||
async function mockUserfrontApis(
|
||||
page: Page,
|
||||
options: MockOptions = {},
|
||||
): Promise<void> {
|
||||
const sessionStatus = options.sessionStatus ?? 200;
|
||||
|
||||
await page.context().route("**/api/v1/**", async (route: Route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const path = requestUrl.pathname;
|
||||
|
||||
if (path.endsWith("/api/v1/user/me")) {
|
||||
options.captureUserMe?.();
|
||||
if (sessionStatus === 200) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
id: "e2e-user",
|
||||
email: "e2e@example.com",
|
||||
name: "E2E User",
|
||||
phone: "+821012341234",
|
||||
department: "QA",
|
||||
affiliationType: "employee",
|
||||
companyCode: "BARON",
|
||||
tenant: {
|
||||
id: "tenant-1",
|
||||
name: "Baron",
|
||||
slug: "baron",
|
||||
description: "E2E tenant",
|
||||
},
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: sessionStatus,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ error: "unauthorized" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/user/rp/linked")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ items: [] }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/audit/auth/timeline")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ items: [], next_cursor: "" }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith("/api/v1/auth/qr/approve")) {
|
||||
if (route.request().method() === "POST") {
|
||||
let pendingRef: string | null = null;
|
||||
try {
|
||||
const body = (route.request().postDataJSON() ?? {}) as {
|
||||
pendingRef?: string;
|
||||
};
|
||||
pendingRef = body.pendingRef ?? null;
|
||||
console.log(`[E2E-MOCK] /api/v1/auth/qr/approve POST body:`, body);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`[E2E-MOCK] /api/v1/auth/qr/approve POST body parse error:`,
|
||||
e,
|
||||
);
|
||||
pendingRef = null;
|
||||
}
|
||||
options.captureApprove?.(pendingRef);
|
||||
} else {
|
||||
console.log(
|
||||
`[E2E-MOCK] /api/v1/auth/qr/approve ${route.request().method()} request`,
|
||||
);
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
path.endsWith("/api/v1/auth/magic-link/verify") ||
|
||||
path.endsWith("/api/v1/auth/login/code/verify") ||
|
||||
path.endsWith("/api/v1/auth/login/code/verify-short")
|
||||
) {
|
||||
let body: Record<string, unknown> = {};
|
||||
try {
|
||||
body = (route.request().postDataJSON() ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
} catch {
|
||||
body = {};
|
||||
}
|
||||
options.captureVerify?.(path, body);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
status: "approved",
|
||||
pendingRef: "e2e-approved",
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function collectClientFailures(page: Page): string[] {
|
||||
const failures: string[] = [];
|
||||
page.on("pageerror", (error) => {
|
||||
failures.push(error.message);
|
||||
});
|
||||
page.on("console", (message) => {
|
||||
const text = message.text();
|
||||
if (
|
||||
message.type() === "error" ||
|
||||
(/exception|verify_failed|verification failed|인증 실패/i.test(text) &&
|
||||
!text.includes("Exception while loading service worker"))
|
||||
) {
|
||||
failures.push(text);
|
||||
}
|
||||
});
|
||||
return failures;
|
||||
}
|
||||
|
||||
async function expectPageToRemainBlank(page: Page): Promise<void> {
|
||||
await expect
|
||||
.poll(() => {
|
||||
const url = page.url();
|
||||
return url === '' || url === 'about:blank';
|
||||
}, { timeout: 5_000 })
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async function makeWindowCloseNavigateToRoot(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
window.close = () => {
|
||||
window.location.href = "/";
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function enableFlutterAccessibility(page: Page): Promise<void> {
|
||||
await page.waitForTimeout(300);
|
||||
const button = page.getByRole("button", { name: "Enable accessibility" });
|
||||
const placeholder = page.locator("flt-semantics-placeholder").first();
|
||||
|
||||
await button.click({ force: true, timeout: 1_000 }).catch(async () => {
|
||||
await placeholder.click({ force: true, timeout: 1_000 }).catch(async () => {
|
||||
await placeholder.evaluate((node) => {
|
||||
(node as HTMLElement).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
test.describe("UserFront WASM auth routing", () => {
|
||||
test.describe.configure({ mode: "default" });
|
||||
|
||||
test("비로그인 /ko 진입 시 /ko/signin 으로 리다이렉트된다", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockUserfrontApis(page, { sessionStatus: 401 });
|
||||
|
||||
await page.goto("/ko");
|
||||
|
||||
await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/);
|
||||
});
|
||||
|
||||
test("로그인 상태 /ko 진입 후 새로고침해도 /ko/dashboard 를 유지한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedTokenLogin(page);
|
||||
await mockUserfrontApis(page);
|
||||
|
||||
await page.goto("/ko");
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
|
||||
await page.reload();
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
});
|
||||
|
||||
test("sessionStorage 기반 로그인 상태에서도 /ko/dashboard 를 유지한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seedSessionTokenLogin(page);
|
||||
await mockUserfrontApis(page);
|
||||
|
||||
await page.goto("/ko");
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
});
|
||||
|
||||
test("비로그인 /ko/approve 는 signin(+notice)으로 이동한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockUserfrontApis(page, { sessionStatus: 401 });
|
||||
|
||||
await page.goto("/ko/approve?ref=e2e-ref");
|
||||
|
||||
await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/);
|
||||
});
|
||||
|
||||
test("로그인 상태 /ko/approve 는 승인 API 호출 후 dashboard로 이동한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
let approvedRef: string | null = null;
|
||||
|
||||
await seedTokenLogin(page);
|
||||
await mockUserfrontApis(page, {
|
||||
captureApprove: (pendingRef) => {
|
||||
approvedRef = pendingRef;
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto("/ko/approve?ref=e2e-approve-ref");
|
||||
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard(?:\?.*)?$/, {
|
||||
timeout: 10_000,
|
||||
});
|
||||
expect(approvedRef).toBe("e2e-approve-ref");
|
||||
});
|
||||
|
||||
test('verifyOnly 승인 완료 화면의 상단 액션은 signin으로 복귀시킨다', async ({
|
||||
page,
|
||||
}) => {
|
||||
let userMeCalls = 0;
|
||||
const clientFailures = collectClientFailures(page);
|
||||
const verifyRequests: Array<{
|
||||
path: string;
|
||||
body: Record<string, unknown>;
|
||||
}> = [];
|
||||
|
||||
await mockUserfrontApis(page, {
|
||||
sessionStatus: 401,
|
||||
captureUserMe: () => {
|
||||
userMeCalls += 1;
|
||||
},
|
||||
captureVerify: (path, body) => {
|
||||
verifyRequests.push({ path, body });
|
||||
},
|
||||
});
|
||||
await makeWindowCloseNavigateToRoot(page);
|
||||
|
||||
await page.goto("/ko/l/AB123456");
|
||||
|
||||
await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1);
|
||||
expect(verifyRequests[0].path).toContain(
|
||||
"/api/v1/auth/login/code/verify-short",
|
||||
);
|
||||
expect(verifyRequests[0].body).toMatchObject({
|
||||
shortCode: "AB123456",
|
||||
verifyOnly: true,
|
||||
});
|
||||
|
||||
await page.locator("flt-glass-pane").click({
|
||||
position: { x: 30, y: 28 },
|
||||
force: true,
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/);
|
||||
expect(userMeCalls).toBe(0);
|
||||
expect(
|
||||
clientFailures.filter(
|
||||
(failure) => !failure.includes('401 (Unauthorized)'),
|
||||
),
|
||||
).toEqual([]);
|
||||
|
||||
});
|
||||
|
||||
test("verifyOnly 승인 완료 버튼은 SMS 링크에서 로그인 창으로 이동하고 user/me 조회를 만들지 않는다", async ({
|
||||
page,
|
||||
}) => {
|
||||
let userMeCalls = 0;
|
||||
let verifyCalls = 0;
|
||||
const clientFailures = collectClientFailures(page);
|
||||
|
||||
await mockUserfrontApis(page, {
|
||||
sessionStatus: 401,
|
||||
captureUserMe: () => {
|
||||
userMeCalls += 1;
|
||||
},
|
||||
captureVerify: () => {
|
||||
verifyCalls += 1;
|
||||
},
|
||||
});
|
||||
await makeWindowCloseNavigateToRoot(page);
|
||||
|
||||
await page.goto("/ko/l/AB123456");
|
||||
|
||||
await expect.poll(() => verifyCalls, { timeout: 10_000 }).toBe(1);
|
||||
await expect(page).toHaveURL(/\/ko\/verify-complete$/);
|
||||
expect(userMeCalls).toBe(0);
|
||||
|
||||
await enableFlutterAccessibility(page);
|
||||
await page.getByRole("button", { name: "로그인 창으로 이동하기" }).click();
|
||||
|
||||
expect(userMeCalls).toBe(0);
|
||||
await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/);
|
||||
expect(
|
||||
clientFailures.filter(
|
||||
(failure) => !failure.includes("401 (Unauthorized)"),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test('verifyOnly 원격 승인 완료는 로그인 창 이동 CTA와 안내 문구를 표시한다', async ({
|
||||
page,
|
||||
}) => {
|
||||
let verifyCalls = 0;
|
||||
const clientFailures = collectClientFailures(page);
|
||||
|
||||
await mockUserfrontApis(page, {
|
||||
sessionStatus: 401,
|
||||
captureVerify: () => {
|
||||
verifyCalls += 1;
|
||||
},
|
||||
});
|
||||
await makeWindowCloseNavigateToRoot(page);
|
||||
|
||||
await page.goto("/ko/l/AB123456");
|
||||
|
||||
await expect.poll(() => verifyCalls, { timeout: 10_000 }).toBe(1);
|
||||
await expect(page).toHaveURL(/\/ko\/verify-complete$/);
|
||||
await enableFlutterAccessibility(page);
|
||||
|
||||
await expect(page.getByText("로그인 승인 완료")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("요청하신 로그인이 완료되었습니다"),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "창 닫기" })).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "로그인 창으로 이동하기" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "로그인 창으로 이동하기" }).click();
|
||||
await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/);
|
||||
expect(clientFailures).toEqual([]);
|
||||
});
|
||||
|
||||
test("루트/로그인에 붙은 인증 payload는 전용 verify 라우트에서만 소비하고 완료 URL을 정리한다", async ({
|
||||
page,
|
||||
}) => {
|
||||
let userMeCalls = 0;
|
||||
const verifyRequests: Array<{
|
||||
path: string;
|
||||
body: Record<string, unknown>;
|
||||
}> = [];
|
||||
const clientFailures = collectClientFailures(page);
|
||||
|
||||
await mockUserfrontApis(page, {
|
||||
sessionStatus: 401,
|
||||
captureUserMe: () => {
|
||||
userMeCalls += 1;
|
||||
},
|
||||
captureVerify: (path, body) => {
|
||||
verifyRequests.push({ path, body });
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto(
|
||||
"/?loginId=e2e%40example.com&code=654321&pendingRef=pending-root&utm=drop",
|
||||
);
|
||||
await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1);
|
||||
await expect.poll(() => page.url(), { timeout: 10_000 }).toContain(
|
||||
'/ko/verify-complete',
|
||||
);
|
||||
expect(verifyRequests[0].path).toContain('/api/v1/auth/login/code/verify');
|
||||
expect(verifyRequests[0].body).toMatchObject({
|
||||
loginId: "e2e@example.com",
|
||||
code: "654321",
|
||||
pendingRef: "pending-root",
|
||||
verifyOnly: true,
|
||||
});
|
||||
expect(page.url()).not.toContain("loginId=");
|
||||
expect(page.url()).not.toContain("code=");
|
||||
expect(page.url()).not.toContain("pendingRef=");
|
||||
expect(page.url()).not.toContain("utm=");
|
||||
expect(clientFailures).toEqual([]);
|
||||
});
|
||||
|
||||
test("로그인 페이지에 붙은 인증 payload도 전용 verify 라우트로 넘긴다", async ({
|
||||
page,
|
||||
}) => {
|
||||
let userMeCalls = 0;
|
||||
const verifyRequests: Array<{
|
||||
path: string;
|
||||
body: Record<string, unknown>;
|
||||
}> = [];
|
||||
const clientFailures = collectClientFailures(page);
|
||||
|
||||
await mockUserfrontApis(page, {
|
||||
sessionStatus: 401,
|
||||
captureUserMe: () => {
|
||||
userMeCalls += 1;
|
||||
},
|
||||
captureVerify: (path, body) => {
|
||||
verifyRequests.push({ path, body });
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto("/ko/signin?loginId=e2e%40example.com&code=999999");
|
||||
await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1);
|
||||
await expect.poll(() => page.url(), { timeout: 10_000 }).toContain(
|
||||
'/ko/verify-complete',
|
||||
);
|
||||
expect(verifyRequests[0].body).toMatchObject({
|
||||
loginId: "e2e@example.com",
|
||||
code: "999999",
|
||||
verifyOnly: true,
|
||||
});
|
||||
expect(page.url()).not.toContain("loginId=");
|
||||
expect(page.url()).not.toContain("code=");
|
||||
expect(clientFailures).toEqual([]);
|
||||
});
|
||||
|
||||
test("verifyOnly 승인 링크를 팝업에서 닫으면 창만 닫히고 부모는 이동하지 않는다", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name === "webkit-mobile-webapp",
|
||||
"Mobile WebKit closes the opener page when this popup flow closes in headless mode.",
|
||||
);
|
||||
let userMeCalls = 0;
|
||||
let verifyCalls = 0;
|
||||
const clientFailures = collectClientFailures(page);
|
||||
|
||||
await mockUserfrontApis(page, {
|
||||
sessionStatus: 401,
|
||||
captureUserMe: () => {
|
||||
userMeCalls += 1;
|
||||
},
|
||||
captureVerify: () => {
|
||||
verifyCalls += 1;
|
||||
},
|
||||
});
|
||||
|
||||
const baseURL = testInfo.project.use.baseURL;
|
||||
if (typeof baseURL !== "string") throw new Error("baseURL is required");
|
||||
const popupURL = new URL("/ko/l/AB123456", baseURL).toString();
|
||||
const parentURL = new URL("/version.json", baseURL).toString();
|
||||
|
||||
await page.goto(parentURL);
|
||||
await expect(page).toHaveURL(parentURL);
|
||||
|
||||
const popupPromise = page.waitForEvent("popup");
|
||||
await page.evaluate((url) => {
|
||||
window.open(url, "_blank");
|
||||
}, popupURL);
|
||||
const popup = await popupPromise;
|
||||
|
||||
await expect.poll(() => verifyCalls, { timeout: 10_000 }).toBe(1);
|
||||
await expect(popup).toHaveURL(/\/ko\/verify-complete$/);
|
||||
expect(userMeCalls).toBe(0);
|
||||
|
||||
if (!popup.isClosed()) {
|
||||
const closePromise = popup.waitForEvent("close").catch(() => undefined);
|
||||
try {
|
||||
await enableFlutterAccessibility(popup);
|
||||
await popup
|
||||
.getByRole("button", { name: "로그인 창으로 이동하기" })
|
||||
.click();
|
||||
} catch (error) {
|
||||
if (!popup.isClosed()) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await closePromise;
|
||||
}
|
||||
|
||||
expect(userMeCalls).toBe(0);
|
||||
await expect(page).toHaveURL(parentURL);
|
||||
expect(clientFailures).toEqual([]);
|
||||
});
|
||||
|
||||
test("verifyOnly 승인 완료 버튼은 이메일 magic link에서도 로그인 창으로 이동하고 user/me 조회를 만들지 않는다", async ({
|
||||
page,
|
||||
}) => {
|
||||
let userMeCalls = 0;
|
||||
const clientFailures = collectClientFailures(page);
|
||||
const verifyRequests: Array<{
|
||||
path: string;
|
||||
body: Record<string, unknown>;
|
||||
}> = [];
|
||||
|
||||
await mockUserfrontApis(page, {
|
||||
sessionStatus: 401,
|
||||
captureUserMe: () => {
|
||||
userMeCalls += 1;
|
||||
},
|
||||
captureVerify: (path, body) => {
|
||||
verifyRequests.push({ path, body });
|
||||
},
|
||||
});
|
||||
await makeWindowCloseNavigateToRoot(page);
|
||||
|
||||
await page.goto("/ko/verify/e2e-email-token");
|
||||
|
||||
await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1);
|
||||
expect(verifyRequests[0].path).toContain('/api/v1/auth/magic-link/verify');
|
||||
expect(verifyRequests[0].body).toMatchObject({
|
||||
token: "e2e-email-token",
|
||||
verifyOnly: true,
|
||||
});
|
||||
|
||||
await enableFlutterAccessibility(page);
|
||||
await page.getByRole("button", { name: "로그인 창으로 이동하기" }).click();
|
||||
|
||||
expect(userMeCalls).toBe(0);
|
||||
await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/);
|
||||
expect(clientFailures).toEqual([]);
|
||||
});
|
||||
|
||||
test("verifyOnly 승인 완료 버튼은 이메일 code link에서도 로그인 창으로 이동하고 user/me 조회를 만들지 않는다", async ({
|
||||
page,
|
||||
}) => {
|
||||
let userMeCalls = 0;
|
||||
const clientFailures = collectClientFailures(page);
|
||||
const verifyRequests: Array<{
|
||||
path: string;
|
||||
body: Record<string, unknown>;
|
||||
}> = [];
|
||||
|
||||
await mockUserfrontApis(page, {
|
||||
sessionStatus: 401,
|
||||
captureUserMe: () => {
|
||||
userMeCalls += 1;
|
||||
},
|
||||
captureVerify: (path, body) => {
|
||||
verifyRequests.push({ path, body });
|
||||
},
|
||||
});
|
||||
await makeWindowCloseNavigateToRoot(page);
|
||||
|
||||
await page.goto(
|
||||
"/ko/verify?loginId=e2e%40example.com&code=654321&pendingRef=pending-email",
|
||||
);
|
||||
|
||||
await expect.poll(() => verifyRequests.length, { timeout: 10_000 }).toBe(1);
|
||||
expect(verifyRequests[0].path).toContain('/api/v1/auth/login/code/verify');
|
||||
expect(verifyRequests[0].body).toMatchObject({
|
||||
loginId: "e2e@example.com",
|
||||
code: "654321",
|
||||
pendingRef: "pending-email",
|
||||
verifyOnly: true,
|
||||
});
|
||||
|
||||
await enableFlutterAccessibility(page);
|
||||
await page.getByRole("button", { name: "로그인 창으로 이동하기" }).click();
|
||||
|
||||
expect(userMeCalls).toBe(0);
|
||||
await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/);
|
||||
expect(clientFailures).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user