fix(ui): stop the add-on shell drawing a title over add-on pages

The app decides what its page shows. The shell drew the add-on name as a
header above every add-on page, so an add-on could not choose its own
title. The shell now draws no header on a live, loading or frozen
add-on page; each add-on titles its own pages. The unavailable and
missing-page states keep the name, because no add-on page is drawn there.

The runtime and shell specs now check that the shell adds no page title.

Assisted-by: Claude Code:claude-sonnet-5-5 node playwright
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-21 17:48:09 +00:00
1 parent 44810e5791
commit bf9ec9a1b0
3 files changed
+17 -13

No files matched your search

+11 -8
View File
@@ -88,7 +88,8 @@ test.describe('Add-on UI runtime', () => {
await expect(surface(page)).toBeVisible({ timeout: 20_000 })
await expect(page.getByTestId('fx-title')).toHaveText('Hello fixture')
await expect(page.getByTestId('fx-where')).toHaveText('page=home path=')
await expect(page.locator('h1.page-title')).toHaveText('UI fixture')
// The shell draws no title over the page: the add-on draws its own.
await expect(page.locator('h1.page-title')).toHaveCount(0)
// Every file came from the owner through the asset route, under one digest.
const js = answers.find(a => a.url.endsWith('/ui.js'))
const css = answers.find(a => a.url.endsWith('/ui/style.css'))
@@ -142,8 +143,9 @@ test.describe('Add-on UI runtime', () => {
return {
sheet: document.querySelector('style[data-addon-style="uifixture"]')?.textContent || '',
title: style(document.querySelector('[data-testid="fx-title"]')).color,
hostTitle: style(document.querySelector('h1.page-title')).color,
hostTitleSize: style(document.querySelector('h1.page-title')).fontSize,
hostNavColor: style(document.querySelector('.sidebar-nav a, .sidebar-nav button')).color,
hostNavSize: style(document.querySelector('.sidebar-nav a, .sidebar-nav button')).fontSize,
shellTitles: document.querySelectorAll('h1.page-title').length,
bodyBackground: style(document.body).backgroundColor,
token: style(root).getPropertyValue('--fx-token').trim(),
rootToken: style(document.documentElement).getPropertyValue('--fx-token').trim(),
@@ -151,8 +153,9 @@ test.describe('Add-on UI runtime', () => {
}
})
expect(probe.title).toBe('rgb(1, 2, 3)')
expect(probe.hostTitle).not.toBe('rgb(255, 0, 0)')
expect(probe.hostTitleSize).not.toBe('11px')
expect(probe.shellTitles).toBe(0)
expect(probe.hostNavColor).not.toBe('rgb(255, 0, 0)')
expect(probe.hostNavSize).not.toBe('11px')
expect(probe.bodyBackground).not.toBe('rgb(255, 0, 255)')
expect(probe.token).toBe('1')
expect(probe.rootToken).toBe('')
@@ -173,8 +176,8 @@ test.describe('Add-on UI runtime', () => {
await expect(card).toBeVisible({ timeout: 20_000 })
await expect(card).toContainText('This add-on had a problem')
await shoot(page, 'esm-error-boundary')
// The page header, the sidebar and the other routes are untouched.
await expect(page.locator('h1.page-title')).toHaveText('UI fixture')
// The sidebar and the other routes are untouched, and the shell adds no title.
await expect(page.locator('h1.page-title')).toHaveCount(0)
await expect(page.locator('.sidebar-nav')).toBeVisible()
await page.goto('/app/addons/uifixture/home')
await expect(page.getByTestId('fx-title')).toBeVisible({ timeout: 20_000 })
@@ -328,7 +331,7 @@ test.describe('Add-on UI runtime', () => {
// The router error page never replaces the host.
await expect(page.getByText('Unexpected Application Error')).toHaveCount(0)
await expect(page.locator('.sidebar-nav')).toBeVisible()
await expect(page.locator('h1.page-title')).toHaveText('UI fixture')
await expect(page.locator('h1.page-title')).toHaveCount(0)
await layer.getByRole('button', { name: 'Close' }).last().click()
await expect(layer).toHaveCount(0)
}
+4 -2
View File
@@ -46,8 +46,10 @@ test.describe('Add-on shell', () => {
await expect(page.getByTestId('fx-title')).toHaveText('Hello fixture')
await page.getByRole('button', { name: 'Increment' }).click()
await expect(surface.getByText('hooks 1')).toBeVisible()
// The sheet's `h1` rule never reaches the host's own page title.
await expect(page.locator('h1.page-title')).not.toHaveCSS('font-size', '11px')
// The shell draws no title over the add-on page, and the sheet's `h1`
// rule never reaches the host's own chrome.
await expect(page.locator('h1.page-title')).toHaveCount(0)
await expect(page.locator('.sidebar-nav a, .sidebar-nav button').first()).not.toHaveCSS('font-size', '11px')
await expect(page.locator('style[data-addon-style="uifixture"]')).toHaveCount(1)
// The detail view of the add-on links the declared navigation page.
await page.goto('/app/addons?detail=uifixture')
+2 -3
View File
@@ -154,7 +154,7 @@ export default function AddonRoute() {
if (registry.status === 'idle' || (registry.status === 'error' && !app)) {
if (registry.status === 'error') return unavailable('import_failed', reloadButton)
return (
<div className="page ad-page" data-testid="addon-loading"><PageHeader title={title} /><LoadingSpinner /></div>
<div className="page ad-page" data-testid="addon-loading"><LoadingSpinner /></div>
)
}
const isStale = Boolean(instance && instance.status === 'stale')
@@ -184,7 +184,6 @@ export default function AddonRoute() {
if (!instance || instance.status === 'loading') {
return (
<div className="page ad-page" data-testid="addon-loading">
<PageHeader title={title} />
<LoadingSpinner />
</div>
)
@@ -193,7 +192,7 @@ export default function AddonRoute() {
// survives the moment it is withdrawn.
return (
<div className="page ad-page" data-testid={isStale ? 'addon-stale' : 'addon-page'}>
<PageHeader title={title} />
{/* The add-on draws its own title; the shell adds none. */}
{isStale && (
<div className="ad-banner" role="alert">
<span>{t('shell.stale', { title })}</span>