Files
lobehub__lobehub/plugins/vite/customBrandingLoadingScreen.test.ts
LiJian c5caffe684 💄 style: brand the loading screen and LobeHub provider card via the business slot (#18638)
* ✨ feat: brand the static loading screen for custom-branding deployments

The SPA entry html hardcodes the LobeHub wordmark SVG in #loading-brand,
so white-label deployments flash the LobeHub logo before the bundle
boots. Add a transformIndexHtml plugin that swaps it for BRANDING_NAME
(from the business-const slot) when custom branding is active; no-op for
default branding.

* 🐛 fix: expose dependency-free ./branding subpath for config-time imports

Importing the business-const package root from vite.config context fails
under Node's type-stripping loader (the barrel uses extensionless
relative imports). Expose branding.ts — a dependency-free leaf — as a
'./branding' subpath and deep-import it from the loading-screen plugin.
branding.ts must stay import-free for this to keep working.

* 💄 style: brand the LobeHub provider card via the business branding slot

Custom-branding distributions (BRANDING_NAME override) get their own
provider display name and drop the LobeHub marketing copy/links; OSS and
cloud keep the exact previous card.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: address codex review on the branding slot PR

- Import BRANDING_NAME from the business-const package root instead of
  the ./branding subpath in modelProviders/lobehub.ts: this file runs at
  normal app runtime, not vite-config time, so it never needed the
  dependency-free leaf — and the root export is what every override
  (including apps/desktop/stubs/business-const) already provides, so no
  companion change is needed there.
- Guard every `t(id.description, { defaultValue: description })` call
  against a falsy description before invoking t() at all, across Header,
  RelatedModels/Item, Related/Item, RelatedProviders/Item, ActionButton
  (share meta), and ProviderGrid/Card. Without the guard, a provider whose
  description was removed (LobeHub under custom branding) still displayed
  the old text for any locale whose already-translated providers.json
  hadn't been regenerated, since i18next only falls back to defaultValue
  when the key is missing, not when it's stale.
- Wire openSettings into ProviderConfig's fallback Button so the primary
  Config CTA still does something when both url and modelsUrl are empty
  (custom branding drops both) — it silently had no onClick before.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-24 15:43:18 +08:00

55 lines
2.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const SAMPLE_HTML = `<body>
<div id="loading-screen">
<div id="loading-brand" aria-label="Loading" role="status">
<svg fill="currentColor" height="40" viewBox="0 0 940 320" xmlns="http://www.w3.org/2000/svg">
<title>LobeHub</title>
<path d="M15 240.035V87.172h39.24V205.75h66.192v34.285H15z" />
</svg>
</div>
</div>
<div id="root" style="height: 100%"></div>
</body>`;
const loadHandler = async () => {
const { customBrandingLoadingScreen } = await import('./customBrandingLoadingScreen');
const plugin = customBrandingLoadingScreen();
return (plugin.transformIndexHtml as { handler: (html: string) => string }).handler;
};
describe('customBrandingLoadingScreen', () => {
beforeEach(() => {
vi.resetModules();
});
it('keeps the default LobeHub wordmark untouched', async () => {
vi.doMock('@lobechat/business-const/branding', () => ({ BRANDING_NAME: 'LobeHub' }));
const handler = await loadHandler();
expect(handler(SAMPLE_HTML)).toBe(SAMPLE_HTML);
});
it('replaces the wordmark with the custom brand name', async () => {
vi.doMock('@lobechat/business-const/branding', () => ({ BRANDING_NAME: 'AI Workstation' }));
const handler = await loadHandler();
const result = handler(SAMPLE_HTML);
expect(result).not.toContain('<svg');
expect(result).not.toContain('LobeHub');
expect(result).toContain('AI Workstation');
expect(result).toContain('id="loading-brand"');
// the rest of the document is preserved
expect(result).toContain('<div id="root" style="height: 100%"></div>');
});
it('escapes HTML-sensitive characters in the brand name', async () => {
vi.doMock('@lobechat/business-const/branding', () => ({ BRANDING_NAME: 'A<B>&"C' }));
const handler = await loadHandler();
const result = handler(SAMPLE_HTML);
expect(result).toContain('A&lt;B&gt;&amp;&quot;C');
expect(result).not.toContain('A<B>');
});
});