release: bump version to 0.74.0
This commit is contained in:
@@ -142,6 +142,12 @@ def iter_frontend_src_files() -> list[Path]:
|
||||
]
|
||||
|
||||
|
||||
def iter_style_family_css_files() -> list[Path]:
|
||||
files = sorted((root / "frontend/src").rglob("*.css"))
|
||||
files.extend(sorted((root / "frontend/public/earth/css").glob("*.css")))
|
||||
return files
|
||||
|
||||
|
||||
def collect_inline_style_context(lines: list[str], start_index: int) -> str:
|
||||
block = []
|
||||
for line in lines[start_index:start_index + 8]:
|
||||
@@ -255,6 +261,48 @@ def css_has_declaration(body: str, prop: str, value_pattern: str) -> bool:
|
||||
return re.search(rf"(^|[;\n])\s*{re.escape(prop)}\s*:\s*{value_pattern}\s*;", body) is not None
|
||||
|
||||
|
||||
STYLE_FAMILY_OWNER_FILES = {
|
||||
"frontend/src/admin/styles.css",
|
||||
"frontend/src/components/tactile-ui/styles.css",
|
||||
"frontend/src/pages/Docs/Docs.css",
|
||||
"frontend/public/earth/css/base.css",
|
||||
"frontend/public/earth/css/coordinates-display.css",
|
||||
"frontend/public/earth/css/earth-stats.css",
|
||||
"frontend/public/earth/css/hud.css",
|
||||
"frontend/public/earth/css/info-panel.css",
|
||||
"frontend/public/earth/css/layer-panel.css",
|
||||
"frontend/public/earth/css/legend.css",
|
||||
"frontend/public/earth/css/news-panel.css",
|
||||
"frontend/public/earth/css/toolbar.css",
|
||||
"frontend/public/earth/css/tv-panel.css",
|
||||
}
|
||||
|
||||
STYLE_FAMILY_PATTERN = re.compile(r"\b(?:badge|chip|pill|tag|status)\b", re.I)
|
||||
|
||||
|
||||
def style_family_selector(selector: str) -> str | None:
|
||||
for class_name in re.findall(r"\.([A-Za-z][A-Za-z0-9_-]*)", selector):
|
||||
if STYLE_FAMILY_PATTERN.search(class_name):
|
||||
return class_name
|
||||
return None
|
||||
|
||||
|
||||
def check_same_category_style_ownership() -> None:
|
||||
for path in iter_style_family_css_files():
|
||||
rel = str(path.relative_to(root))
|
||||
if rel in STYLE_FAMILY_OWNER_FILES:
|
||||
continue
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for line_no, selector, _body in iter_css_blocks(text):
|
||||
class_name = style_family_selector(selector)
|
||||
if class_name is None:
|
||||
continue
|
||||
warn(
|
||||
f"{rel}:{line_no}: same-category {class_name!r} styles should reuse "
|
||||
"an existing shared style owner instead of introducing a page-local visual variant"
|
||||
)
|
||||
|
||||
|
||||
def check_admin_shell_height_chain() -> None:
|
||||
text = read_text("frontend/src/admin/styles.css")
|
||||
required: dict[str, dict[str, str]] = {
|
||||
@@ -324,6 +372,160 @@ def check_debug_output() -> None:
|
||||
fail(f"{rel}:{line_no}: console output must not include token material")
|
||||
|
||||
|
||||
EARTH_RUNTIME_COPY_FILES = (
|
||||
"frontend/public/earth/js/main.js",
|
||||
"frontend/public/earth/js/controls.js",
|
||||
"frontend/public/earth/js/cables.js",
|
||||
"frontend/public/earth/js/news.js",
|
||||
"frontend/public/earth/js/tv.js",
|
||||
"frontend/public/earth/js/info-card.js",
|
||||
"frontend/public/earth/js/layer-startup-tasks.js",
|
||||
"frontend/public/earth/js/i18n.js",
|
||||
)
|
||||
|
||||
EARTH_RUNTIME_COPY_ENTRYPOINTS = (
|
||||
"showStatusMessage",
|
||||
"queueStatusMessage",
|
||||
"showGestureStatusMessage",
|
||||
"showError",
|
||||
"setLoadingMessage",
|
||||
"resolveStartupMessage",
|
||||
)
|
||||
|
||||
CJK_PATTERN = re.compile(r"[\u3400-\u9fff]")
|
||||
|
||||
|
||||
def collect_balanced_js(text: str, start_index: int, opener: str, closer: str) -> str:
|
||||
depth = 0
|
||||
quote: str | None = None
|
||||
template_depth = 0
|
||||
cursor = start_index
|
||||
while cursor < len(text):
|
||||
char = text[cursor]
|
||||
if quote:
|
||||
if char == "\\":
|
||||
cursor += 2
|
||||
continue
|
||||
if quote == "`" and char == "$" and cursor + 1 < len(text) and text[cursor + 1] == "{":
|
||||
template_depth += 1
|
||||
cursor += 2
|
||||
continue
|
||||
if template_depth > 0 and char == "}":
|
||||
template_depth -= 1
|
||||
cursor += 1
|
||||
continue
|
||||
if char == quote and template_depth == 0:
|
||||
quote = None
|
||||
cursor += 1
|
||||
continue
|
||||
|
||||
if char in ("'", '"', "`"):
|
||||
quote = char
|
||||
elif char == opener:
|
||||
depth += 1
|
||||
elif char == closer:
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return text[start_index:cursor + 1]
|
||||
cursor += 1
|
||||
return text[start_index:]
|
||||
|
||||
|
||||
def collect_js_call(text: str, start_index: int) -> str:
|
||||
open_index = text.find("(", start_index)
|
||||
if open_index == -1:
|
||||
return text[start_index:]
|
||||
return collect_balanced_js(text, open_index, "(", ")")
|
||||
|
||||
|
||||
def collect_js_value(text: str, start_index: int) -> str:
|
||||
cursor = start_index
|
||||
while cursor < len(text) and text[cursor].isspace():
|
||||
cursor += 1
|
||||
if cursor >= len(text):
|
||||
return ""
|
||||
char = text[cursor]
|
||||
if char == "{":
|
||||
return collect_balanced_js(text, cursor, "{", "}")
|
||||
if char == "[":
|
||||
return collect_balanced_js(text, cursor, "[", "]")
|
||||
if text.startswith("earthMessage", cursor):
|
||||
open_index = text.find("(", cursor)
|
||||
if open_index == -1:
|
||||
return text[cursor:]
|
||||
return text[cursor:open_index] + collect_balanced_js(text, open_index, "(", ")")
|
||||
if char in ("'", '"', "`"):
|
||||
quote = char
|
||||
cursor += 1
|
||||
while cursor < len(text):
|
||||
if text[cursor] == "\\":
|
||||
cursor += 2
|
||||
continue
|
||||
if text[cursor] == quote:
|
||||
return text[start_index:cursor + 1]
|
||||
cursor += 1
|
||||
return text[start_index:]
|
||||
|
||||
end_candidates = [
|
||||
index for index in (text.find(",", cursor), text.find("\n", cursor)) if index != -1
|
||||
]
|
||||
end_index = min(end_candidates) if end_candidates else len(text)
|
||||
return text[start_index:end_index]
|
||||
|
||||
|
||||
def check_earth_runtime_copy_entrypoints() -> None:
|
||||
i18n_text = read_text("frontend/public/earth/js/i18n.js")
|
||||
for token in ("EARTH_MESSAGE_TEMPLATES", "export function earthMessage", "export function formatEarthMessage"):
|
||||
if token not in i18n_text:
|
||||
fail(f"frontend/public/earth/js/i18n.js: missing centralized Earth runtime copy token {token}")
|
||||
|
||||
direct_literal_pattern = re.compile(r"^\(\s*['\"`]")
|
||||
for file_path in EARTH_RUNTIME_COPY_FILES:
|
||||
text = read_text(file_path)
|
||||
for entrypoint in EARTH_RUNTIME_COPY_ENTRYPOINTS:
|
||||
pattern = re.compile(rf"\b{re.escape(entrypoint)}\s*\(")
|
||||
for match in pattern.finditer(text):
|
||||
call = collect_js_call(text, match.start())
|
||||
if direct_literal_pattern.search(call):
|
||||
fail(
|
||||
f"{file_path}:{jsx_line_number(text, match.start())}: "
|
||||
f"Earth runtime copy entrypoint {entrypoint} must use earthMessage(...), not a direct string literal"
|
||||
)
|
||||
if CJK_PATTERN.search(call) and "earthMessage(" not in call:
|
||||
fail(
|
||||
f"{file_path}:{jsx_line_number(text, match.start())}: "
|
||||
f"Earth runtime copy entrypoint {entrypoint} contains CJK without earthMessage(...)"
|
||||
)
|
||||
|
||||
for match in re.finditer(r"\bstartupMessage\s*:\s*", text):
|
||||
value = collect_js_value(text, match.end()).strip()
|
||||
if value in {'""', "''", "``"}:
|
||||
continue
|
||||
if "earthMessage(" not in value:
|
||||
fail(
|
||||
f"{file_path}:{jsx_line_number(text, match.start())}: "
|
||||
"Earth layer startupMessage must use earthMessage(...) so startup copy has one i18n entrypoint"
|
||||
)
|
||||
elif CJK_PATTERN.search(value) and "earthMessage(" not in value:
|
||||
fail(
|
||||
f"{file_path}:{jsx_line_number(text, match.start())}: "
|
||||
"Earth layer startupMessage contains CJK outside the centralized runtime copy map"
|
||||
)
|
||||
|
||||
for match in re.finditer(r"new\s+CustomEvent\(\s*['\"]earth:status['\"]", text):
|
||||
call = collect_js_call(text, match.start())
|
||||
if re.search(r"\bmessage\s*:\s*['\"`]", call):
|
||||
fail(
|
||||
f"{file_path}:{jsx_line_number(text, match.start())}: "
|
||||
"earth:status event messages must use earthMessage(...) instead of direct strings"
|
||||
)
|
||||
if CJK_PATTERN.search(call) and "earthMessage(" not in call:
|
||||
fail(
|
||||
f"{file_path}:{jsx_line_number(text, match.start())}: "
|
||||
"earth:status event contains CJK without earthMessage(...)"
|
||||
)
|
||||
|
||||
|
||||
def jsx_line_number(text: str, index: int) -> int:
|
||||
return text.count("\n", 0, index) + 1
|
||||
|
||||
@@ -512,12 +714,14 @@ def main() -> None:
|
||||
check_literal_internal_links()
|
||||
check_admin_search_route_targets()
|
||||
check_debug_output()
|
||||
check_earth_runtime_copy_entrypoints()
|
||||
check_native_button_safety()
|
||||
check_icon_button_accessibility()
|
||||
check_no_nested_cards()
|
||||
check_no_antd_layout_primitives()
|
||||
check_connection_test_input_pattern()
|
||||
check_admin_shell_height_chain()
|
||||
check_same_category_style_ownership()
|
||||
check_uiux_static_warnings()
|
||||
|
||||
for message in warnings:
|
||||
|
||||
@@ -22,6 +22,15 @@ const publicRoutes = [
|
||||
{ path: '/docs/manual', text: '智能星球使用手册' },
|
||||
{ path: '/docs/quickstart', text: '快速开始' },
|
||||
]
|
||||
const publicEnglishI18nRoutes = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/verify-email',
|
||||
'/forgot-password',
|
||||
'/docs',
|
||||
'/docs/manual',
|
||||
'/docs/quickstart',
|
||||
]
|
||||
|
||||
const desktopViewport = { width: 1440, height: 900 }
|
||||
const mobileViewport = { width: 390, height: 844 }
|
||||
@@ -39,9 +48,13 @@ const interactionChecks = [
|
||||
'logs raw tab',
|
||||
'users add dialog',
|
||||
'data distribution toggle',
|
||||
'earth i18n settings',
|
||||
'earth news source test',
|
||||
'earth news source draft cancel',
|
||||
'earth news group create',
|
||||
'public english i18n sweep',
|
||||
'english login backend error i18n',
|
||||
'admin english i18n sweep',
|
||||
'authenticated unknown route fallback',
|
||||
]
|
||||
const authInteractionChecks = [
|
||||
@@ -49,6 +62,22 @@ const authInteractionChecks = [
|
||||
'forgot password reset',
|
||||
'standalone verify email',
|
||||
]
|
||||
const adminEnglishI18nRoutes = [
|
||||
'/admin',
|
||||
'/data',
|
||||
'/datasources',
|
||||
'/bgp',
|
||||
'/alerts/system',
|
||||
'/alerts/bgp',
|
||||
'/alerts/situational',
|
||||
'/ai',
|
||||
'/earth-content',
|
||||
'/collection-management',
|
||||
'/logs',
|
||||
'/users',
|
||||
'/settings',
|
||||
]
|
||||
const adminEnglishAllowedCjkText = ['中文']
|
||||
|
||||
function readRepoText(relativePath) {
|
||||
return readFileSync(new URL(`../../${relativePath}`, import.meta.url), 'utf8')
|
||||
@@ -381,6 +410,26 @@ async function clickFirstVisible(locator, label) {
|
||||
await item.click()
|
||||
}
|
||||
|
||||
async function ensureAdminPreferencesOpen(page, label) {
|
||||
const drawer = page.locator('.admin__preferences-drawer.is-open').first()
|
||||
if (await drawer.isVisible().catch(() => false)) return
|
||||
|
||||
const toggle = await firstVisible(page.locator('.admin__account-preferences'), label)
|
||||
await toggle.click()
|
||||
await page.locator('.admin__preferences-drawer.is-open').waitFor({ state: 'visible', timeout: 5000 })
|
||||
}
|
||||
|
||||
async function getEarthFrame(page, label) {
|
||||
const iframe = page.locator('iframe[title="3D Earth"]')
|
||||
await iframe.waitFor({ state: 'visible', timeout: 10000 })
|
||||
const handle = await iframe.elementHandle()
|
||||
const frame = await handle?.contentFrame()
|
||||
if (!frame) {
|
||||
throw new Error(`${label}: Earth iframe did not expose a content frame`)
|
||||
}
|
||||
return frame
|
||||
}
|
||||
|
||||
async function firstVisible(locator, label, timeoutMs = 10000) {
|
||||
const item = await firstVisibleOrNull(locator, timeoutMs)
|
||||
if (item) return item
|
||||
@@ -448,7 +497,13 @@ async function expectUrl(page, route, predicate) {
|
||||
}
|
||||
|
||||
async function checkNoFrameworkOverlay(page, route) {
|
||||
const bodyText = await page.locator('body').innerText({ timeout: 5000 }).catch(() => '')
|
||||
let bodyText = await page.locator('body').innerText({ timeout: 5000 }).catch(() => '')
|
||||
if (!bodyText.trim() && !(await page.locator('iframe').count())) {
|
||||
await page.waitForFunction(() => (
|
||||
Boolean(document.body?.innerText?.trim()) || document.querySelectorAll('iframe').length > 0
|
||||
), null, { timeout: 3000 }).catch(() => {})
|
||||
bodyText = await page.locator('body').innerText({ timeout: 5000 }).catch(() => '')
|
||||
}
|
||||
const overlayNeedles = [
|
||||
'Internal server error',
|
||||
'Failed to resolve import',
|
||||
@@ -487,6 +542,70 @@ async function checkNoGlobalHorizontalOverflow(page, route) {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNoUnexpectedEnglishCjk(page, route) {
|
||||
const result = await page.evaluate((allowed) => {
|
||||
const allowedText = new Set(allowed)
|
||||
const cjk = /[\u3400-\u9fff]/
|
||||
const ignoredSelector = [
|
||||
'script',
|
||||
'style',
|
||||
'code',
|
||||
'pre',
|
||||
'svg',
|
||||
'textarea',
|
||||
'.an-json-viewer',
|
||||
'.an-log-output',
|
||||
'.an-detail-json',
|
||||
'.an-data-json',
|
||||
].join(', ')
|
||||
const root = document.querySelector('.admin-theme-root') || document.body
|
||||
const textNodes = []
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode(node) {
|
||||
const parent = node.parentElement
|
||||
if (!parent || parent.closest(ignoredSelector)) return NodeFilter.FILTER_REJECT
|
||||
const text = (node.textContent || '').replace(/\s+/g, ' ').trim()
|
||||
if (!text || allowedText.has(text) || !cjk.test(text)) return NodeFilter.FILTER_REJECT
|
||||
return NodeFilter.FILTER_ACCEPT
|
||||
},
|
||||
})
|
||||
while (walker.nextNode()) {
|
||||
const node = walker.currentNode
|
||||
const parent = node.parentElement
|
||||
textNodes.push({
|
||||
text: (node.textContent || '').replace(/\s+/g, ' ').trim(),
|
||||
tag: parent?.tagName || '',
|
||||
className: typeof parent?.className === 'string' ? parent.className : '',
|
||||
})
|
||||
}
|
||||
|
||||
const attributes = []
|
||||
root.querySelectorAll('*').forEach((element) => {
|
||||
if (element.closest(ignoredSelector)) return
|
||||
for (const attributeName of ['aria-label', 'placeholder', 'title']) {
|
||||
const value = element.getAttribute(attributeName)
|
||||
if (value && !allowedText.has(value) && cjk.test(value)) {
|
||||
attributes.push({
|
||||
attr: attributeName,
|
||||
value,
|
||||
tag: element.tagName,
|
||||
className: typeof element.className === 'string' ? element.className : '',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
textNodes: textNodes.slice(0, 20),
|
||||
attributes: attributes.slice(0, 20),
|
||||
}
|
||||
}, adminEnglishAllowedCjkText)
|
||||
|
||||
if (result.textNodes.length || result.attributes.length) {
|
||||
throw new Error(`${route}: English UI contains unexpected Chinese text ${JSON.stringify(result)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAdminShellOneScreen(page, route, options = {}) {
|
||||
const { expectAccountVisible = false, expectPreferencesVisible = false } = options
|
||||
const layout = await page.evaluate(() => {
|
||||
@@ -785,7 +904,81 @@ function apiPayloadFor(requestUrl, method) {
|
||||
if (path === '/earth/boundaries/status') return jsonResponse({ status: 'ready', provider: 'low', updated_at: now })
|
||||
if (path === '/earth/boundaries/build/status') return jsonResponse({ status: 'idle', progress: 0 })
|
||||
if (path === '/settings/tv') return jsonResponse({ tv: { default_source_id: 'smoke-tv', sources: [{ id: 'smoke-tv', name: 'Smoke TV', source_type: 'hls', stream_url: 'https://example.invalid/live.m3u8', is_enabled: true, sort_order: 10 }] } })
|
||||
if (path === '/tv/streams') return jsonResponse({ sources: [] })
|
||||
if (path === '/tv/streams') return jsonResponse({
|
||||
generated_at: now,
|
||||
latest_updated_at: now,
|
||||
source_count: 1,
|
||||
default_source_id: 'cctv-news',
|
||||
selected_source: { id: 'cctv-news' },
|
||||
sources: [{
|
||||
id: 'cctv-news',
|
||||
name: '央视新闻',
|
||||
provider: '中国媒体',
|
||||
region: '中国',
|
||||
language: '中文',
|
||||
source_type: 'external',
|
||||
homepage_url: 'https://example.invalid/live',
|
||||
notes: '默认直播源',
|
||||
is_enabled: true,
|
||||
sort_order: 10,
|
||||
}],
|
||||
})
|
||||
if (path === '/news/earth-feed') return jsonResponse({
|
||||
generated_at: now,
|
||||
focus: { lat: 0, lon: 0, region: 'global', label: 'Global Focus', display_region: 'Global' },
|
||||
filters: { locale: url.searchParams.get('locale') || 'zh-CN', categories: [], sources: [], limit: 12 },
|
||||
sources: [
|
||||
{ id: 'bbc-world', name: 'BBC World', region: 'global', homepage_url: 'https://www.bbc.com/news', source_type: 'rss', enabled: true },
|
||||
{ id: '36kr', name: '36氪', region: 'asia-pacific', homepage_url: 'https://www.36kr.com/', source_type: 'rss', enabled: true },
|
||||
],
|
||||
items: [
|
||||
{
|
||||
id: 'bbc-world:smoke-english',
|
||||
source_id: 'bbc-world',
|
||||
title: 'Global trade ministers agree on supply-chain safeguards',
|
||||
summary: 'Officials said the plan focuses on resilient logistics and energy infrastructure.',
|
||||
content_language: 'en',
|
||||
localizations: {},
|
||||
display_title: 'Global trade ministers agree on supply-chain safeguards',
|
||||
display_summary: 'Officials said the plan focuses on resilient logistics and energy infrastructure.',
|
||||
url: 'https://example.invalid/news',
|
||||
source: 'BBC World',
|
||||
feed_name: 'BBC World',
|
||||
source_type: 'rss',
|
||||
category: 'business',
|
||||
region: 'global',
|
||||
display_region: 'Global',
|
||||
published_at: now,
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
enrichment_status: 'success',
|
||||
},
|
||||
{
|
||||
id: '36kr:smoke-chinese',
|
||||
source_id: '36kr',
|
||||
title: '中国电商平台发布季度增长数据',
|
||||
summary: '平台表示,跨境电商订单量同比增长。',
|
||||
content_language: 'zh-CN',
|
||||
localizations: { 'zh-CN': { title: '中国电商平台发布季度增长数据', summary: '平台表示,跨境电商订单量同比增长。' } },
|
||||
display_title: '',
|
||||
display_summary: '',
|
||||
url: 'https://example.invalid/zh-news',
|
||||
source: '36氪',
|
||||
feed_name: '综合资讯',
|
||||
source_type: 'rss',
|
||||
category: 'business',
|
||||
region: 'asia-pacific',
|
||||
display_region: '亚太',
|
||||
published_at: now,
|
||||
latitude: 31,
|
||||
longitude: 121,
|
||||
enrichment_status: 'queued',
|
||||
},
|
||||
],
|
||||
cruise_items: [],
|
||||
errors: [],
|
||||
stale: false,
|
||||
})
|
||||
if (path === '/earth/news-sources') return jsonResponse({
|
||||
is_default: true,
|
||||
sources: [
|
||||
@@ -964,7 +1157,7 @@ async function runAdminInteractionChecks(browser, failures, consoleErrors) {
|
||||
await expectVisibleText(page, 'SMTP 邮件', 'global search smtp')
|
||||
|
||||
await page.goto(urlFor('/admin'), { waitUntil: 'domcontentloaded' })
|
||||
await clickFirstVisible(page.getByRole('button', { name: '展开偏好设置' }), 'admin preferences drawer open')
|
||||
await ensureAdminPreferencesOpen(page, 'admin preferences drawer open')
|
||||
await page.waitForTimeout(350)
|
||||
await checkAdminShellOneScreen(page, 'admin preferences drawer open', {
|
||||
expectAccountVisible: true,
|
||||
@@ -981,10 +1174,17 @@ async function runAdminInteractionChecks(browser, failures, consoleErrors) {
|
||||
location.pathname === '/settings' && location.search.includes('section=smtp')
|
||||
))
|
||||
await expectVisibleText(page, 'SMTP Email', 'admin english global search smtp')
|
||||
const reopenEnglishPreferences = await firstVisibleOrNull(page.getByRole('button', { name: 'Expand preferences' }), 500)
|
||||
if (reopenEnglishPreferences) {
|
||||
await reopenEnglishPreferences.click()
|
||||
smokeProgress('admin english i18n sweep')
|
||||
for (const route of adminEnglishI18nRoutes) {
|
||||
const label = `admin english i18n ${route}`
|
||||
smokeProgress(label, 'verbose')
|
||||
await page.goto(urlFor(route), { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForTimeout(450)
|
||||
await checkNoFrameworkOverlay(page, label)
|
||||
await checkNoUnexpectedEnglishCjk(page, label)
|
||||
}
|
||||
await page.goto(urlFor('/settings?section=smtp'), { waitUntil: 'domcontentloaded' })
|
||||
await ensureAdminPreferencesOpen(page, 'admin preferences drawer reopen')
|
||||
await clickFirstVisible(
|
||||
page.getByRole('group', { name: 'Console language' }).getByRole('button', { name: '中文' }),
|
||||
'admin language switch chinese',
|
||||
@@ -1009,15 +1209,23 @@ async function runAdminInteractionChecks(browser, failures, consoleErrors) {
|
||||
await expectVisibleText(page, 'SMTP 邮件', 'section tab settings smtp')
|
||||
|
||||
await page.goto(urlFor('/ai?section=integrations'), { waitUntil: 'domcontentloaded' })
|
||||
await clickFirstVisible(page.getByRole('button', { name: '测试 AI Provider 连通性' }), 'connection test input ai provider')
|
||||
await expectVisibleText(page, 'LLM 基础地址', 'connection test input ai provider')
|
||||
await clickFirstVisible(page.locator('.an-connection-test-input button'), 'connection test input ai provider')
|
||||
await expectVisibleText(page, 'AI Provider连通性正常', 'connection test input ai provider')
|
||||
|
||||
await page.goto(urlFor('/ai?section=tools'), { waitUntil: 'domcontentloaded' })
|
||||
await clickFirstVisible(page.getByRole('button', { name: /tavily/i }), 'connection test input websearch provider')
|
||||
await clickFirstVisible(page.getByRole('switch', { name: '停用 WebSearch' }), 'connection test input websearch switch')
|
||||
await expectVisibleText(page, 'WebSearch 已在草稿中停用', 'connection test input websearch disabled')
|
||||
const webSearchEnabledInput = page.locator('[data-admin-search-field="enabled"] input[type="checkbox"]')
|
||||
await firstVisible(webSearchEnabledInput, 'connection test input websearch switch')
|
||||
if (await webSearchEnabledInput.first().isChecked()) {
|
||||
await webSearchEnabledInput.first().click()
|
||||
}
|
||||
await page.waitForFunction(() => {
|
||||
const input = document.querySelector('[data-admin-search-field="enabled"] input[type="checkbox"]')
|
||||
return input instanceof HTMLInputElement && !input.checked
|
||||
}, null, { timeout: 5000 })
|
||||
await expectFirstVisibleDisabled(page.getByLabel('API 基础地址'), 'connection test input websearch base url')
|
||||
await expectFirstVisibleDisabled(page.getByRole('button', { name: '测试 Web Search 连通性' }), 'connection test input websearch action')
|
||||
await expectFirstVisibleDisabled(page.locator('.an-connection-test-input button'), 'connection test input websearch action')
|
||||
|
||||
await page.goto(urlFor('/logs'), { waitUntil: 'domcontentloaded' })
|
||||
await clickFirstVisible(page.getByRole('tab', { name: /原始日志/ }), 'logs raw tab')
|
||||
@@ -1259,16 +1467,487 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
smokeProgress('public english i18n sweep')
|
||||
await page.goto(urlFor('/login'), { waitUntil: 'domcontentloaded' })
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('planet-locale', 'en-US')
|
||||
localStorage.setItem('docs-lang', 'en')
|
||||
})
|
||||
for (const route of publicEnglishI18nRoutes) {
|
||||
const label = `public english i18n ${route}`
|
||||
smokeProgress(label, 'verbose')
|
||||
await page.goto(urlFor(route), { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForTimeout(450)
|
||||
await checkNoFrameworkOverlay(page, label)
|
||||
await checkNoUnexpectedEnglishCjk(page, label)
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('planet-locale', 'zh-CN')
|
||||
localStorage.setItem('docs-lang', 'zh')
|
||||
})
|
||||
} catch (error) {
|
||||
failures.push(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
|
||||
try {
|
||||
smokeProgress('earth public route')
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('planet-locale', 'en-US')
|
||||
localStorage.setItem('docs-lang', 'en')
|
||||
})
|
||||
await page.goto(urlFor('/earth'), { waitUntil: 'domcontentloaded' })
|
||||
await checkNoFrameworkOverlay(page, '/earth')
|
||||
await page.locator('iframe[title="3D Earth"]').waitFor({ state: 'visible', timeout: 10000 })
|
||||
const earthFrame = await getEarthFrame(page, '/earth')
|
||||
await earthFrame.waitForFunction(() => document.documentElement.lang === 'en-US', null, { timeout: 10000 })
|
||||
await earthFrame.waitForFunction(() => (
|
||||
document.querySelector('.earth-brand__title')?.getAttribute('src')?.includes('title-en.png')
|
||||
), null, { timeout: 10000 })
|
||||
const brandState = await earthFrame.evaluate(() => {
|
||||
const brand = document.querySelector('#brand-panel .earth-brand')
|
||||
const panel = document.getElementById('brand-panel')
|
||||
const title = document.querySelector('.earth-brand__title')
|
||||
const subtitle = document.querySelector('.earth-brand__subtitle')
|
||||
const description = document.querySelector('.earth-brand__description')
|
||||
if (
|
||||
!(brand instanceof HTMLElement)
|
||||
|| !(panel instanceof HTMLElement)
|
||||
|| !(title instanceof HTMLImageElement)
|
||||
|| !(subtitle instanceof HTMLElement)
|
||||
|| !(description instanceof HTMLElement)
|
||||
) return null
|
||||
const brandRect = brand.getBoundingClientRect()
|
||||
const panelRect = panel.getBoundingClientRect()
|
||||
const titleRect = title.getBoundingClientRect()
|
||||
const subtitleRect = subtitle.getBoundingClientRect()
|
||||
const descriptionRect = description.getBoundingClientRect()
|
||||
return {
|
||||
brandWidth: brandRect.width,
|
||||
panelWidth: panelRect.width,
|
||||
titleWidth: titleRect.width,
|
||||
titleRight: titleRect.right,
|
||||
brandRight: brandRect.right,
|
||||
panelRight: panelRect.right,
|
||||
subtitleText: subtitle.innerText,
|
||||
descriptionText: description.innerText,
|
||||
subtitleRight: subtitleRect.right,
|
||||
descriptionRight: descriptionRect.right,
|
||||
viewportWidth: window.innerWidth,
|
||||
}
|
||||
})
|
||||
if (
|
||||
!brandState
|
||||
|| brandState.brandWidth > 285
|
||||
|| brandState.panelWidth > 290
|
||||
|| brandState.titleWidth < 110
|
||||
|| brandState.titleRight > brandState.panelRight + 1
|
||||
|| brandState.brandRight > brandState.panelRight + 1
|
||||
|| brandState.subtitleRight > brandState.panelRight + 1
|
||||
|| brandState.descriptionRight > brandState.panelRight + 1
|
||||
|| !brandState.subtitleText
|
||||
|| !brandState.descriptionText
|
||||
|| /[\u4e00-\u9fff]/.test(`${brandState.subtitleText}\n${brandState.descriptionText}`)
|
||||
|| brandState.brandRight > brandState.viewportWidth - 8
|
||||
) {
|
||||
throw new Error(`/earth i18n settings: English brand text is clipped or oversized ${JSON.stringify(brandState)}`)
|
||||
}
|
||||
await earthFrame.evaluate(() => document.getElementById('settings-trigger')?.click())
|
||||
await earthFrame.waitForTimeout(350)
|
||||
await earthFrame.evaluate(() => (
|
||||
document.querySelector('.earth-settings-tabs [data-settings-tab="system"]')?.click()
|
||||
))
|
||||
await earthFrame.waitForFunction(() => (
|
||||
!document.querySelector('.earth-settings-sheet [data-settings-tab-panel="system"]')?.hidden
|
||||
), null, { timeout: 5000 })
|
||||
const systemText = await earthFrame.evaluate(() => (
|
||||
document.querySelector('.earth-settings-sheet [data-settings-tab-panel="system"]')?.innerText || ''
|
||||
))
|
||||
if (!systemText.includes('Earth Language')) {
|
||||
throw new Error('/earth i18n settings: expected English language control')
|
||||
}
|
||||
const localeSegmentInitial = await earthFrame.evaluate(() => {
|
||||
const segmented = document.querySelector('.earth-settings-sheet [data-settings-tab-panel="system"] .earth-settings-segmented')
|
||||
const english = segmented?.querySelector('[data-earth-locale="en-US"]')
|
||||
const chinese = segmented?.querySelector('[data-earth-locale="zh-CN"]')
|
||||
if (!(segmented instanceof HTMLElement) || !(english instanceof HTMLButtonElement) || !(chinese instanceof HTMLButtonElement)) return null
|
||||
return {
|
||||
englishActive: english.classList.contains('is-active'),
|
||||
chineseActive: chinese.classList.contains('is-active'),
|
||||
activeIndex: getComputedStyle(segmented).getPropertyValue('--active-index').trim(),
|
||||
itemCount: getComputedStyle(segmented).getPropertyValue('--item-count').trim(),
|
||||
}
|
||||
})
|
||||
if (
|
||||
!localeSegmentInitial?.englishActive
|
||||
|| localeSegmentInitial.chineseActive
|
||||
|| localeSegmentInitial.activeIndex !== '1'
|
||||
|| localeSegmentInitial.itemCount !== '2'
|
||||
) {
|
||||
throw new Error(`/earth i18n settings: English language segmented control is not visually active ${JSON.stringify(localeSegmentInitial)}`)
|
||||
}
|
||||
await earthFrame.evaluate(() => document.querySelector('.earth-settings-sheet [data-earth-locale="zh-CN"]')?.click())
|
||||
await earthFrame.waitForFunction(() => {
|
||||
const segmented = document.querySelector('.earth-settings-sheet [data-settings-tab-panel="system"] .earth-settings-segmented')
|
||||
const english = segmented?.querySelector('[data-earth-locale="en-US"]')
|
||||
const chinese = segmented?.querySelector('[data-earth-locale="zh-CN"]')
|
||||
return (
|
||||
document.documentElement.lang === 'zh-CN'
|
||||
&& segmented instanceof HTMLElement
|
||||
&& english instanceof HTMLButtonElement
|
||||
&& chinese instanceof HTMLButtonElement
|
||||
&& chinese.classList.contains('is-active')
|
||||
&& !english.classList.contains('is-active')
|
||||
&& getComputedStyle(segmented).getPropertyValue('--active-index').trim() === '0'
|
||||
)
|
||||
}, null, { timeout: 5000 })
|
||||
const zhSystemText = await earthFrame.evaluate(() => (
|
||||
document.querySelector('.earth-settings-sheet [data-settings-tab-panel="system"]')?.innerText || ''
|
||||
))
|
||||
if (!zhSystemText.includes('星球语言')) {
|
||||
throw new Error('/earth i18n settings: expected Chinese language control after switching locale')
|
||||
}
|
||||
await earthFrame.evaluate(() => document.querySelector('.earth-settings-sheet [data-earth-locale="en-US"]')?.click())
|
||||
await earthFrame.waitForFunction(() => {
|
||||
const segmented = document.querySelector('.earth-settings-sheet [data-settings-tab-panel="system"] .earth-settings-segmented')
|
||||
const english = segmented?.querySelector('[data-earth-locale="en-US"]')
|
||||
const chinese = segmented?.querySelector('[data-earth-locale="zh-CN"]')
|
||||
return (
|
||||
document.documentElement.lang === 'en-US'
|
||||
&& localStorage.getItem('planet-locale') === 'en-US'
|
||||
&& localStorage.getItem('docs-lang') === 'en'
|
||||
&& segmented instanceof HTMLElement
|
||||
&& english instanceof HTMLButtonElement
|
||||
&& chinese instanceof HTMLButtonElement
|
||||
&& english.classList.contains('is-active')
|
||||
&& !chinese.classList.contains('is-active')
|
||||
&& getComputedStyle(segmented).getPropertyValue('--active-index').trim() === '1'
|
||||
)
|
||||
}, null, { timeout: 5000 })
|
||||
const hdTextureStatus = await earthFrame.evaluate(async () => {
|
||||
const { earthMessage, formatEarthMessage } = await import('/earth/js/i18n.js?frontend_smoke_i18n')
|
||||
const ui = await import('/earth/js/ui.js?frontend_smoke_i18n')
|
||||
const message = earthMessage('startup.hdTexture')
|
||||
const translated = formatEarthMessage(message, 'en-US')
|
||||
ui.setLoading(true)
|
||||
ui.setLoadingMessage(message)
|
||||
return {
|
||||
translated,
|
||||
rendered: document.querySelector('#status-message .earth-status-text')?.textContent || '',
|
||||
}
|
||||
})
|
||||
if (
|
||||
/[\u4e00-\u9fff]/.test(`${hdTextureStatus.translated}\n${hdTextureStatus.rendered}`)
|
||||
|| !hdTextureStatus.translated.includes('Enabling HD texture')
|
||||
|| !hdTextureStatus.rendered.includes('Enabling HD texture')
|
||||
) {
|
||||
throw new Error(`/earth i18n settings: HD texture loading status is not translated: ${JSON.stringify(hdTextureStatus)}`)
|
||||
}
|
||||
const earthEnglishCjkAudit = await earthFrame.evaluate(() => {
|
||||
const cjk = /[\u4e00-\u9fff]/
|
||||
const allowed = new Set(['中文'])
|
||||
const skip = 'script,style,svg,canvas,video,iframe,.material-symbols-rounded'
|
||||
const isVisible = (element) => {
|
||||
if (!(element instanceof HTMLElement)) return false
|
||||
if (element.matches(skip) || element.closest(skip)) return false
|
||||
const style = getComputedStyle(element)
|
||||
if (style.visibility === 'hidden' || style.display === 'none' || Number(style.opacity) === 0) return false
|
||||
const rect = element.getBoundingClientRect()
|
||||
return rect.width > 0 && rect.height > 0
|
||||
}
|
||||
const textHits = []
|
||||
const attrHits = []
|
||||
document.querySelectorAll('body *').forEach((element) => {
|
||||
if (!isVisible(element)) return
|
||||
const ownText = Array.from(element.childNodes)
|
||||
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.textContent || '')
|
||||
.join(' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
if (ownText && cjk.test(ownText) && !allowed.has(ownText)) {
|
||||
textHits.push({
|
||||
selector: element.id ? `#${element.id}` : String(element.className || element.tagName.toLowerCase()),
|
||||
text: ownText,
|
||||
})
|
||||
}
|
||||
;['title', 'aria-label', 'placeholder', 'alt'].forEach((attr) => {
|
||||
const value = element.getAttribute(attr)
|
||||
if (value && cjk.test(value) && !allowed.has(value)) {
|
||||
attrHits.push({
|
||||
selector: element.id ? `#${element.id}` : String(element.className || element.tagName.toLowerCase()),
|
||||
attr,
|
||||
value,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
return {
|
||||
textHits: textHits.slice(0, 20),
|
||||
attrHits: attrHits.slice(0, 20),
|
||||
}
|
||||
})
|
||||
if (earthEnglishCjkAudit.textHits.length || earthEnglishCjkAudit.attrHits.length) {
|
||||
throw new Error(`/earth i18n settings: English UI still has visible CJK ${JSON.stringify(earthEnglishCjkAudit)}`)
|
||||
}
|
||||
await earthFrame.evaluate(() => (
|
||||
document.querySelector('.earth-settings-tabs [data-settings-tab="panels"]')?.click()
|
||||
))
|
||||
await earthFrame.waitForFunction(() => (
|
||||
!document.querySelector('.earth-settings-sheet [data-settings-tab-panel="panels"]')?.hidden
|
||||
), null, { timeout: 5000 })
|
||||
const switchState = await earthFrame.evaluate(() => {
|
||||
const label = document.querySelector('label[for="toggle-view-legend"]')
|
||||
const input = document.getElementById('toggle-view-legend')
|
||||
if (!(label instanceof HTMLElement) || !(input instanceof HTMLInputElement)) return null
|
||||
const before = input.checked
|
||||
label.click()
|
||||
const shell = input.closest('.earth-settings-switch')
|
||||
return {
|
||||
before,
|
||||
checked: input.checked,
|
||||
visualChecked: shell?.classList.contains('is-checked') === true,
|
||||
}
|
||||
})
|
||||
if (
|
||||
!switchState
|
||||
|| switchState.checked === switchState.before
|
||||
|| switchState.visualChecked !== switchState.checked
|
||||
) {
|
||||
throw new Error('/earth i18n settings: system switch visual state did not follow checked state')
|
||||
}
|
||||
await earthFrame.evaluate(() => {
|
||||
const label = document.querySelector('label[for="toggle-view-legend"]')
|
||||
const input = document.getElementById('toggle-view-legend')
|
||||
if (input instanceof HTMLInputElement && !input.checked && label instanceof HTMLElement) {
|
||||
label.click()
|
||||
}
|
||||
})
|
||||
await earthFrame.waitForFunction(() => (
|
||||
!document.getElementById('legend')?.classList.contains('hud-panel-hidden')
|
||||
), null, { timeout: 5000 })
|
||||
const legendState = await earthFrame.evaluate(async () => {
|
||||
const legend = await import('/earth/js/legend.js')
|
||||
legend.setLegendItems('satellites', [
|
||||
{ color: '#ff3333', label: '赤道轨道(0-30°)' },
|
||||
{ color: '#ff9933', label: '低倾角轨道(30-60°)' },
|
||||
{ color: '#ffff33', label: '中倾角轨道(60-90°)' },
|
||||
{ color: '#33ff33', label: '高倾角轨道(90-120°)' },
|
||||
{ color: '#3333ff', label: '逆行轨道(120-180°)' },
|
||||
{ color: '#5eead4', label: '低轨' },
|
||||
{ color: '#93c5fd', label: '中轨' },
|
||||
{ color: '#c4b5fd', label: '高轨' },
|
||||
{ color: '#f9a8d4', label: '地球静止轨道' },
|
||||
{ color: '#fcd34d', label: '太阳同步轨道' },
|
||||
{ color: '#e5e7eb', label: '极轨' },
|
||||
])
|
||||
legend.setLegendMode('satellites')
|
||||
const panel = document.getElementById('legend')
|
||||
const panelRect = panel?.getBoundingClientRect()
|
||||
const currentLabel = document.getElementById('legend-current-label')
|
||||
const labels = Array.from(document.querySelectorAll('#legend-current-label, #legend .legend-label'))
|
||||
const mobileLabels = Array.from(document.querySelectorAll('#mobile-situation-legend-mode, #mobile-situation-legend-list .legend-label'))
|
||||
const text = labels.concat(mobileLabels)
|
||||
.map((element) => `${element.textContent || ''}\n${element.getAttribute('title') || ''}`)
|
||||
.join('\n')
|
||||
const overflows = labels
|
||||
.map((element) => {
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (!panelRect) return null
|
||||
const overflow = rect.left < panelRect.left - 1 || rect.right > panelRect.right + 1
|
||||
return overflow ? {
|
||||
text: element.textContent,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
panelLeft: panelRect.left,
|
||||
panelRight: panelRect.right,
|
||||
} : null
|
||||
})
|
||||
.filter(Boolean)
|
||||
return {
|
||||
text,
|
||||
overflows,
|
||||
currentText: currentLabel?.textContent || '',
|
||||
currentTitle: currentLabel?.getAttribute('title') || '',
|
||||
}
|
||||
})
|
||||
if (/[\u4e00-\u9fff]/.test(legendState.text)) {
|
||||
throw new Error('/earth i18n settings: English satellite legend contains Chinese text')
|
||||
}
|
||||
if (!legendState.text.includes('Orbits')) {
|
||||
throw new Error('/earth i18n settings: English satellite legend should use compact Orbits label')
|
||||
}
|
||||
if (!legendState.text.includes('Low Incl.') || !legendState.text.includes('High Incl.') || !legendState.text.includes('Retrograde')) {
|
||||
throw new Error(`/earth i18n settings: English satellite legend missing translated orbit labels ${JSON.stringify(legendState)}`)
|
||||
}
|
||||
for (const compactOrbitLabel of ['LEO', 'MEO', 'HEO', 'GEO', 'SSO', 'Polar']) {
|
||||
if (!legendState.text.includes(compactOrbitLabel)) {
|
||||
throw new Error(`/earth i18n settings: English satellite legend missing compact orbit label ${compactOrbitLabel} ${JSON.stringify(legendState)}`)
|
||||
}
|
||||
}
|
||||
if (legendState.currentText !== legendState.currentTitle) {
|
||||
throw new Error(`/earth i18n settings: legend current title mismatch ${JSON.stringify(legendState)}`)
|
||||
}
|
||||
if (legendState.overflows.length > 0) {
|
||||
throw new Error(`/earth i18n settings: legend labels overflow panel ${JSON.stringify(legendState.overflows)}`)
|
||||
}
|
||||
const earthDynamicTranslations = await earthFrame.evaluate(async () => {
|
||||
const i18n = await import('/earth/js/i18n.js')
|
||||
return {
|
||||
satelliteShown: i18n.translateText('卫星已显示', 'en-US'),
|
||||
cruisePaused: i18n.translateText('巡航已暂停', 'en-US'),
|
||||
locatedCompute: i18n.translateText('已定位算力中心:Smoke Node', 'en-US'),
|
||||
loadingCables: i18n.translateText('正在加载线缆数据...', 'en-US'),
|
||||
}
|
||||
})
|
||||
if (
|
||||
earthDynamicTranslations.satelliteShown !== 'Satellites shown'
|
||||
|| earthDynamicTranslations.cruisePaused !== 'Cruise paused'
|
||||
|| earthDynamicTranslations.locatedCompute !== 'Located compute centers: Smoke Node'
|
||||
|| earthDynamicTranslations.loadingCables !== 'Loading cable data...'
|
||||
) {
|
||||
throw new Error(`/earth i18n settings: dynamic status translations regressed ${JSON.stringify(earthDynamicTranslations)}`)
|
||||
}
|
||||
const earthNotificationState = await earthFrame.evaluate(async () => {
|
||||
const ui = await import('/earth/js/ui.js')
|
||||
ui.setLoading(false)
|
||||
ui.showStatusMessage('成功加载 12 条电缆', 'success')
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 360))
|
||||
const status = document.getElementById('status-message')
|
||||
const brand = document.getElementById('brand-panel')
|
||||
const ticker = document.getElementById('desktop-news-ticker')
|
||||
const rect = status?.getBoundingClientRect()
|
||||
const brandRect = brand?.getBoundingClientRect()
|
||||
const tickerRect = ticker?.getBoundingClientRect()
|
||||
return {
|
||||
text: status?.innerText || '',
|
||||
visible: status?.classList.contains('visible') === true,
|
||||
left: rect?.left || 0,
|
||||
top: rect?.top || 0,
|
||||
brandRight: brandRect?.right || 0,
|
||||
tickerBottom: tickerRect?.bottom || 0,
|
||||
width: rect?.width || 0,
|
||||
maxWidth: status instanceof HTMLElement ? getComputedStyle(status).maxWidth : '',
|
||||
}
|
||||
})
|
||||
if (
|
||||
/[\u4e00-\u9fff]/.test(earthNotificationState.text)
|
||||
|| !earthNotificationState.text.includes('Loaded 12 cables')
|
||||
|| !earthNotificationState.visible
|
||||
|| earthNotificationState.left < earthNotificationState.brandRight + 8
|
||||
|| earthNotificationState.left > earthNotificationState.brandRight + 28
|
||||
|| earthNotificationState.top < earthNotificationState.tickerBottom + 2
|
||||
|| earthNotificationState.width < 120
|
||||
|| earthNotificationState.width > 240
|
||||
) {
|
||||
throw new Error(`/earth i18n settings: English notification text or width regressed ${JSON.stringify(earthNotificationState)}`)
|
||||
}
|
||||
const earthTooltipState = await earthFrame.evaluate(async () => {
|
||||
const i18n = await import('/earth/js/i18n.js')
|
||||
const ui = await import('/earth/js/ui.js')
|
||||
const countryName = i18n.localizeCountryName({ name: 'China', nameZh: '中国', continent: 'Asia' }, 'en-US')
|
||||
const content = [
|
||||
`<strong>${countryName}</strong>`,
|
||||
`ISO: CHN`,
|
||||
`${i18n.translateText('大洲', 'en-US')}: ${i18n.translateText('亚洲', 'en-US')}`,
|
||||
`${i18n.translateText('纬度', 'en-US')}: 31.2300°`,
|
||||
`${i18n.translateText('经度', 'en-US')}: 121.4700°`,
|
||||
`${i18n.translateText('海拔', 'en-US')}: 8 m`,
|
||||
`${i18n.translateText('country', 'en-US')}: ${i18n.localizeCountryName('中国', 'en-US')}`,
|
||||
].join('<br>')
|
||||
ui.showTooltip(180, 180, content)
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve))
|
||||
const tooltip = document.getElementById('tooltip')
|
||||
const rect = tooltip?.getBoundingClientRect()
|
||||
return {
|
||||
text: tooltip?.innerText || '',
|
||||
display: tooltip instanceof HTMLElement ? getComputedStyle(tooltip).display : '',
|
||||
width: rect?.width || 0,
|
||||
}
|
||||
})
|
||||
if (
|
||||
/[\u4e00-\u9fff]/.test(earthTooltipState.text)
|
||||
|| !earthTooltipState.text.includes('China')
|
||||
|| !earthTooltipState.text.includes('Continent: Asia')
|
||||
|| !earthTooltipState.text.includes('Latitude')
|
||||
|| !earthTooltipState.text.includes('Longitude')
|
||||
|| !earthTooltipState.text.includes('Altitude')
|
||||
|| !earthTooltipState.text.includes('Country: China')
|
||||
) {
|
||||
throw new Error(`/earth i18n settings: English tooltip translation regressed ${JSON.stringify(earthTooltipState)}`)
|
||||
}
|
||||
await earthFrame.evaluate(() => document.getElementById('settings-close')?.click())
|
||||
await earthFrame.waitForFunction(() => (
|
||||
document.getElementById('news-ticker-track')?.innerText.includes('Officials said')
|
||||
), null, { timeout: 10000 })
|
||||
await earthFrame.evaluate(() => document.getElementById('desktop-news-ticker')?.click())
|
||||
await earthFrame.waitForFunction(() => (
|
||||
!document.getElementById('news-hud-panel')?.classList.contains('hud-panel-hidden')
|
||||
), null, { timeout: 5000 })
|
||||
const newsHudText = await earthFrame.evaluate(() => (
|
||||
document.getElementById('news-hud-panel')?.innerText || ''
|
||||
))
|
||||
if (/[\u4e00-\u9fff]/.test(newsHudText)) {
|
||||
throw new Error('/earth i18n settings: English news panel contains Chinese text')
|
||||
}
|
||||
const detailCardText = await earthFrame.evaluate(async () => {
|
||||
const card = await import('/earth/js/info-card.js')
|
||||
card.showInfoCard('satellite', {
|
||||
name: 'SmokeSat',
|
||||
norad_id: '12345',
|
||||
constellation: 'Smoke',
|
||||
footprint_capability: '支持 Starlink 地表覆盖',
|
||||
current_display: '真实地表覆盖(Starlink)',
|
||||
footprint_model: 'Starlink 单星地表覆盖',
|
||||
inclination: '53.00',
|
||||
period: '90.0',
|
||||
perigee: '550',
|
||||
apogee: '560',
|
||||
field_sources: { current_display: 'smoke' },
|
||||
}, { x: 460, y: 180, absolute: true })
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve))
|
||||
const panel = document.getElementById('info-panel')
|
||||
const titleText = Array.from(panel?.querySelectorAll('[title]') || [])
|
||||
.map((element) => element.getAttribute('title') || '')
|
||||
.join('\\n')
|
||||
return [panel?.innerText || '', titleText].join('\\n')
|
||||
})
|
||||
if (/[\u4e00-\u9fff]/.test(detailCardText)) {
|
||||
throw new Error('/earth i18n settings: English detail card contains Chinese text')
|
||||
}
|
||||
await earthFrame.evaluate(async () => {
|
||||
const tv = await import('/earth/js/tv.js')
|
||||
await tv.ensureTVPanelReady()
|
||||
})
|
||||
await earthFrame.waitForFunction(() => (
|
||||
document.getElementById('tv-source-title')?.innerText.includes('Cctv News')
|
||||
), null, { timeout: 5000 })
|
||||
const tvText = await earthFrame.evaluate(() => [
|
||||
document.getElementById('tv-source-title')?.innerText || '',
|
||||
document.getElementById('tv-source-origin')?.innerText || '',
|
||||
document.getElementById('tv-source-status')?.innerText || '',
|
||||
document.getElementById('tv-source-catalog')?.innerText || '',
|
||||
document.getElementById('tv-source-notes')?.innerText || '',
|
||||
Array.from(document.querySelectorAll('#tv-source-select option')).map((option) => option.textContent || '').join(' '),
|
||||
].join('\n'))
|
||||
if (!tvText.includes('Default') || !tvText.includes('Built-in')) {
|
||||
throw new Error('/earth i18n settings: English TV panel is missing localized default/built-in labels')
|
||||
}
|
||||
if (/[\u4e00-\u9fff]/.test(tvText)) {
|
||||
throw new Error('/earth i18n settings: English TV panel contains Chinese text')
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('planet-locale', 'zh-CN')
|
||||
localStorage.setItem('docs-lang', 'zh')
|
||||
})
|
||||
} catch (error) {
|
||||
failures.push(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
|
||||
smokeProgress('protected route guards')
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('planet-locale', 'zh-CN')
|
||||
localStorage.setItem('docs-lang', 'zh')
|
||||
}).catch(() => {})
|
||||
for (const route of adminRoutes) {
|
||||
try {
|
||||
smokeProgress(`guard ${route}`, 'verbose')
|
||||
@@ -1292,6 +1971,28 @@ async function main() {
|
||||
failures.push(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
|
||||
try {
|
||||
smokeProgress('english login backend error i18n')
|
||||
await page.goto(urlFor('/login'), { waitUntil: 'domcontentloaded' })
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('planet-locale', 'en-US')
|
||||
localStorage.setItem('docs-lang', 'en')
|
||||
})
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await page.getByLabel('Username').fill('smoke-user')
|
||||
await page.getByLabel('Password').fill('wrong-password')
|
||||
await page.getByRole('button', { name: 'Log in' }).click()
|
||||
await expectVisibleText(page, 'Login failed. Check your account or password.', 'english login backend error i18n')
|
||||
await checkNoUnexpectedEnglishCjk(page, 'english login backend error i18n')
|
||||
await checkNoFrameworkOverlay(page, 'english login backend error i18n')
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('planet-locale', 'zh-CN')
|
||||
localStorage.setItem('docs-lang', 'zh')
|
||||
})
|
||||
} catch (error) {
|
||||
failures.push(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
|
||||
await runAuthenticatedAdminChecks(browser, failures, consoleErrors, {
|
||||
label: 'desktop',
|
||||
viewport: desktopViewport,
|
||||
|
||||
Reference in New Issue
Block a user