1
0
forked from baron/baron-sso

모바일 로그인창 테스트 강화

This commit is contained in:
2026-05-27 11:46:11 +09:00
parent 53830b20d8
commit 368f4bbad8
17 changed files with 268 additions and 156 deletions

View File

@@ -13,7 +13,7 @@ export default defineConfig({
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: configuredWorkers ?? (process.env.CI ? 1 : undefined),
workers: configuredWorkers ?? 1,
reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'html',
use: {
baseURL,
@@ -23,6 +23,20 @@ export default defineConfig({
locale: process.env.LOCALE ?? 'ko-KR',
},
projects: [
{
name: 'webkit-desktop',
use: {
...devices['Desktop Safari'],
serviceWorkers: 'block',
},
},
{
name: 'webkit-mobile-webapp',
use: {
...devices['iPhone 13'],
serviceWorkers: 'block',
},
},
{
name: 'chromium-desktop',
use: {
@@ -30,11 +44,18 @@ export default defineConfig({
serviceWorkers: 'block',
},
},
{
name: 'firefox-desktop',
use: {
...devices['Desktop Firefox'],
serviceWorkers: 'block',
},
},
{
name: 'chromium-mobile-webapp',
use: {
...devices['Pixel 7'],
serviceWorkers: 'allow',
serviceWorkers: 'block',
},
},
],

View File

@@ -101,9 +101,7 @@ function expectNoDuplicateStaticRequests(metrics: LoadMetrics): void {
!path.endsWith('/main.dart.wasm') &&
!path.endsWith('/main.dart.mjs') &&
!path.endsWith('/skwasm.js') &&
!path.endsWith('/skwasm.wasm') &&
!path.endsWith('/assets/assets/fonts/NotoSansKR-Regular.ttf') &&
!path.endsWith('/assets/assets/fonts/NotoSansKR-Bold.ttf')
!path.endsWith('/skwasm.wasm')
);
},
);

View File

@@ -1,6 +1,17 @@
import { expect, test, type Page } from '@playwright/test';
import {
expect,
test,
type BrowserContext,
type Page,
type TestInfo,
} from '@playwright/test';
import { readFileSync } from 'node:fs';
import { inflateSync } from 'node:zlib';
const lightweightTestFont = readFileSync(
new URL('../fixtures/fonts/NotoSansKR-TestSubset.woff2', import.meta.url),
);
type SigninCase = {
path: '/ko/signin' | '/en/signin';
theme: 'light' | 'dark';
@@ -13,8 +24,8 @@ const signinCases: SigninCase[] = [
{ path: '/en/signin', theme: 'dark' },
];
async function mockPublicApis(page: Page): Promise<void> {
await page.route('**/api/v1/**', async (route) => {
async function mockPublicApis(context: BrowserContext): Promise<void> {
await context.route(/\/api\/v1\//, async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.pathname.endsWith('/api/v1/user/me')) {
await route.fulfill({
@@ -42,10 +53,27 @@ async function mockPublicApis(page: Page): Promise<void> {
});
}
async function routeLightweightTestFonts(context: BrowserContext): Promise<void> {
await context.route('https://fonts.gstatic.com/**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'font/woff2',
body: lightweightTestFont,
headers: {
'access-control-allow-origin': '*',
'cache-control': 'public, max-age=31536000, immutable',
},
});
});
}
async function expectFlutterCanvasRendered(
page: Page,
timeoutMs = 5_000,
): Promise<void> {
await expect(page.locator('#baron-bootstrap-shell')).toBeHidden({
timeout: timeoutMs,
});
await expect
.poll(() => page.screenshot().then(screenshotHasSigninPaint), {
timeout: timeoutMs,
@@ -59,6 +87,36 @@ async function expectBootstrapShellVisible(page: Page): Promise<void> {
await expect(shell).toContainText(/Baron SW Portal/);
}
async function expectSigninSurfaceWithinBudget(
page: Page,
testInfo: TestInfo,
entry: SigninCase,
): Promise<void> {
await seedAuthState(page, entry);
await page.goto(entry.path, { waitUntil: 'domcontentloaded' });
const slug = `${entry.path.slice(1).replace('/', '-')}-${entry.theme}`;
let paintedAtMs: number | null = null;
let previousElapsedMs = 0;
for (const elapsedMs of [500, 1000]) {
await page.waitForTimeout(elapsedMs - previousElapsedMs);
previousElapsedMs = elapsedMs;
const screenshot = await page.screenshot({
path: testInfo.outputPath(`${testInfo.project.name}-${slug}-${elapsedMs}ms.png`),
fullPage: true,
});
if (paintedAtMs === null && screenshotHasSigninPaint(screenshot)) {
paintedAtMs = elapsedMs;
}
}
expect(paintedAtMs).not.toBeNull();
expect(paintedAtMs ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual(1_000);
console.log(
`[userfront-e2e] ${testInfo.project.name} ${entry.path} ${entry.theme} signin surface painted at ${paintedAtMs}ms`,
);
}
function screenshotHasSigninPaint(buffer: Buffer): boolean {
const image = decodePng(buffer);
let sampled = 0;
@@ -204,16 +262,22 @@ function paeth(left: number, up: number, upLeft: number): number {
return upLeft;
}
async function seedAuthTheme(page: Page, theme: SigninCase['theme']): Promise<void> {
await page.addInitScript((themeValue) => {
async function seedAuthState(page: Page, entry: SigninCase): Promise<void> {
const localeCode = entry.path.slice(1, 3);
await page.addInitScript(({ themeValue, localeValue }) => {
window.localStorage.setItem('userfront_auth_theme', themeValue);
window.localStorage.setItem('flutter.userfront_auth_theme', themeValue);
}, theme);
window.localStorage.setItem('locale', localeValue);
window.localStorage.setItem('flutter.locale', localeValue);
}, { themeValue: entry.theme, localeValue: localeCode });
}
test.describe('UserFront signin runtime matrix', () => {
test.beforeEach(async ({ page }) => {
await mockPublicApis(page);
test.beforeEach(async ({ context }, testInfo) => {
await mockPublicApis(context);
if (testInfo.project.name !== 'webkit-desktop') {
await routeLightweightTestFonts(context);
}
});
test('first paint exposes bootstrap shell before Flutter renders', async ({
@@ -227,39 +291,17 @@ test.describe('UserFront signin runtime matrix', () => {
});
});
test('mobile signin paints final UI within 2 seconds with 0.5s captures', async ({
page,
}, testInfo) => {
test.skip(
!testInfo.project.name.includes('mobile'),
'mobile loading budget is verified on the mobile project',
);
await seedAuthTheme(page, 'light');
await page.goto('/ko/signin', { waitUntil: 'domcontentloaded' });
let paintedAtMs: number | null = null;
let previousElapsedMs = 0;
for (const elapsedMs of [500, 1000, 1500, 2000]) {
await page.waitForTimeout(elapsedMs - previousElapsedMs);
previousElapsedMs = elapsedMs;
const screenshot = await page.screenshot({
path: testInfo.outputPath(`mobile-ko-signin-${elapsedMs}ms.png`),
fullPage: true,
});
if (paintedAtMs === null && screenshotHasSigninPaint(screenshot)) {
paintedAtMs = elapsedMs;
}
}
expect(paintedAtMs).not.toBeNull();
expect(paintedAtMs ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual(2_000);
console.log(`[userfront-e2e] mobile signin painted at ${paintedAtMs}ms`);
});
for (const entry of signinCases) {
test(`${entry.path} ${entry.theme} paints sign-in surface within 1 second with 0.5s captures`, async ({
page,
}, testInfo) => {
await expectSigninSurfaceWithinBudget(page, testInfo, entry);
});
}
for (const entry of signinCases) {
test(`${entry.path} renders in ${entry.theme} theme`, async ({ page }) => {
await seedAuthTheme(page, entry.theme);
await seedAuthState(page, entry);
await page.goto(entry.path);
await expect(page).toHaveURL(new RegExp(`${entry.path}(?:\\?.*)?$`));
await expectFlutterCanvasRendered(page);
@@ -281,7 +323,7 @@ test.describe('UserFront signin runtime matrix', () => {
});
for (const entry of signinCases) {
await seedAuthTheme(page, entry.theme);
await seedAuthState(page, entry);
await page.goto(entry.path);
await expectFlutterCanvasRendered(page);
await expect
@@ -290,4 +332,32 @@ test.describe('UserFront signin runtime matrix', () => {
expect(requestedApiOrigins).not.toContain('https://sso.example.test');
}
});
test('Korean signin renders with test-only lightweight web font', async ({
context,
page,
}, testInfo) => {
if (testInfo.project.name === 'webkit-desktop') {
await routeLightweightTestFonts(context);
}
const requestedUrls: string[] = [];
page.on('request', (request) => {
requestedUrls.push(request.url());
});
await seedAuthState(page, { path: '/ko/signin', theme: 'light' });
await page.goto('/ko/signin', { waitUntil: 'domcontentloaded' });
await expectFlutterCanvasRendered(page, 10_000);
await page.screenshot({
path: testInfo.outputPath(`${testInfo.project.name}-ko-signin-korean-font.png`),
fullPage: true,
});
expect(requestedUrls).toContainEqual(
expect.stringContaining('https://fonts.gstatic.com/'),
);
expect(requestedUrls).not.toContainEqual(
expect.stringContaining('/assets/assets/fonts/NotoSansKR-Regular.ttf'),
);
});
});