forked from baron/baron-sso
모바일 로그인창 테스트 강화
This commit is contained in:
BIN
userfront-e2e/fixtures/fonts/NotoSansKR-TestSubset.woff2
Normal file
BIN
userfront-e2e/fixtures/fonts/NotoSansKR-TestSubset.woff2
Normal file
Binary file not shown.
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user