diff --git a/external-admin/index.html b/external-admin/index.html index a83fee8..61d85dd 100644 --- a/external-admin/index.html +++ b/external-admin/index.html @@ -4,7 +4,7 @@ - Fami + External Admin
diff --git a/external-admin/src/i18n/ExternalI18nProvider.test.jsx b/external-admin/src/i18n/ExternalI18nProvider.test.jsx index 86af77f..7a30b33 100644 --- a/external-admin/src/i18n/ExternalI18nProvider.test.jsx +++ b/external-admin/src/i18n/ExternalI18nProvider.test.jsx @@ -16,7 +16,7 @@ describe("ExternalI18nProvider", () => { render(); expect(screen.getByTestId("locale")).toHaveTextContent("en"); - expect(screen.getByTestId("title")).toHaveTextContent("Fami"); + expect(screen.getByTestId("title")).toHaveTextContent("External Admin"); expect(document.documentElement).toHaveAttribute("lang", "en"); expect(document.documentElement).toHaveAttribute("dir", "ltr"); }); @@ -26,25 +26,25 @@ describe("ExternalI18nProvider", () => { const firstRender = render(); await user.click(screen.getByRole("button", { name: "ar" })); - expect(screen.getByTestId("title")).toHaveTextContent("Fami"); + expect(screen.getByTestId("title")).toHaveTextContent("الإدارة الخارجية"); expect(document.documentElement).toHaveAttribute("lang", "ar"); expect(document.documentElement).toHaveAttribute("dir", "rtl"); expect(document.body).toHaveAttribute("dir", "rtl"); expect(localStorage.getItem(EXTERNAL_LOCALE_STORAGE_KEY)).toBe("ar"); await user.click(screen.getByRole("button", { name: "tr" })); - expect(screen.getByTestId("title")).toHaveTextContent("Fami"); + expect(screen.getByTestId("title")).toHaveTextContent("Harici Yönetim"); expect(document.documentElement).toHaveAttribute("lang", "tr"); expect(document.documentElement).toHaveAttribute("dir", "ltr"); await user.click(screen.getByRole("button", { name: "zh-CN" })); - expect(screen.getByTestId("title")).toHaveTextContent("Fami"); + expect(screen.getByTestId("title")).toHaveTextContent("外管后台"); expect(localStorage.getItem(EXTERNAL_LOCALE_STORAGE_KEY)).toBe("zh-CN"); firstRender.unmount(); render(); expect(screen.getByTestId("locale")).toHaveTextContent("zh-CN"); - expect(screen.getByTestId("title")).toHaveTextContent("Fami"); + expect(screen.getByTestId("title")).toHaveTextContent("外管后台"); }); }); diff --git a/external-admin/src/i18n/messages.js b/external-admin/src/i18n/messages.js index cb399f3..71b14ec 100644 --- a/external-admin/src/i18n/messages.js +++ b/external-admin/src/i18n/messages.js @@ -10,8 +10,8 @@ export const EXTERNAL_LANGUAGE_OPTIONS = Object.freeze([ ]); const en = { - "app.title": "Fami", - "app.shortTitle": "Fami", + "app.title": "External Admin", + "app.shortTitle": "External Admin", "language.label": "Language", "common.actions": "Actions", "common.active": "Active", @@ -321,8 +321,8 @@ const en = { }; const zhCN = { - "app.title": "Fami", - "app.shortTitle": "Fami", + "app.title": "外管后台", + "app.shortTitle": "外管后台", "language.label": "语言", "common.actions": "操作", "common.active": "正常", "common.all": "全部", "common.allStatuses": "全部状态", "common.banned": "已封禁", "common.cancel": "取消", "common.confirm": "确认", "common.create": "创建", "common.disabled": "禁用", "common.edit": "编辑", "common.enabled": "启用", "common.expired": "过期", "common.loading": "正在加载", "common.noData": "当前无数据", "common.normal": "正常", "common.query": "查询", "common.reason": "原因", "common.reload": "重新加载", "common.retry": "重试", "common.save": "保存", "common.status": "状态", "common.user": "用户", "auth.account": "外管账号", "auth.changePassword": "修改外管密码", "auth.checkingSession": "正在校验外管会话", "auth.confirmNewPassword": "确认新密码", "auth.currentPassword": "当前密码", "auth.fillAccountPassword": "请填写账号和密码", "auth.firstLoginChange": "首次登录必须修改密码", "auth.hidePassword": "隐藏密码", "auth.invalidCredentials": "账号或密码错误", "auth.login": "登录", "auth.loginFailed": "登录失败", "auth.logout": "退出登录", "auth.newPassword": "新密码", "auth.noPermission": "当前外管账号没有访问此功能的权限。", "auth.password": "密码", "auth.passwordChangedFailed": "密码修改失败", "auth.passwordMismatch": "两次输入的新密码不一致", "auth.passwordMin": "至少 8 个字符", "auth.passwordMinError": "新密码至少 8 个字符", "auth.passwordWhitespace": "新密码不能为空或全为空白字符", "auth.recheck": "重新校验", "auth.returnOverview": "返回权限总览", "auth.savePassword": "保存密码", "auth.sessionFailed": "会话校验失败", "auth.showPassword": "显示密码", @@ -345,7 +345,7 @@ const zhCN = { const ar = { ...en, - "app.title": "Fami", "app.shortTitle": "Fami", "language.label": "اللغة", + "app.title": "الإدارة الخارجية", "app.shortTitle": "الإدارة الخارجية", "language.label": "اللغة", "common.actions": "الإجراءات", "common.active": "نشط", "common.all": "الكل", "common.allStatuses": "كل الحالات", "common.banned": "محظور", "common.cancel": "إلغاء", "common.confirm": "تأكيد", "common.create": "إنشاء", "common.disabled": "معطّل", "common.edit": "تعديل", "common.enabled": "مفعّل", "common.expired": "منتهي", "common.loading": "جارٍ التحميل", "common.noData": "لا توجد بيانات", "common.normal": "طبيعي", "common.query": "بحث", "common.reason": "السبب", "common.reload": "إعادة التحميل", "common.retry": "إعادة المحاولة", "common.save": "حفظ", "common.status": "الحالة", "common.user": "المستخدم", "auth.account": "حساب الإدارة الخارجية", "auth.changePassword": "تغيير كلمة مرور الإدارة الخارجية", "auth.checkingSession": "جارٍ التحقق من الجلسة", "auth.confirmNewPassword": "تأكيد كلمة المرور الجديدة", "auth.currentPassword": "كلمة المرور الحالية", "auth.fillAccountPassword": "أدخل الحساب وكلمة المرور", "auth.firstLoginChange": "يجب تغيير كلمة المرور عند تسجيل الدخول لأول مرة", "auth.hidePassword": "إخفاء كلمة المرور", "auth.invalidCredentials": "الحساب أو كلمة المرور غير صحيحة", "auth.login": "تسجيل الدخول", "auth.loginFailed": "فشل تسجيل الدخول", "auth.logout": "تسجيل الخروج", "auth.newPassword": "كلمة المرور الجديدة", "auth.noPermission": "لا يملك هذا الحساب صلاحية الوصول إلى هذه الميزة.", "auth.password": "كلمة المرور", "auth.passwordChangedFailed": "فشل تغيير كلمة المرور", "auth.passwordMismatch": "كلمتا المرور الجديدتان غير متطابقتين", "auth.passwordMin": "8 أحرف على الأقل", "auth.passwordMinError": "يجب أن تتكون كلمة المرور الجديدة من 8 أحرف على الأقل", "auth.passwordWhitespace": "لا يمكن أن تكون كلمة المرور الجديدة فارغة أو مسافات فقط", "auth.recheck": "إعادة التحقق", "auth.returnOverview": "العودة إلى ملخص الصلاحيات", "auth.savePassword": "حفظ كلمة المرور", "auth.sessionFailed": "فشل التحقق من الجلسة", "auth.showPassword": "إظهار كلمة المرور", "nav.expand": "فتح القائمة", "nav.overview": "ملخص الصلاحيات", "nav.users": "إدارة المستخدمين", "nav.bans": "إدارة الحظر", "nav.hosts": "المضيفون", "nav.agencies": "الوكالات", "nav.bds": "قائمة BD", "nav.bdManagers": "مديرو BD", "nav.superAdmins": "المشرفون العامون", "nav.rooms": "إدارة الغرف", "nav.grants": "الموارد والمعرّفات المميزة", "nav.banners": "إدارة اللافتات", "nav.team": "فريقي", @@ -367,7 +367,7 @@ const ar = { const tr = { ...en, - "app.title": "Fami", "app.shortTitle": "Fami", "language.label": "Dil", + "app.title": "Harici Yönetim", "app.shortTitle": "Harici Yönetim", "language.label": "Dil", "common.actions": "İşlemler", "common.active": "Aktif", "common.all": "Tümü", "common.allStatuses": "Tüm durumlar", "common.banned": "Yasaklı", "common.cancel": "İptal", "common.confirm": "Onayla", "common.create": "Oluştur", "common.disabled": "Devre dışı", "common.edit": "Düzenle", "common.enabled": "Etkin", "common.expired": "Süresi dolmuş", "common.loading": "Yükleniyor", "common.noData": "Veri yok", "common.normal": "Normal", "common.query": "Ara", "common.reason": "Neden", "common.reload": "Yeniden yükle", "common.retry": "Tekrar dene", "common.save": "Kaydet", "common.status": "Durum", "common.user": "Kullanıcı", "auth.account": "Harici yönetim hesabı", "auth.changePassword": "Harici yönetim parolasını değiştir", "auth.checkingSession": "Oturum kontrol ediliyor", "auth.confirmNewPassword": "Yeni parolayı doğrula", "auth.currentPassword": "Mevcut parola", "auth.fillAccountPassword": "Hesabınızı ve parolanızı girin", "auth.firstLoginChange": "İlk girişte parolanızı değiştirmeniz gerekir", "auth.hidePassword": "Parolayı gizle", "auth.invalidCredentials": "Hesap veya parola hatalı", "auth.login": "Giriş yap", "auth.loginFailed": "Giriş başarısız", "auth.logout": "Çıkış yap", "auth.newPassword": "Yeni parola", "auth.noPermission": "Bu harici yönetim hesabının bu özelliğe erişimi yok.", "auth.password": "Parola", "auth.passwordChangedFailed": "Parola değiştirilemedi", "auth.passwordMismatch": "Yeni parolalar eşleşmiyor", "auth.passwordMin": "En az 8 karakter", "auth.passwordMinError": "Yeni parola en az 8 karakter olmalıdır", "auth.passwordWhitespace": "Yeni parola boş veya yalnızca boşluk olamaz", "auth.recheck": "Tekrar kontrol et", "auth.returnOverview": "Yetki özetine dön", "auth.savePassword": "Parolayı kaydet", "auth.sessionFailed": "Oturum doğrulanamadı", "auth.showPassword": "Parolayı göster", "nav.expand": "Menüyü aç", "nav.overview": "Yetki özeti", "nav.users": "Kullanıcı yönetimi", "nav.bans": "Yasak yönetimi", "nav.hosts": "Yayıncılar", "nav.agencies": "Ajanslar", "nav.bds": "BD listesi", "nav.bdManagers": "BD Yöneticileri", "nav.superAdmins": "Süper Yöneticiler", "nav.rooms": "Oda yönetimi", "nav.grants": "Kaynaklar ve özel ID'ler", "nav.banners": "Banner yönetimi", "nav.team": "Ekibim", diff --git a/external-admin/src/i18n/messages.test.js b/external-admin/src/i18n/messages.test.js index 08ee1cf..19c7cf1 100644 --- a/external-admin/src/i18n/messages.test.js +++ b/external-admin/src/i18n/messages.test.js @@ -13,10 +13,12 @@ describe("external admin translations", () => { test("uses native language names and Arabic RTL only", () => { expect(EXTERNAL_LANGUAGE_OPTIONS.map((item) => item.label)).toEqual(["English", "简体中文", "العربية", "Türkçe"]); - for (const locale of ["en", "zh-CN", "ar", "tr"]) { - expect(externalMessage(locale, "app.title")).toBe("Fami"); - expect(externalMessage(locale, "app.shortTitle")).toBe("Fami"); - } + expect(["en", "zh-CN", "ar", "tr"].map((locale) => externalMessage(locale, "app.title"))).toEqual([ + "External Admin", "外管后台", "الإدارة الخارجية", "Harici Yönetim" + ]); + expect(["en", "zh-CN", "ar", "tr"].map((locale) => externalMessage(locale, "app.shortTitle"))).toEqual([ + "External Admin", "外管后台", "الإدارة الخارجية", "Harici Yönetim" + ]); expect(externalDirection("ar")).toBe("rtl"); expect(externalDirection("en")).toBe("ltr"); expect(externalDirection("zh-CN")).toBe("ltr"); diff --git a/external-admin/src/layout/ExternalAdminLayout.jsx b/external-admin/src/layout/ExternalAdminLayout.jsx index 5354b28..aedfaf5 100644 --- a/external-admin/src/layout/ExternalAdminLayout.jsx +++ b/external-admin/src/layout/ExternalAdminLayout.jsx @@ -27,13 +27,14 @@ import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useTheme } from "@mui/material/styles"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { useExternalAuth } from "../auth/ExternalAuthProvider.jsx"; import { hasAnyExternalCapability } from "../config/capabilities.js"; import { useExternalI18n } from "../i18n/ExternalI18nProvider.jsx"; import { LanguageSwitcher } from "../i18n/LanguageSwitcher.jsx"; +import { useExternalSessionBrand } from "../shared/useExternalSessionBrand.js"; const drawerWidth = 248; const navigation = [ @@ -64,19 +65,7 @@ export function ExternalAdminLayout() { [session] ); const current = [...visibleNavigation].reverse().find((item) => location.pathname.startsWith(item.path)); - - useEffect(() => { - // Login resolves the account's App server-side. Both the visible brand and browser metadata must use that apps row rather than a bundled image or client-selected tenant. - const appName = String(session?.appName || session?.appCode || t("app.title")).trim(); - const logoUrl = String(session?.logoUrl || "").trim(); - globalThis.document.title = appName; - syncBrandFavicon(logoUrl); - return () => { - // Leaving an authenticated App session must not leak its brand into the next account's login screen. - globalThis.document.title = t("app.title"); - syncBrandFavicon(""); - }; - }, [session?.appCode, session?.appName, session?.logoUrl, t]); + useExternalSessionBrand(session, t("app.title")); const handleLogout = async () => { await logout(); @@ -157,19 +146,3 @@ export function ExternalAdminLayout() { ); } - -function syncBrandFavicon(logoUrl) { - const selector = 'link[data-external-app-favicon="true"]'; - const current = globalThis.document?.head?.querySelector(selector); - if (!logoUrl) { - current?.remove(); - return; - } - const link = current || globalThis.document.createElement("link"); - link.dataset.externalAppFavicon = "true"; - link.rel = "icon"; - link.href = logoUrl; - if (!current) { - globalThis.document.head.append(link); - } -} diff --git a/external-admin/src/layout/ExternalAdminLayout.test.jsx b/external-admin/src/layout/ExternalAdminLayout.test.jsx index 6f158b9..7419cad 100644 --- a/external-admin/src/layout/ExternalAdminLayout.test.jsx +++ b/external-admin/src/layout/ExternalAdminLayout.test.jsx @@ -69,7 +69,7 @@ describe("ExternalAdminLayout RTL", () => { expect(screen.getByRole("combobox", { name: "اللغة" })).toHaveTextContent("العربية"); view.unmount(); - expect(document.title).toBe("Fami"); + expect(document.title).toBe("الإدارة الخارجية"); expect(document.head.querySelector('link[data-external-app-favicon="true"]')).not.toBeInTheDocument(); }); @@ -89,7 +89,7 @@ describe("ExternalAdminLayout RTL", () => { expect(document.head.querySelector('link[data-external-app-favicon="true"]')).toHaveAttribute("href", "https://media.example.com/apps/lalu.png"); view.unmount(); - expect(document.title).toBe("Fami"); + expect(document.title).toBe("الإدارة الخارجية"); expect(document.head.querySelector('link[data-external-app-favicon="true"]')).not.toBeInTheDocument(); }); }); diff --git a/external-admin/src/pages/ChangePasswordPage.jsx b/external-admin/src/pages/ChangePasswordPage.jsx index ca8c517..02b533f 100644 --- a/external-admin/src/pages/ChangePasswordPage.jsx +++ b/external-admin/src/pages/ChangePasswordPage.jsx @@ -1,6 +1,4 @@ -import KeyOutlined from "@mui/icons-material/KeyOutlined"; import Alert from "@mui/material/Alert"; -import Avatar from "@mui/material/Avatar"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Card from "@mui/material/Card"; @@ -10,10 +8,12 @@ import TextField from "@mui/material/TextField"; import Typography from "@mui/material/Typography"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; +import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { useExternalAuth } from "../auth/ExternalAuthProvider.jsx"; import { useExternalI18n } from "../i18n/ExternalI18nProvider.jsx"; import { LanguageSwitcher } from "../i18n/LanguageSwitcher.jsx"; import { translateExternalError } from "../i18n/errors.js"; +import { useExternalSessionBrand } from "../shared/useExternalSessionBrand.js"; export default function ChangePasswordPage() { const { changePassword, logout, session } = useExternalAuth(); @@ -22,6 +22,7 @@ export default function ChangePasswordPage() { const [error, setError] = useState(""); const [submitting, setSubmitting] = useState(false); const navigate = useNavigate(); + useExternalSessionBrand(session, t("app.title")); const submit = async (event) => { event.preventDefault(); @@ -59,7 +60,18 @@ export default function ChangePasswordPage() { - {t("auth.changePassword")}{session?.passwordChangeRequired ? t("auth.firstLoginChange") : session?.account} + + + {t("auth.changePassword")} + {session?.passwordChangeRequired ? t("auth.firstLoginChange") : session?.account} + {error ? {error} : null} setForm({ ...form, oldPassword: event.target.value })} /> diff --git a/external-admin/src/pages/ChangePasswordPage.test.jsx b/external-admin/src/pages/ChangePasswordPage.test.jsx index 51351d7..c1f6e4f 100644 --- a/external-admin/src/pages/ChangePasswordPage.test.jsx +++ b/external-admin/src/pages/ChangePasswordPage.test.jsx @@ -7,7 +7,13 @@ import ChangePasswordPage from "./ChangePasswordPage.jsx"; const auth = vi.hoisted(() => ({ changePassword: vi.fn(), logout: vi.fn(), - session: { account: "fami-manager", passwordChangeRequired: true } + session: { + account: "fami-manager", + appCode: "fami", + appName: "Fami", + logoUrl: "https://media.example.com/apps/fami.png", + passwordChangeRequired: true + } })); vi.mock("../auth/ExternalAuthProvider.jsx", () => ({ useExternalAuth: () => auth })); @@ -16,6 +22,29 @@ describe("ChangePasswordPage", () => { beforeEach(() => { auth.changePassword.mockReset().mockResolvedValue({}); auth.logout.mockReset().mockResolvedValue(undefined); + auth.session = { + account: "fami-manager", + appCode: "fami", + appName: "Fami", + logoUrl: "https://media.example.com/apps/fami.png", + passwordChangeRequired: true + }; + }); + + test("shows the App brand resolved by the authenticated session", () => { + auth.session = { + ...auth.session, + account: "lalu-manager", + appCode: "lalu", + appName: "Lalu", + logoUrl: "https://media.example.com/apps/lalu.png" + }; + const view = render(); + + expect(screen.getByText("Lalu")).toBeInTheDocument(); + expect(view.container.querySelector("img")).toHaveAttribute("src", "https://media.example.com/apps/lalu.png"); + expect(document.title).toBe("Lalu"); + expect(document.head.querySelector('link[data-external-app-favicon="true"]')).toHaveAttribute("href", "https://media.example.com/apps/lalu.png"); }); test("blocks a whitespace-only new password", async () => { diff --git a/external-admin/src/pages/LoginPage.jsx b/external-admin/src/pages/LoginPage.jsx index c9a2747..963cc02 100644 --- a/external-admin/src/pages/LoginPage.jsx +++ b/external-admin/src/pages/LoginPage.jsx @@ -28,6 +28,12 @@ export default function LoginPage() { const navigate = useNavigate(); const location = useLocation(); + useEffect(() => { + // 登录成功前没有可信 App 上下文;使用通用标题并清掉上一个会话的图标,避免泄露或误导用户所属 App。 + globalThis.document.title = t("app.title"); + globalThis.document.head.querySelector('link[data-external-app-favicon="true"]')?.remove(); + }, [t]); + useEffect(() => { if (!initializing && session) { const destination = session.passwordChangeRequired ? "/change-password" : location.state?.from?.pathname || "/overview"; diff --git a/external-admin/src/pages/LoginPage.test.jsx b/external-admin/src/pages/LoginPage.test.jsx index eb4ed36..000df23 100644 --- a/external-admin/src/pages/LoginPage.test.jsx +++ b/external-admin/src/pages/LoginPage.test.jsx @@ -18,12 +18,22 @@ describe("LoginPage", () => { test("defaults to English, has no App field and submits only account and password", async () => { const user = userEvent.setup(); + const staleFavicon = document.createElement("link"); + staleFavicon.dataset.externalAppFavicon = "true"; + staleFavicon.rel = "icon"; + staleFavicon.href = "https://media.example.com/apps/lalu.png"; + document.head.append(staleFavicon); + document.title = "Lalu"; const { container } = renderLogin(); - expect(screen.getByRole("heading", { name: "Fami" })).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: "External Admin" })).toBeInTheDocument(); expect(container.querySelector("img")).toBeNull(); + expect(document.title).toBe("External Admin"); + expect(document.head.querySelector('link[data-external-app-favicon="true"]')).not.toBeInTheDocument(); expect(screen.queryByRole("combobox", { name: "App" })).not.toBeInTheDocument(); await user.type(screen.getByRole("textbox", { name: "External admin account" }), "fami-manager"); + expect(screen.getByRole("heading", { name: "External Admin" })).toBeInTheDocument(); + expect(container.querySelector("img")).toBeNull(); await user.type(container.querySelector('input[autocomplete="current-password"]'), "secret-password"); await user.click(screen.getByRole("button", { name: "Sign in" })); diff --git a/external-admin/src/shared/useExternalSessionBrand.js b/external-admin/src/shared/useExternalSessionBrand.js new file mode 100644 index 0000000..f7cbbef --- /dev/null +++ b/external-admin/src/shared/useExternalSessionBrand.js @@ -0,0 +1,32 @@ +import { useEffect } from "react"; + +export function useExternalSessionBrand(session, fallbackTitle) { + const appName = String(session?.appName || session?.appCode || fallbackTitle || "External Admin").trim(); + const logoUrl = String(session?.logoUrl || "").trim(); + + useEffect(() => { + // 认证后的 App 品牌只采用服务端 session;session 又来自 apps 行,前端不解析账号名称猜租户。 + globalThis.document.title = appName; + syncBrandFavicon(logoUrl); + return () => { + globalThis.document.title = fallbackTitle || "External Admin"; + syncBrandFavicon(""); + }; + }, [appName, fallbackTitle, logoUrl]); +} + +function syncBrandFavicon(logoUrl) { + const selector = 'link[data-external-app-favicon="true"]'; + const current = globalThis.document?.head?.querySelector(selector); + if (!logoUrl) { + current?.remove(); + return; + } + const link = current || globalThis.document.createElement("link"); + link.dataset.externalAppFavicon = "true"; + link.rel = "icon"; + link.href = logoUrl; + if (!current) { + globalThis.document.head.append(link); + } +} diff --git a/external-admin/src/styles/index.css b/external-admin/src/styles/index.css index d0ed2ac..3bff03d 100644 --- a/external-admin/src/styles/index.css +++ b/external-admin/src/styles/index.css @@ -35,9 +35,9 @@ body { min-width: 140px !important; } -.external-login-avatar { - background: var(--brand-gradient) !important; - color: #fff; +.external-change-password-brand { + font-size: 18px; + font-weight: 750; } .external-full-state { diff --git a/src/shared/ui/AppIdentity.test.jsx b/src/shared/ui/AppIdentity.test.jsx index a4d3c88..ac57526 100644 --- a/src/shared/ui/AppIdentity.test.jsx +++ b/src/shared/ui/AppIdentity.test.jsx @@ -39,3 +39,17 @@ test("can hide the fallback when a surface must only show a database logo", () = expect(screen.getByText("Fami")).toBeInTheDocument(); expect(screen.queryByText("F", { selector: "span" })).not.toBeInTheDocument(); }); + +test("does not invent a fallback when an external portal database logo fails to load", () => { + const { container } = render( + , + ); + + fireEvent.error(container.querySelector("img")); + expect(container.querySelector("img")).toBeNull(); + expect(screen.getByText("Fami")).toBeInTheDocument(); + expect(screen.queryByText("F", { selector: "span" })).not.toBeInTheDocument(); +});