forked from baron/baron-sso
e2e 구조변경
This commit is contained in:
3
userfront-e2e/.gitignore
vendored
Normal file
3
userfront-e2e/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
playwright-report/
|
||||
test-results/
|
||||
29
userfront-e2e/README.md
Normal file
29
userfront-e2e/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# UserFront WASM E2E
|
||||
|
||||
`userfront` WASM 빌드 산출물을 Playwright로 검증하는 테스트 워크스페이스입니다.
|
||||
|
||||
## 실행 방법
|
||||
|
||||
1. 의존성 설치
|
||||
```bash
|
||||
cd userfront-e2e
|
||||
npm install
|
||||
```
|
||||
|
||||
2. 테스트 실행(빌드 포함)
|
||||
```bash
|
||||
cd userfront-e2e
|
||||
npm run test:wasm
|
||||
```
|
||||
|
||||
3. 이미 빌드가 있을 때 테스트만 실행
|
||||
```bash
|
||||
cd userfront-e2e
|
||||
npm test
|
||||
```
|
||||
|
||||
## 환경변수
|
||||
|
||||
- `BASE_URL`: 외부 배포 URL을 테스트할 때 사용합니다. 설정하면 로컬 정적 서버를 띄우지 않습니다.
|
||||
- `PORT`: 로컬 정적 서버 포트 (기본 `4173`)
|
||||
- `LOCALE`: 브라우저 locale (기본 `ko-KR`)
|
||||
111
userfront-e2e/package-lock.json
generated
Normal file
111
userfront-e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"name": "userfront-e2e",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "userfront-e2e",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^24.3.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz",
|
||||
"integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
userfront-e2e/package.json
Normal file
18
userfront-e2e/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "userfront-e2e",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:ui": "playwright test --ui",
|
||||
"serve:build": "node ./scripts/serve-userfront-build.mjs",
|
||||
"build:userfront:wasm": "cd ../userfront && flutter build web --wasm --release",
|
||||
"test:wasm": "npm run build:userfront:wasm && npm test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^24.3.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
39
userfront-e2e/playwright.config.ts
Normal file
39
userfront-e2e/playwright.config.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
const port = Number.parseInt(process.env.PORT ?? '4173', 10);
|
||||
const defaultBaseUrl = `http://127.0.0.1:${port}`;
|
||||
const baseURL = process.env.BASE_URL ?? defaultBaseUrl;
|
||||
const reuseExistingServer = !process.env.CI;
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'html',
|
||||
use: {
|
||||
baseURL,
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
locale: process.env.LOCALE ?? 'ko-KR',
|
||||
serviceWorkers: 'block',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: process.env.BASE_URL
|
||||
? undefined
|
||||
: {
|
||||
command: 'node ./scripts/serve-userfront-build.mjs',
|
||||
url: defaultBaseUrl,
|
||||
reuseExistingServer,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
68
userfront-e2e/scripts/serve-userfront-build.mjs
Normal file
68
userfront-e2e/scripts/serve-userfront-build.mjs
Normal file
@@ -0,0 +1,68 @@
|
||||
import { createReadStream, existsSync, statSync } from 'node:fs';
|
||||
import { dirname, extname, join, normalize } from 'node:path';
|
||||
import { createServer } from 'node:http';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const root = normalize(join(__dirname, '../../userfront/build/web'));
|
||||
|
||||
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
||||
console.error(
|
||||
'[userfront-e2e] userfront/build/web not found. Run: cd userfront && flutter build web --wasm --release',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const port = Number.parseInt(process.env.PORT ?? '4173', 10);
|
||||
|
||||
const contentTypes = {
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.ico': 'image/x-icon',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.mjs': 'application/javascript; charset=utf-8',
|
||||
'.png': 'image/png',
|
||||
'.svg': 'image/svg+xml; charset=utf-8',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.wasm': 'application/wasm',
|
||||
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
};
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const url = new URL(req.url ?? '/', 'http://localhost');
|
||||
const pathname = decodeURIComponent(url.pathname);
|
||||
const relative = pathname === '/' ? '/index.html' : pathname;
|
||||
const candidate = normalize(join(root, relative));
|
||||
|
||||
if (!candidate.startsWith(root)) {
|
||||
res.statusCode = 403;
|
||||
res.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
let filePath = candidate;
|
||||
|
||||
if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
|
||||
// Flutter web 라우팅 경로(`/ko`, `/ko/signin`)도 index.html로 fallback 처리
|
||||
filePath = join(root, 'index.html');
|
||||
}
|
||||
|
||||
const ext = extname(filePath);
|
||||
const contentType = contentTypes[ext] ?? 'application/octet-stream';
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
createReadStream(filePath)
|
||||
.on('error', () => {
|
||||
res.statusCode = 500;
|
||||
res.end('Internal Server Error');
|
||||
})
|
||||
.pipe(res);
|
||||
});
|
||||
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
console.log(`[userfront-e2e] serving ${root} at http://127.0.0.1:${port}`);
|
||||
});
|
||||
143
userfront-e2e/tests/auth-routing.spec.ts
Normal file
143
userfront-e2e/tests/auth-routing.spec.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
type MockOptions = {
|
||||
sessionStatus?: number;
|
||||
captureApprove?: (pendingRef: string | null) => 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 mockUserfrontApis(
|
||||
page: Page,
|
||||
options: MockOptions = {},
|
||||
): Promise<void> {
|
||||
const sessionStatus = options.sessionStatus ?? 200;
|
||||
|
||||
await page.route('**/api/v1/**', async (route: Route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const path = requestUrl.pathname;
|
||||
|
||||
if (path.endsWith('/api/v1/user/me')) {
|
||||
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')) {
|
||||
const body = route.request().postDataJSON() as { pendingRef?: string };
|
||||
options.captureApprove?.(body.pendingRef ?? null);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('UserFront WASM auth routing', () => {
|
||||
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('비로그인 /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$/);
|
||||
expect(approvedRef).toBe('e2e-approve-ref');
|
||||
});
|
||||
});
|
||||
257
userfront-e2e/tests/password-and-reset.spec.ts
Normal file
257
userfront-e2e/tests/password-and-reset.spec.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
type RequestCapture = {
|
||||
loginBody?: Record<string, unknown>;
|
||||
resetBody?: Record<string, unknown>;
|
||||
resetToken?: string | null;
|
||||
clientLogs: string[];
|
||||
};
|
||||
|
||||
const SIGNIN_PASSWORD_TAB_X = 522;
|
||||
const SIGNIN_TAB_Y = 158;
|
||||
const SIGNIN_LOGIN_ID_X = 640;
|
||||
const SIGNIN_LOGIN_ID_Y = 245;
|
||||
const SIGNIN_PASSWORD_X = 640;
|
||||
const SIGNIN_PASSWORD_Y = 311;
|
||||
const SIGNIN_SUBMIT_X = 640;
|
||||
const SIGNIN_SUBMIT_Y = 381;
|
||||
|
||||
const RESET_NEW_PASSWORD_X = 640;
|
||||
const RESET_NEW_PASSWORD_Y = 401;
|
||||
const RESET_CONFIRM_PASSWORD_X = 640;
|
||||
const RESET_CONFIRM_PASSWORD_Y = 464;
|
||||
const RESET_SUBMIT_X = 640;
|
||||
const RESET_SUBMIT_Y = 534;
|
||||
|
||||
async function clickPasswordTab(page: Page): Promise<void> {
|
||||
await page.waitForTimeout(900);
|
||||
const pane = page.locator('flt-glass-pane');
|
||||
await pane.click({
|
||||
position: { x: SIGNIN_PASSWORD_TAB_X, y: SIGNIN_TAB_Y },
|
||||
force: true,
|
||||
});
|
||||
await page.waitForTimeout(120);
|
||||
await pane.click({
|
||||
position: { x: SIGNIN_PASSWORD_TAB_X, y: SIGNIN_TAB_Y },
|
||||
force: true,
|
||||
});
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
async function fillAt(page: Page, x: number, y: number, value: string): Promise<void> {
|
||||
const pane = page.locator('flt-glass-pane');
|
||||
await pane.click({ position: { x, y }, force: true });
|
||||
await page.waitForTimeout(100);
|
||||
await page.keyboard.press('Control+A');
|
||||
await page.keyboard.press('Backspace');
|
||||
await page.keyboard.type(value);
|
||||
}
|
||||
|
||||
async function mockAuthApis(page: Page, capture: RequestCapture): Promise<void> {
|
||||
await page.route('**/api/v1/**', async (route: Route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const path = requestUrl.pathname;
|
||||
|
||||
if (path.endsWith('/api/v1/auth/password/login')) {
|
||||
capture.loginBody = (route.request().postDataJSON() ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const loginId = String(capture.loginBody.loginId ?? '');
|
||||
const password = String(capture.loginBody.password ?? '');
|
||||
if (loginId === 'e2e@example.com' && password === 'ValidPass1!') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
sessionJwt: 'e30.e30.e30',
|
||||
provider: 'ory',
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'password_or_email_mismatch' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/auth/password/policy')) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
minLength: 12,
|
||||
minCharacterTypes: 3,
|
||||
lowercase: true,
|
||||
uppercase: true,
|
||||
number: true,
|
||||
nonAlphanumeric: true,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/auth/password/reset/complete')) {
|
||||
capture.resetBody = (route.request().postDataJSON() ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
capture.resetToken = requestUrl.searchParams.get('token');
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ status: 'ok' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/client-log')) {
|
||||
const payload = (route.request().postDataJSON() ?? {}) as {
|
||||
message?: string;
|
||||
};
|
||||
if (payload.message != null) {
|
||||
capture.clientLogs.push(payload.message);
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/user/me')) {
|
||||
const authHeader = route.request().headers()['authorization'] ?? '';
|
||||
if (!authHeader.startsWith('Bearer ')) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'unauthorized' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('UserFront WASM password login and reset', () => {
|
||||
test('비밀번호 로그인 성공 시 dashboard로 이동하고 토큰을 저장한다', async ({ page }) => {
|
||||
const capture: RequestCapture = { clientLogs: [] };
|
||||
await mockAuthApis(page, capture);
|
||||
|
||||
await page.goto('/ko/signin');
|
||||
await clickPasswordTab(page);
|
||||
await fillAt(page, SIGNIN_LOGIN_ID_X, SIGNIN_LOGIN_ID_Y, 'e2e@example.com');
|
||||
await fillAt(page, SIGNIN_PASSWORD_X, SIGNIN_PASSWORD_Y, 'ValidPass1!');
|
||||
await page.locator('flt-glass-pane').click({
|
||||
position: { x: SIGNIN_SUBMIT_X, y: SIGNIN_SUBMIT_Y },
|
||||
force: true,
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
|
||||
expect(capture.loginBody?.loginId).toBe('e2e@example.com');
|
||||
expect(capture.loginBody?.password).toBe('ValidPass1!');
|
||||
|
||||
const storedToken = await page.evaluate(() =>
|
||||
window.localStorage.getItem('baron_auth_token'),
|
||||
);
|
||||
expect(storedToken).toBe('e30.e30.e30');
|
||||
});
|
||||
|
||||
test('비밀번호 로그인 실패 시 에러 코드를 사용자에게 표시한다', async ({ page }) => {
|
||||
const capture: RequestCapture = { clientLogs: [] };
|
||||
await mockAuthApis(page, capture);
|
||||
|
||||
await page.goto('/ko/signin');
|
||||
await clickPasswordTab(page);
|
||||
await fillAt(page, SIGNIN_LOGIN_ID_X, SIGNIN_LOGIN_ID_Y, 'e2e@example.com');
|
||||
await fillAt(page, SIGNIN_PASSWORD_X, SIGNIN_PASSWORD_Y, 'WrongPass1!');
|
||||
await page.locator('flt-glass-pane').click({
|
||||
position: { x: SIGNIN_SUBMIT_X, y: SIGNIN_SUBMIT_Y },
|
||||
force: true,
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/ko\/signin$/);
|
||||
await expect
|
||||
.poll(() =>
|
||||
capture.clientLogs.some((message) =>
|
||||
message.includes('password_or_email_mismatch'),
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test('reset-password에서 변경 성공 시 signin으로 이동한다', async ({ page }) => {
|
||||
const capture: RequestCapture = { clientLogs: [] };
|
||||
await mockAuthApis(page, capture);
|
||||
|
||||
await page.goto('/ko/reset-password?token=reset-token-e2e');
|
||||
await page.waitForTimeout(900);
|
||||
await fillAt(page, RESET_NEW_PASSWORD_X, RESET_NEW_PASSWORD_Y, 'ValidPass1!A');
|
||||
await fillAt(
|
||||
page,
|
||||
RESET_CONFIRM_PASSWORD_X,
|
||||
RESET_CONFIRM_PASSWORD_Y,
|
||||
'ValidPass1!A',
|
||||
);
|
||||
await page.locator('flt-glass-pane').click({
|
||||
position: { x: RESET_SUBMIT_X, y: RESET_SUBMIT_Y },
|
||||
force: true,
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/ko\/signin$/);
|
||||
expect(capture.resetToken).toBe('reset-token-e2e');
|
||||
expect(capture.resetBody?.newPassword).toBe('ValidPass1!A');
|
||||
});
|
||||
});
|
||||
275
userfront-e2e/tests/profile-department.spec.ts
Normal file
275
userfront-e2e/tests/profile-department.spec.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
type ProfileState = {
|
||||
department: string;
|
||||
getMeCount: number;
|
||||
putBodies: Array<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
const PROFILE_DEPARTMENT_EDIT_X = 1170;
|
||||
const PROFILE_DEPARTMENT_EDIT_Y = 680;
|
||||
const PROFILE_DEPARTMENT_INPUT_X = 110;
|
||||
const PROFILE_DEPARTMENT_INPUT_Y = 685;
|
||||
const PROFILE_BLUR_X = 200;
|
||||
const PROFILE_BLUR_Y = 260;
|
||||
|
||||
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 fillAt(page: Page, x: number, y: number, value: string): Promise<void> {
|
||||
const pane = page.locator('flt-glass-pane');
|
||||
await pane.click({ position: { x, y }, force: true });
|
||||
await page.waitForTimeout(100);
|
||||
await page.keyboard.press('Control+A');
|
||||
await page.keyboard.press('Backspace');
|
||||
await page.keyboard.type(value);
|
||||
}
|
||||
|
||||
async function openDepartmentEditor(page: Page): Promise<void> {
|
||||
await page.locator('flt-glass-pane').click({
|
||||
position: { x: PROFILE_DEPARTMENT_EDIT_X, y: PROFILE_DEPARTMENT_EDIT_Y },
|
||||
force: true,
|
||||
});
|
||||
await page.waitForTimeout(200);
|
||||
}
|
||||
|
||||
async function blurDepartmentEditor(page: Page): Promise<void> {
|
||||
await page.locator('flt-glass-pane').click({
|
||||
position: { x: PROFILE_BLUR_X, y: PROFILE_BLUR_Y },
|
||||
force: true,
|
||||
});
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
|
||||
async function mockProfileApis(page: Page, state: ProfileState): Promise<void> {
|
||||
await page.route('**/api/v1/**', async (route: Route) => {
|
||||
const request = route.request();
|
||||
const requestUrl = new URL(request.url());
|
||||
const path = requestUrl.pathname;
|
||||
const method = request.method().toUpperCase();
|
||||
|
||||
if (path.endsWith('/api/v1/user/me') && method === 'GET') {
|
||||
const authHeader = request.headers()['authorization'] ?? '';
|
||||
if (!authHeader.startsWith('Bearer ')) {
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'unauthorized' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
state.getMeCount += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
id: 'e2e-user',
|
||||
email: 'e2e@example.com',
|
||||
name: 'E2E User',
|
||||
phone: '+821012341234',
|
||||
department: state.department,
|
||||
affiliationType: 'employee',
|
||||
companyCode: 'BARON',
|
||||
tenant: {
|
||||
id: 'tenant-1',
|
||||
name: 'Baron',
|
||||
slug: 'baron',
|
||||
description: 'E2E tenant',
|
||||
},
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/user/me') && method === 'PUT') {
|
||||
const body = (request.postDataJSON() ?? {}) as Record<string, unknown>;
|
||||
state.putBodies.push(body);
|
||||
const nextDepartment = String(body.department ?? '').trim();
|
||||
if (nextDepartment !== '') {
|
||||
state.department = nextDepartment;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
status: 'success',
|
||||
updatedAt: '2026-02-24T00:00:00Z',
|
||||
}),
|
||||
});
|
||||
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/client-log')) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function openProfilePage(page: Page): Promise<void> {
|
||||
await page.goto('/ko/profile');
|
||||
await expect(page).toHaveURL(/\/ko\/profile$/);
|
||||
await page.waitForTimeout(1200);
|
||||
}
|
||||
|
||||
async function waitForInitialProfileLoad(state: ProfileState): Promise<void> {
|
||||
await expect.poll(() => state.getMeCount).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
test.describe('UserFront WASM profile department editing', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await page.unroute('**/api/v1/**');
|
||||
});
|
||||
|
||||
test('소속 수정 후 포커스 아웃하면 저장 요청이 전송되고 새로고침 후 최신 값으로 재조회된다', async ({
|
||||
page,
|
||||
}) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
await seedTokenLogin(page);
|
||||
await mockProfileApis(page, state);
|
||||
await openProfilePage(page);
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Updated');
|
||||
await blurDepartmentEditor(page);
|
||||
|
||||
await expect.poll(() => state.putBodies.length).toBe(1);
|
||||
expect(state.putBodies[0]?.department).toBe('QA-Updated');
|
||||
expect(state.department).toBe('QA-Updated');
|
||||
|
||||
const getCountBeforeReload = state.getMeCount;
|
||||
await page.reload();
|
||||
await expect(page).toHaveURL(/\/ko\/profile$/);
|
||||
await expect.poll(() => state.getMeCount).toBeGreaterThan(getCountBeforeReload);
|
||||
});
|
||||
|
||||
test('재현: 소속 입력만 하고 즉시 새로고침하면 저장 요청이 전송되지 않는다', async ({
|
||||
page,
|
||||
}) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
await seedTokenLogin(page);
|
||||
await mockProfileApis(page, state);
|
||||
await openProfilePage(page);
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-Repro');
|
||||
|
||||
await page.reload();
|
||||
await expect(page).toHaveURL(/\/ko\/profile$/);
|
||||
expect(state.putBodies).toHaveLength(0);
|
||||
expect(state.department).toBe('QA');
|
||||
});
|
||||
|
||||
test('소속에 기존값을 그대로 입력하고 포커스 아웃하면 저장 요청이 전송되지 않는다', async ({
|
||||
page,
|
||||
}) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
await seedTokenLogin(page);
|
||||
await mockProfileApis(page, state);
|
||||
await openProfilePage(page);
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA');
|
||||
await blurDepartmentEditor(page);
|
||||
|
||||
expect(state.putBodies).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('소속을 빈 값으로 입력하면 저장 요청이 전송되지 않는다', async ({ page }) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
await seedTokenLogin(page);
|
||||
await mockProfileApis(page, state);
|
||||
await openProfilePage(page);
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, '');
|
||||
await blurDepartmentEditor(page);
|
||||
|
||||
expect(state.putBodies).toHaveLength(0);
|
||||
expect(state.department).toBe('QA');
|
||||
});
|
||||
|
||||
test('소속을 수정한 뒤 새로고침 후 다시 수정해도 저장 요청이 누락되지 않는다', async ({ page }) => {
|
||||
const state: ProfileState = {
|
||||
department: 'QA',
|
||||
getMeCount: 0,
|
||||
putBodies: [],
|
||||
};
|
||||
await seedTokenLogin(page);
|
||||
await mockProfileApis(page, state);
|
||||
await openProfilePage(page);
|
||||
await waitForInitialProfileLoad(state);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-1');
|
||||
await blurDepartmentEditor(page);
|
||||
await expect.poll(() => state.putBodies.length).toBe(1);
|
||||
|
||||
await page.reload();
|
||||
await expect(page).toHaveURL(/\/ko\/profile$/);
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
await openDepartmentEditor(page);
|
||||
await fillAt(page, PROFILE_DEPARTMENT_INPUT_X, PROFILE_DEPARTMENT_INPUT_Y, 'QA-2');
|
||||
await blurDepartmentEditor(page);
|
||||
await expect.poll(() => state.putBodies.length).toBe(2);
|
||||
|
||||
expect(state.putBodies[0]?.department).toBe('QA-1');
|
||||
expect(state.putBodies[1]?.department).toBe('QA-2');
|
||||
expect(state.department).toBe('QA-2');
|
||||
});
|
||||
});
|
||||
320
userfront-e2e/tests/route-inventory.spec.ts
Normal file
320
userfront-e2e/tests/route-inventory.spec.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
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 mockInventoryApis(page: Page): Promise<void> {
|
||||
await page.route('**/api/v1/**', async (route: Route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const path = requestUrl.pathname;
|
||||
const method = route.request().method().toUpperCase();
|
||||
|
||||
if (path.endsWith('/api/v1/user/me')) {
|
||||
const authHeader = route.request().headers()['authorization'] ?? '';
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
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: 401,
|
||||
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/password/policy')) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
minLength: 12,
|
||||
minCharacterTypes: 3,
|
||||
lowercase: true,
|
||||
uppercase: true,
|
||||
number: true,
|
||||
nonAlphanumeric: true,
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/auth/magic-link/verify')) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ status: 'approved' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/auth/login/code/verify')) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ status: 'approved' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/auth/login/code/verify-short')) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ status: 'approved' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/auth/consent') && method === 'GET') {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
client: {
|
||||
client_name: 'E2E Client',
|
||||
client_id: 'e2e-client',
|
||||
},
|
||||
requested_scope: ['openid'],
|
||||
scope_details: {
|
||||
openid: {
|
||||
description: 'OpenID',
|
||||
mandatory: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/auth/qr/approve')) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.endsWith('/api/v1/client-log')) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ ok: true }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('UserFront WASM route inventory (unauth)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockInventoryApis(page);
|
||||
});
|
||||
|
||||
test('route: /', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/\/(ko|en)\/signin(?:\?.*)?$/);
|
||||
});
|
||||
|
||||
test('route: /ko', async ({ page }) => {
|
||||
await page.goto('/ko');
|
||||
await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/);
|
||||
});
|
||||
|
||||
test('route: /ko/dashboard', async ({ page }) => {
|
||||
await page.goto('/ko/dashboard');
|
||||
await expect(page).toHaveURL(/\/ko\/signin$/);
|
||||
});
|
||||
|
||||
test('route: /ko/profile', async ({ page }) => {
|
||||
await page.goto('/ko/profile');
|
||||
await expect(page).toHaveURL(/\/ko\/signin$/);
|
||||
});
|
||||
|
||||
test('route: /ko/admin/users', async ({ page }) => {
|
||||
await page.goto('/ko/admin/users');
|
||||
await expect(page).toHaveURL(/\/ko\/signin$/);
|
||||
});
|
||||
|
||||
test('route: /ko/scan', async ({ page }) => {
|
||||
await page.goto('/ko/scan');
|
||||
await expect(page).toHaveURL(/\/ko\/signin$/);
|
||||
});
|
||||
|
||||
test('route: /ko/signin', async ({ page }) => {
|
||||
await page.goto('/ko/signin');
|
||||
await expect(page).toHaveURL(/\/ko\/signin$/);
|
||||
});
|
||||
|
||||
test('route: /ko/login', async ({ page }) => {
|
||||
await page.goto('/ko/login');
|
||||
await expect(page).toHaveURL(/\/ko\/login$/);
|
||||
});
|
||||
|
||||
test('route: /ko/signup', async ({ page }) => {
|
||||
await page.goto('/ko/signup');
|
||||
await expect(page).toHaveURL(/\/ko\/signup$/);
|
||||
});
|
||||
|
||||
test('route: /ko/registration', async ({ page }) => {
|
||||
await page.goto('/ko/registration');
|
||||
await expect(page).toHaveURL(/\/ko\/registration$/);
|
||||
});
|
||||
|
||||
test('route: /ko/verify', async ({ page }) => {
|
||||
await page.goto('/ko/verify');
|
||||
await expect(page).toHaveURL(/\/ko\/verify$/);
|
||||
});
|
||||
|
||||
test('route: /ko/verify/:token', async ({ page }) => {
|
||||
await page.goto('/ko/verify/e2e-token');
|
||||
await expect(page).toHaveURL(/\/ko\/verify\/e2e-token$/);
|
||||
});
|
||||
|
||||
test('route: /ko/verification', async ({ page }) => {
|
||||
await page.goto('/ko/verification');
|
||||
await expect(page).toHaveURL(/\/ko\/verification$/);
|
||||
});
|
||||
|
||||
test('route: /ko/l/:shortCode', async ({ page }) => {
|
||||
await page.goto('/ko/l/AB123456');
|
||||
await expect(page).toHaveURL(/\/ko\/l\/AB123456$/);
|
||||
});
|
||||
|
||||
test('route: /ko/forgot-password', async ({ page }) => {
|
||||
await page.goto('/ko/forgot-password');
|
||||
await expect(page).toHaveURL(/\/ko\/forgot-password$/);
|
||||
});
|
||||
|
||||
test('route: /ko/recovery', async ({ page }) => {
|
||||
await page.goto('/ko/recovery');
|
||||
await expect(page).toHaveURL(/\/ko\/recovery$/);
|
||||
});
|
||||
|
||||
test('route: /ko/reset-password', async ({ page }) => {
|
||||
await page.goto('/ko/reset-password?token=e2e-reset-token');
|
||||
await expect(page).toHaveURL(/\/ko\/reset-password\?token=e2e-reset-token$/);
|
||||
});
|
||||
|
||||
test('route: /ko/error', async ({ page }) => {
|
||||
await page.goto('/ko/error?error=invalid_request');
|
||||
await expect(page).toHaveURL(/\/ko\/error\?error=invalid_request$/);
|
||||
});
|
||||
|
||||
test('route: /ko/settings', async ({ page }) => {
|
||||
await page.goto('/ko/settings');
|
||||
await expect(page).toHaveURL(/\/ko\/settings$/);
|
||||
});
|
||||
|
||||
test('route: /ko/consent (missing challenge)', async ({ page }) => {
|
||||
await page.goto('/ko/consent');
|
||||
await expect(page).toHaveURL(/\/ko\/consent$/);
|
||||
});
|
||||
|
||||
test('route: /ko/consent?consent_challenge=...', async ({ page }) => {
|
||||
await page.goto('/ko/consent?consent_challenge=e2e-consent');
|
||||
await expect(page).toHaveURL(/\/ko\/consent\?consent_challenge=e2e-consent$/);
|
||||
});
|
||||
|
||||
test('route: /ko/approve?ref=...', async ({ page }) => {
|
||||
await page.goto('/ko/approve?ref=e2e-ref');
|
||||
await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/);
|
||||
});
|
||||
|
||||
test('route: /ko/ql/:ref', async ({ page }) => {
|
||||
await page.goto('/ko/ql/e2e-ref');
|
||||
await expect(page).toHaveURL(/\/ko\/signin\?notice=qr_login_required$/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UserFront WASM route inventory (authed)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await seedTokenLogin(page);
|
||||
await mockInventoryApis(page);
|
||||
});
|
||||
|
||||
test('route: /ko -> /ko/dashboard', async ({ page }) => {
|
||||
await page.goto('/ko');
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
});
|
||||
|
||||
test('route: /ko/dashboard', async ({ page }) => {
|
||||
await page.goto('/ko/dashboard');
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
});
|
||||
|
||||
test('route: /ko/profile', async ({ page }) => {
|
||||
await page.goto('/ko/profile');
|
||||
await expect(page).toHaveURL(/\/ko\/profile$/);
|
||||
});
|
||||
|
||||
test('route: /ko/admin/users', async ({ page }) => {
|
||||
await page.goto('/ko/admin/users');
|
||||
await expect(page).toHaveURL(/\/ko\/admin\/users$/);
|
||||
});
|
||||
|
||||
test('route: /ko/scan', async ({ page }) => {
|
||||
await page.goto('/ko/scan');
|
||||
await expect(page).toHaveURL(/\/ko\/scan$/);
|
||||
});
|
||||
|
||||
test('route: /ko/approve?ref=... -> /ko/dashboard', async ({ page }) => {
|
||||
await page.goto('/ko/approve?ref=e2e-ref');
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
});
|
||||
|
||||
test('route: /ko/ql/:ref -> /ko/dashboard', async ({ page }) => {
|
||||
await page.goto('/ko/ql/e2e-ref');
|
||||
await expect(page).toHaveURL(/\/ko\/dashboard$/);
|
||||
});
|
||||
});
|
||||
14
userfront-e2e/tsconfig.json
Normal file
14
userfront-e2e/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"types": ["node", "@playwright/test"],
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["playwright.config.ts", "tests/**/*.ts", "scripts/**/*.mjs"]
|
||||
}
|
||||
Reference in New Issue
Block a user