forked from baron/baron-sso
Merge branch 'dev' into bugfix/org
This commit is contained in:
1
.playwright-mcp/page-2026-05-26T09-02-03-101Z.yml
Normal file
1
.playwright-mcp/page-2026-05-26T09-02-03-101Z.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- generic [ref=e2]: Bad Gateway
|
||||||
File diff suppressed because one or more lines are too long
@@ -45,12 +45,22 @@ grep -Fq -- "--dart-define=CLIENT_LOG_DEBUG=" "$USERFRONT_DEV_SERVER" || fail "u
|
|||||||
grep -Fq -- "--dart-define=APP_ENV=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass app env through dart-define"
|
grep -Fq -- "--dart-define=APP_ENV=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass app env through dart-define"
|
||||||
grep -Fq -- "--dart-define=USERFRONT_URL=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass userfront URL through dart-define"
|
grep -Fq -- "--dart-define=USERFRONT_URL=" "$USERFRONT_DEV_SERVER" || fail "userfront dev server must pass userfront URL through dart-define"
|
||||||
grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must accept optional Flutter run flags"
|
grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must accept optional Flutter run flags"
|
||||||
|
grep -Fq -- 'USERFRONT_FLUTTER_RUN_FLAGS="${USERFRONT_FLUTTER_RUN_FLAGS:---debug}"' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must keep Flutter debug mode as the default"
|
||||||
|
grep -Fq -- 'warm_userfront_once' "$USERFRONT_DEV_SERVER" || fail "userfront dev server must run a one-shot boot warmup"
|
||||||
|
grep -Fq -- 'USERFRONT_BOOT_WARMUP_LOCALES' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must declare the language matrix"
|
||||||
|
grep -Fq -- 'USERFRONT_BOOT_WARMUP_VIEWPORTS' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must declare the viewport matrix"
|
||||||
|
grep -Fq -- 'Accept-Language:' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must GET each route with the locale header"
|
||||||
|
grep -Fq -- 'Viewport-Width:' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must GET each route with viewport width metadata"
|
||||||
|
grep -Fq -- '/signin' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must cover signin routes"
|
||||||
|
grep -Fq -- '/flutter_bootstrap.js' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must load the Flutter bootstrap entrypoint"
|
||||||
|
grep -Fq -- '/main.dart.mjs' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must load the wasm JS module entrypoint"
|
||||||
|
grep -Fq -- '/main.dart.wasm' "$USERFRONT_DEV_SERVER" || fail "userfront boot warmup must load the wasm module"
|
||||||
assert_contains 'CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}'
|
assert_contains 'CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}'
|
||||||
assert_contains 'BACKEND_URL=${BACKEND_URL:-}'
|
assert_contains 'BACKEND_URL=${BACKEND_URL:-}'
|
||||||
assert_contains 'USERFRONT_URL=${USERFRONT_URL}'
|
assert_contains 'USERFRONT_URL=${USERFRONT_URL}'
|
||||||
assert_contains 'USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-}'
|
assert_contains 'USERFRONT_FLUTTER_RUN_FLAGS=${USERFRONT_FLUTTER_RUN_FLAGS:-}'
|
||||||
if grep -Fq -- "--debug" "$USERFRONT_DEV_SERVER"; then
|
if grep -Fq -- "while true" "$USERFRONT_DEV_SERVER"; then
|
||||||
fail "make dev must not hard-code Flutter debug mode in the userfront dev server"
|
fail "userfront boot warmup must not run as a periodic health check"
|
||||||
fi
|
fi
|
||||||
if grep -Fq -- "--release" "$USERFRONT_DEV_SERVER"; then
|
if grep -Fq -- "--release" "$USERFRONT_DEV_SERVER"; then
|
||||||
fail "userfront dev server must not run Flutter in release mode"
|
fail "userfront dev server must not run Flutter in release mode"
|
||||||
|
|||||||
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,
|
fullyParallel: false,
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
retries: process.env.CI ? 2 : 0,
|
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',
|
reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'html',
|
||||||
use: {
|
use: {
|
||||||
baseURL,
|
baseURL,
|
||||||
@@ -23,6 +23,20 @@ export default defineConfig({
|
|||||||
locale: process.env.LOCALE ?? 'ko-KR',
|
locale: process.env.LOCALE ?? 'ko-KR',
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'webkit-desktop',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Safari'],
|
||||||
|
serviceWorkers: 'block',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'webkit-mobile-webapp',
|
||||||
|
use: {
|
||||||
|
...devices['iPhone 13'],
|
||||||
|
serviceWorkers: 'block',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'chromium-desktop',
|
name: 'chromium-desktop',
|
||||||
use: {
|
use: {
|
||||||
@@ -30,11 +44,18 @@ export default defineConfig({
|
|||||||
serviceWorkers: 'block',
|
serviceWorkers: 'block',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'firefox-desktop',
|
||||||
|
use: {
|
||||||
|
...devices['Desktop Firefox'],
|
||||||
|
serviceWorkers: 'block',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'chromium-mobile-webapp',
|
name: 'chromium-mobile-webapp',
|
||||||
use: {
|
use: {
|
||||||
...devices['Pixel 7'],
|
...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.wasm') &&
|
||||||
!path.endsWith('/main.dart.mjs') &&
|
!path.endsWith('/main.dart.mjs') &&
|
||||||
!path.endsWith('/skwasm.js') &&
|
!path.endsWith('/skwasm.js') &&
|
||||||
!path.endsWith('/skwasm.wasm') &&
|
!path.endsWith('/skwasm.wasm')
|
||||||
!path.endsWith('/assets/assets/fonts/NotoSansKR-Regular.ttf') &&
|
|
||||||
!path.endsWith('/assets/assets/fonts/NotoSansKR-Bold.ttf')
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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';
|
import { inflateSync } from 'node:zlib';
|
||||||
|
|
||||||
|
const lightweightTestFont = readFileSync(
|
||||||
|
new URL('../fixtures/fonts/NotoSansKR-TestSubset.woff2', import.meta.url),
|
||||||
|
);
|
||||||
|
|
||||||
type SigninCase = {
|
type SigninCase = {
|
||||||
path: '/ko/signin' | '/en/signin';
|
path: '/ko/signin' | '/en/signin';
|
||||||
theme: 'light' | 'dark';
|
theme: 'light' | 'dark';
|
||||||
@@ -13,8 +24,8 @@ const signinCases: SigninCase[] = [
|
|||||||
{ path: '/en/signin', theme: 'dark' },
|
{ path: '/en/signin', theme: 'dark' },
|
||||||
];
|
];
|
||||||
|
|
||||||
async function mockPublicApis(page: Page): Promise<void> {
|
async function mockPublicApis(context: BrowserContext): Promise<void> {
|
||||||
await page.route('**/api/v1/**', async (route) => {
|
await context.route(/\/api\/v1\//, async (route) => {
|
||||||
const requestUrl = new URL(route.request().url());
|
const requestUrl = new URL(route.request().url());
|
||||||
if (requestUrl.pathname.endsWith('/api/v1/user/me')) {
|
if (requestUrl.pathname.endsWith('/api/v1/user/me')) {
|
||||||
await route.fulfill({
|
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(
|
async function expectFlutterCanvasRendered(
|
||||||
page: Page,
|
page: Page,
|
||||||
timeoutMs = 5_000,
|
timeoutMs = 5_000,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
await expect(page.locator('#baron-bootstrap-shell')).toBeHidden({
|
||||||
|
timeout: timeoutMs,
|
||||||
|
});
|
||||||
await expect
|
await expect
|
||||||
.poll(() => page.screenshot().then(screenshotHasSigninPaint), {
|
.poll(() => page.screenshot().then(screenshotHasSigninPaint), {
|
||||||
timeout: timeoutMs,
|
timeout: timeoutMs,
|
||||||
@@ -59,6 +87,36 @@ async function expectBootstrapShellVisible(page: Page): Promise<void> {
|
|||||||
await expect(shell).toContainText(/Baron SW Portal/);
|
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 {
|
function screenshotHasSigninPaint(buffer: Buffer): boolean {
|
||||||
const image = decodePng(buffer);
|
const image = decodePng(buffer);
|
||||||
let sampled = 0;
|
let sampled = 0;
|
||||||
@@ -204,16 +262,22 @@ function paeth(left: number, up: number, upLeft: number): number {
|
|||||||
return upLeft;
|
return upLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedAuthTheme(page: Page, theme: SigninCase['theme']): Promise<void> {
|
async function seedAuthState(page: Page, entry: SigninCase): Promise<void> {
|
||||||
await page.addInitScript((themeValue) => {
|
const localeCode = entry.path.slice(1, 3);
|
||||||
|
await page.addInitScript(({ themeValue, localeValue }) => {
|
||||||
window.localStorage.setItem('userfront_auth_theme', themeValue);
|
window.localStorage.setItem('userfront_auth_theme', themeValue);
|
||||||
window.localStorage.setItem('flutter.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.describe('UserFront signin runtime matrix', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ context }, testInfo) => {
|
||||||
await mockPublicApis(page);
|
await mockPublicApis(context);
|
||||||
|
if (testInfo.project.name !== 'webkit-desktop') {
|
||||||
|
await routeLightweightTestFonts(context);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('first paint exposes bootstrap shell before Flutter renders', async ({
|
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 ({
|
for (const entry of signinCases) {
|
||||||
page,
|
test(`${entry.path} ${entry.theme} paints sign-in surface within 1 second with 0.5s captures`, async ({
|
||||||
}, testInfo) => {
|
page,
|
||||||
test.skip(
|
}, testInfo) => {
|
||||||
!testInfo.project.name.includes('mobile'),
|
await expectSigninSurfaceWithinBudget(page, testInfo, entry);
|
||||||
'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) {
|
for (const entry of signinCases) {
|
||||||
test(`${entry.path} renders in ${entry.theme} theme`, async ({ page }) => {
|
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 page.goto(entry.path);
|
||||||
await expect(page).toHaveURL(new RegExp(`${entry.path}(?:\\?.*)?$`));
|
await expect(page).toHaveURL(new RegExp(`${entry.path}(?:\\?.*)?$`));
|
||||||
await expectFlutterCanvasRendered(page);
|
await expectFlutterCanvasRendered(page);
|
||||||
@@ -281,7 +323,7 @@ test.describe('UserFront signin runtime matrix', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const entry of signinCases) {
|
for (const entry of signinCases) {
|
||||||
await seedAuthTheme(page, entry.theme);
|
await seedAuthState(page, entry);
|
||||||
await page.goto(entry.path);
|
await page.goto(entry.path);
|
||||||
await expectFlutterCanvasRendered(page);
|
await expectFlutterCanvasRendered(page);
|
||||||
await expect
|
await expect
|
||||||
@@ -290,4 +332,32 @@ test.describe('UserFront signin runtime matrix', () => {
|
|||||||
expect(requestedApiOrigins).not.toContain('https://sso.example.test');
|
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'),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -10,45 +10,37 @@ class TomlAssetLoader extends AssetLoader {
|
|||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> load(String path, Locale locale) async {
|
Future<Map<String, dynamic>> load(String path, Locale locale) async {
|
||||||
final languageCode = locale.languageCode.toLowerCase();
|
final languageCode = locale.languageCode.toLowerCase();
|
||||||
final source = switch (languageCode) {
|
return switch (languageCode) {
|
||||||
'ko' => koStrings,
|
'ko' => _normalizedKoStrings,
|
||||||
'en' => enStrings,
|
'en' => _normalizedEnStrings,
|
||||||
_ => enStrings,
|
_ => _normalizedEnStrings,
|
||||||
};
|
};
|
||||||
return _expandFlatTranslations(source);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _expandFlatTranslations(Map<String, String> flatMap) {
|
final Map<String, dynamic> _normalizedKoStrings = _normalizeFlatTranslations(
|
||||||
final nested = <String, dynamic>{};
|
koStrings,
|
||||||
for (final entry in flatMap.entries) {
|
);
|
||||||
final key = entry.key;
|
final Map<String, dynamic> _normalizedEnStrings = _normalizeFlatTranslations(
|
||||||
if (key.isEmpty) {
|
enStrings,
|
||||||
continue;
|
);
|
||||||
}
|
|
||||||
final segments = key.split('.');
|
Map<String, dynamic> _normalizeFlatTranslations(Map<String, String> flatMap) =>
|
||||||
Map<String, dynamic> cursor = nested;
|
Map.fromEntries(
|
||||||
for (var index = 0; index < segments.length; index++) {
|
flatMap.entries
|
||||||
final segment = segments[index];
|
.where((entry) => _isUserfrontTranslationKey(entry.key))
|
||||||
if (segment.isEmpty) {
|
.map(
|
||||||
continue;
|
(entry) =>
|
||||||
}
|
MapEntry(entry.key, _normalizeLocalizationValue(entry.value)),
|
||||||
final isLeaf = index == segments.length - 1;
|
),
|
||||||
if (isLeaf) {
|
);
|
||||||
cursor[segment] = _normalizeLocalizationValue(entry.value);
|
|
||||||
continue;
|
bool _isUserfrontTranslationKey(String key) {
|
||||||
}
|
return key.startsWith('domain.') ||
|
||||||
final next = cursor.putIfAbsent(segment, () => <String, dynamic>{});
|
key.startsWith('err.userfront.') ||
|
||||||
if (next is Map<String, dynamic>) {
|
key.startsWith('msg.userfront.') ||
|
||||||
cursor = next;
|
key.startsWith('ui.userfront.') ||
|
||||||
continue;
|
key.startsWith('ui.common.');
|
||||||
}
|
|
||||||
final replacement = <String, dynamic>{};
|
|
||||||
cursor[segment] = replacement;
|
|
||||||
cursor = replacement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nested;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String _normalizeLocalizationValue(String value) {
|
String _normalizeLocalizationValue(String value) {
|
||||||
|
|||||||
@@ -52,14 +52,11 @@ class LogPolicy {
|
|||||||
required String? appEnv,
|
required String? appEnv,
|
||||||
required String? productionDebugFlag,
|
required String? productionDebugFlag,
|
||||||
}) {
|
}) {
|
||||||
final flag = parseOptionalBoolFlag(productionDebugFlag);
|
|
||||||
if (flag.specified) {
|
|
||||||
return flag.enabled;
|
|
||||||
}
|
|
||||||
if (!isProductionEnv(appEnv)) {
|
if (!isProductionEnv(appEnv)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
final flag = parseOptionalBoolFlag(productionDebugFlag);
|
||||||
|
return flag.specified && flag.enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool shouldRelayClientLog({
|
static bool shouldRelayClientLog({
|
||||||
@@ -67,10 +64,12 @@ class LogPolicy {
|
|||||||
required String? appEnv,
|
required String? appEnv,
|
||||||
required String? productionDebugFlag,
|
required String? productionDebugFlag,
|
||||||
}) {
|
}) {
|
||||||
if (debugEnabled(
|
final flag = parseOptionalBoolFlag(productionDebugFlag);
|
||||||
appEnv: appEnv,
|
final debugRelayEnabled = isProductionEnv(appEnv)
|
||||||
productionDebugFlag: productionDebugFlag,
|
? flag.specified && flag.enabled
|
||||||
)) {
|
: !(flag.specified && !flag.enabled);
|
||||||
|
|
||||||
|
if (debugRelayEnabled) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final normalized = level.trim().toUpperCase();
|
final normalized = level.trim().toUpperCase();
|
||||||
|
|||||||
@@ -1781,7 +1781,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
style: theme.textTheme.headlineMedium?.copyWith(
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
fontSize: 34,
|
fontSize: 34,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
letterSpacing: -0.7,
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -45,10 +45,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
cli_config:
|
cli_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -320,18 +320,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -653,26 +653,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
|
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.26.3"
|
version: "1.30.0"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.10"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
|
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.12"
|
version: "0.6.16"
|
||||||
toml:
|
toml:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -5,16 +5,98 @@ cd /workspace
|
|||||||
/bin/sh ./scripts/sync_userfront_locales.sh
|
/bin/sh ./scripts/sync_userfront_locales.sh
|
||||||
|
|
||||||
cd /workspace/userfront
|
cd /workspace/userfront
|
||||||
|
USERFRONT_INTERNAL_PORT="${USERFRONT_INTERNAL_PORT:-5000}"
|
||||||
|
USERFRONT_FLUTTER_RUN_FLAGS="${USERFRONT_FLUTTER_RUN_FLAGS:---debug}"
|
||||||
|
USERFRONT_BOOT_WARMUP_ATTEMPTS="${USERFRONT_BOOT_WARMUP_ATTEMPTS:-120}"
|
||||||
|
USERFRONT_BOOT_WARMUP_INTERVAL_SECONDS="${USERFRONT_BOOT_WARMUP_INTERVAL_SECONDS:-0.5}"
|
||||||
|
USERFRONT_BOOT_WARMUP_LOCALES="${USERFRONT_BOOT_WARMUP_LOCALES:-ko en}"
|
||||||
|
USERFRONT_BOOT_WARMUP_VIEWPORTS="${USERFRONT_BOOT_WARMUP_VIEWPORTS:-mobile:390 desktop:1440}"
|
||||||
|
|
||||||
|
warm_get() {
|
||||||
|
path="$1"
|
||||||
|
locale="$2"
|
||||||
|
viewport="$3"
|
||||||
|
width="${viewport#*:}"
|
||||||
|
if [ "$width" = "$viewport" ]; then
|
||||||
|
width=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget -qO- \
|
||||||
|
--header="Accept-Language: $locale" \
|
||||||
|
--header="Viewport-Width: $width" \
|
||||||
|
"http://127.0.0.1:${USERFRONT_INTERNAL_PORT}${path}" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
warm_userfront_once() {
|
||||||
|
flutter_pid="$1"
|
||||||
|
attempt=1
|
||||||
|
started_at="$(date +%s)"
|
||||||
|
|
||||||
|
while [ "$attempt" -le "$USERFRONT_BOOT_WARMUP_ATTEMPTS" ]; do
|
||||||
|
if wget -qO- "http://127.0.0.1:${USERFRONT_INTERNAL_PORT}/flutter_bootstrap.js" >/dev/null 2>&1; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! kill -0 "$flutter_pid" 2>/dev/null; then
|
||||||
|
echo "[userfront-boot] warmup skipped because flutter exited before readiness" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep "$USERFRONT_BOOT_WARMUP_INTERVAL_SECONDS"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$attempt" -gt "$USERFRONT_BOOT_WARMUP_ATTEMPTS" ]; then
|
||||||
|
echo "[userfront-boot] warmup skipped after ${USERFRONT_BOOT_WARMUP_ATTEMPTS} readiness attempts" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[userfront-boot] one-shot warmup starting locales=\"${USERFRONT_BOOT_WARMUP_LOCALES}\" viewports=\"${USERFRONT_BOOT_WARMUP_VIEWPORTS}\"" >&2
|
||||||
|
|
||||||
|
for locale in $USERFRONT_BOOT_WARMUP_LOCALES; do
|
||||||
|
for viewport in $USERFRONT_BOOT_WARMUP_VIEWPORTS; do
|
||||||
|
warm_get "/${locale}/signin" "$locale" "$viewport" || true
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
for asset in \
|
||||||
|
/ \
|
||||||
|
/flutter_bootstrap.js \
|
||||||
|
/main.dart.mjs \
|
||||||
|
/main.dart.wasm \
|
||||||
|
/canvaskit/skwasm.js \
|
||||||
|
/canvaskit/skwasm.wasm \
|
||||||
|
/canvaskit/skwasm_heavy.js \
|
||||||
|
/canvaskit/skwasm_heavy.wasm \
|
||||||
|
/assets/AssetManifest.bin.json \
|
||||||
|
/assets/FontManifest.json
|
||||||
|
do
|
||||||
|
wget -qO- "http://127.0.0.1:${USERFRONT_INTERNAL_PORT}${asset}" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
finished_at="$(date +%s)"
|
||||||
|
elapsed_seconds=$((finished_at - started_at))
|
||||||
|
echo "[userfront-boot] one-shot warmup completed in ${elapsed_seconds}s" >&2
|
||||||
|
}
|
||||||
|
|
||||||
set -- flutter run \
|
set -- flutter run \
|
||||||
-d web-server \
|
-d web-server \
|
||||||
--web-hostname 0.0.0.0 \
|
--web-hostname 0.0.0.0 \
|
||||||
--web-port "${USERFRONT_INTERNAL_PORT:-5000}" \
|
--web-port "${USERFRONT_INTERNAL_PORT}" \
|
||||||
--wasm \
|
--wasm \
|
||||||
--dart-define=BACKEND_URL="${BACKEND_URL:-}" \
|
--dart-define=BACKEND_URL="${BACKEND_URL:-}" \
|
||||||
--dart-define=CLIENT_LOG_DEBUG="${CLIENT_LOG_DEBUG:-false}" \
|
--dart-define=CLIENT_LOG_DEBUG="${CLIENT_LOG_DEBUG:-false}" \
|
||||||
--dart-define=APP_ENV="${APP_ENV:-dev}" \
|
--dart-define=APP_ENV="${APP_ENV:-dev}" \
|
||||||
--dart-define=USERFRONT_URL="${USERFRONT_URL:-}" \
|
--dart-define=USERFRONT_URL="${USERFRONT_URL:-}" \
|
||||||
${USERFRONT_FLUTTER_RUN_FLAGS:-} \
|
${USERFRONT_FLUTTER_RUN_FLAGS} \
|
||||||
--no-web-resources-cdn
|
--no-web-resources-cdn
|
||||||
|
|
||||||
exec "$@"
|
"$@" &
|
||||||
|
flutter_pid="$!"
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
kill "$flutter_pid" 2>/dev/null || true
|
||||||
|
wait "$flutter_pid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
trap terminate INT TERM
|
||||||
|
warm_userfront_once "$flutter_pid"
|
||||||
|
wait "$flutter_pid"
|
||||||
|
|||||||
@@ -18,17 +18,6 @@ const buildDir = process.argv[2] ?? join(__dirname, '..', 'build', 'web');
|
|||||||
const bootstrapPath = join(buildDir, 'flutter_bootstrap.js');
|
const bootstrapPath = join(buildDir, 'flutter_bootstrap.js');
|
||||||
const indexPath = join(buildDir, 'index.html');
|
const indexPath = join(buildDir, 'index.html');
|
||||||
const hashableEntrypoints = ['main.dart.js', 'main.dart.mjs', 'main.dart.wasm'];
|
const hashableEntrypoints = ['main.dart.js', 'main.dart.mjs', 'main.dart.wasm'];
|
||||||
const loginFontFallbackPreloads = [
|
|
||||||
'https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Me4GZLCzYlKw.woff2',
|
|
||||||
'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.110.woff2',
|
|
||||||
'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.113.woff2',
|
|
||||||
'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.114.woff2',
|
|
||||||
'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.115.woff2',
|
|
||||||
'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.116.woff2',
|
|
||||||
'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.117.woff2',
|
|
||||||
'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.118.woff2',
|
|
||||||
'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.119.woff2',
|
|
||||||
];
|
|
||||||
const compressibleExtensions = new Set([
|
const compressibleExtensions = new Set([
|
||||||
'.css',
|
'.css',
|
||||||
'.html',
|
'.html',
|
||||||
@@ -116,6 +105,9 @@ if (existsSync(indexPath)) {
|
|||||||
let index = readFileSync(indexPath, 'utf8');
|
let index = readFileSync(indexPath, 'utf8');
|
||||||
const preloadLinks = [
|
const preloadLinks = [
|
||||||
'<link rel="preload" href="flutter_bootstrap.js" as="script" />',
|
'<link rel="preload" href="flutter_bootstrap.js" as="script" />',
|
||||||
|
hashedEntrypoints.has('main.dart.js')
|
||||||
|
? `<link rel="preload" href="${hashedEntrypoints.get('main.dart.js')}" as="script" />`
|
||||||
|
: '',
|
||||||
hashedEntrypoints.has('main.dart.mjs')
|
hashedEntrypoints.has('main.dart.mjs')
|
||||||
? `<link rel="modulepreload" href="${hashedEntrypoints.get('main.dart.mjs')}" />`
|
? `<link rel="modulepreload" href="${hashedEntrypoints.get('main.dart.mjs')}" />`
|
||||||
: '',
|
: '',
|
||||||
@@ -124,22 +116,22 @@ if (existsSync(indexPath)) {
|
|||||||
: '',
|
: '',
|
||||||
'<link rel="modulepreload" href="canvaskit/skwasm.js" />',
|
'<link rel="modulepreload" href="canvaskit/skwasm.js" />',
|
||||||
'<link rel="preload" href="canvaskit/skwasm.wasm" as="fetch" type="application/wasm" crossorigin />',
|
'<link rel="preload" href="canvaskit/skwasm.wasm" as="fetch" type="application/wasm" crossorigin />',
|
||||||
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />',
|
|
||||||
...loginFontFallbackPreloads.map(
|
|
||||||
(href) =>
|
|
||||||
`<link rel="preload" href="${href}" as="fetch" type="font/woff2" crossorigin />`,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n ');
|
.join('\n ');
|
||||||
|
|
||||||
index = index
|
index = index
|
||||||
.replace(/\n\s*<link rel="preload" href="flutter_bootstrap\.js" as="script" \/>/g, '')
|
.replace(/\n\s*<link rel="preload" href="flutter_bootstrap\.js" as="script" \/>/g, '')
|
||||||
|
.replace(/\n\s*<link rel="preload" href="main\.dart\.[^"]+\.js" as="script" \/>/g, '')
|
||||||
.replace(/\n\s*<link rel="modulepreload" href="main\.dart\.[^"]+\.mjs" \/>/g, '')
|
.replace(/\n\s*<link rel="modulepreload" href="main\.dart\.[^"]+\.mjs" \/>/g, '')
|
||||||
.replace(
|
.replace(
|
||||||
/\n\s*<link rel="preload" href="main\.dart\.[^"]+\.wasm" as="fetch" type="application\/wasm" crossorigin \/>/g,
|
/\n\s*<link rel="preload" href="main\.dart\.[^"]+\.wasm" as="fetch" type="application\/wasm" crossorigin \/>/g,
|
||||||
'',
|
'',
|
||||||
)
|
)
|
||||||
|
.replace(
|
||||||
|
/\n\s*<link rel="preload" href="assets\/assets\/translations\/(?:en|ko)\.toml" as="fetch" crossorigin \/>/g,
|
||||||
|
'',
|
||||||
|
)
|
||||||
.replace(/\n\s*<link rel="modulepreload" href="canvaskit\/skwasm\.js" \/>/g, '')
|
.replace(/\n\s*<link rel="modulepreload" href="canvaskit\/skwasm\.js" \/>/g, '')
|
||||||
.replace(
|
.replace(
|
||||||
/\n\s*<link rel="preload" href="canvaskit\/skwasm\.wasm" as="fetch" type="application\/wasm" crossorigin \/>/g,
|
/\n\s*<link rel="preload" href="canvaskit\/skwasm\.wasm" as="fetch" type="application\/wasm" crossorigin \/>/g,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('explicit debug flag applies in development-like environment', () {
|
test('explicit true enables debug in development-like environment', () {
|
||||||
expect(
|
expect(
|
||||||
LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'true'),
|
LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'true'),
|
||||||
isTrue,
|
isTrue,
|
||||||
@@ -23,13 +23,16 @@ void main() {
|
|||||||
LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '1'),
|
LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '1'),
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('explicit false does not suppress local debug in development', () {
|
||||||
expect(
|
expect(
|
||||||
LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'false'),
|
LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'false'),
|
||||||
isFalse,
|
isTrue,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '0'),
|
LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '0'),
|
||||||
isFalse,
|
isTrue,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
54
userfront/test/toml_asset_loader_test.dart
Normal file
54
userfront/test/toml_asset_loader_test.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:userfront/core/i18n/toml_asset_loader.dart';
|
||||||
|
import 'package:userfront/i18n_data.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('TomlAssetLoader keeps flat keys for fast startup lookup', () async {
|
||||||
|
const loader = TomlAssetLoader();
|
||||||
|
|
||||||
|
final translations = await loader.load(
|
||||||
|
'assets/translations',
|
||||||
|
const Locale('en'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(translations['domain.company.baron'], 'Baron');
|
||||||
|
expect(translations['domain'], isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('English signup policy copy stays small enough for first render', () {
|
||||||
|
const sensitiveKeys = [
|
||||||
|
'msg.userfront.signup.privacy_full',
|
||||||
|
'msg.userfront.signup.tos_full',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final key in sensitiveKeys) {
|
||||||
|
final value = enStrings[key];
|
||||||
|
expect(value, isNotNull, reason: key);
|
||||||
|
expect(value!.length, lessThan(1024), reason: key);
|
||||||
|
expect(value.contains(r'\\\\'), isFalse, reason: key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'TomlAssetLoader excludes non-userfront dictionaries at startup',
|
||||||
|
() async {
|
||||||
|
const loader = TomlAssetLoader();
|
||||||
|
|
||||||
|
final translations = await loader.load(
|
||||||
|
'assets/translations',
|
||||||
|
const Locale('en'),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(translations['ui.admin.nav.api_keys'], isNull);
|
||||||
|
expect(translations['ui.dev.console_title'], isNull);
|
||||||
|
expect(
|
||||||
|
translations['err.userfront.auth_proxy.login_failed'],
|
||||||
|
'Login failed.',
|
||||||
|
);
|
||||||
|
expect(translations['ui.userfront.login.action.submit'], 'Sign in');
|
||||||
|
expect(translations['ui.common.theme_light'], 'Light');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 18px;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
@@ -83,6 +83,20 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#baron-bootstrap-shell .signin-preview {
|
||||||
|
align-items: center;
|
||||||
|
background: #1d4ed8;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
font-size: clamp(15px, 3vw, 18px);
|
||||||
|
font-weight: 700;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 48px;
|
||||||
|
min-width: min(280px, 80vw);
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
#baron-bootstrap-shell .loader {
|
#baron-bootstrap-shell .loader {
|
||||||
animation: baron-spin 880ms linear infinite;
|
animation: baron-spin 880ms linear infinite;
|
||||||
border: 5px solid #cbd5e1;
|
border: 5px solid #cbd5e1;
|
||||||
@@ -104,15 +118,81 @@
|
|||||||
<h1>Baron SW Portal</h1>
|
<h1>Baron SW Portal</h1>
|
||||||
<div class="loader" aria-hidden="true"></div>
|
<div class="loader" aria-hidden="true"></div>
|
||||||
<p>Loading sign-in</p>
|
<p>Loading sign-in</p>
|
||||||
|
<div class="signin-preview" aria-hidden="true">Sign in</div>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
|
var baronBootstrapStartedAt = performance.now();
|
||||||
|
var baronMinimumShellMs = 0;
|
||||||
window.addEventListener("flutter-first-frame", function () {
|
window.addEventListener("flutter-first-frame", function () {
|
||||||
document.body.classList.add("flutter-ready");
|
var elapsedMs = performance.now() - baronBootstrapStartedAt;
|
||||||
window.setTimeout(function () {
|
window.setTimeout(
|
||||||
document.getElementById("baron-bootstrap-shell")?.remove();
|
function () {
|
||||||
}, 220);
|
document.body.classList.add("flutter-ready");
|
||||||
|
window.setTimeout(function () {
|
||||||
|
document.getElementById("baron-bootstrap-shell")?.remove();
|
||||||
|
}, 220);
|
||||||
|
},
|
||||||
|
Math.max(0, baronMinimumShellMs - elapsedMs),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="flutter_bootstrap.js" async></script>
|
<script>
|
||||||
|
(function () {
|
||||||
|
function loadFlutter() {
|
||||||
|
var script = document.createElement("script");
|
||||||
|
script.src = "flutter_bootstrap.js";
|
||||||
|
script.async = true;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostname = window.location.hostname;
|
||||||
|
var isLocalhost =
|
||||||
|
hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
||||||
|
if (!isLocalhost || !("serviceWorker" in navigator)) {
|
||||||
|
loadFlutter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.serviceWorker
|
||||||
|
.getRegistrations()
|
||||||
|
.then(function (registrations) {
|
||||||
|
return Promise.all(
|
||||||
|
registrations.map(function (registration) {
|
||||||
|
return registration.unregister();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
if (!window.caches) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return caches.keys().then(function (keys) {
|
||||||
|
return Promise.all(
|
||||||
|
keys
|
||||||
|
.filter(function (key) {
|
||||||
|
return (
|
||||||
|
key.indexOf("baron-userfront-") === 0 ||
|
||||||
|
key.indexOf("flutter-app-cache") === 0
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(function (key) {
|
||||||
|
return caches.delete(key);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadFlutter();
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.warn("[baron] failed to clear local service worker", error);
|
||||||
|
loadFlutter();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user