FIX - Implement LB guest app layout and lifecycle bridge

Ensure custom LB apps are properly sized and visible by applying the required CSS contract on initialization and upon receiving the 'componentsLoaded' signal.
This commit is contained in:
Alec Schitzkat
2026-08-20 19:05:07 +02:00
committed by DerEchteAlec
parent 33e6b1aa46
commit 5dba13ce4b
2 changed files with 102 additions and 1 deletions
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { runInNewContext } from 'node:vm'
import type { ExternalPhoneAppDefinition } from '@/types/apps' import type { ExternalPhoneAppDefinition } from '@/types/apps'
import { import {
@@ -139,6 +140,79 @@ describe('LB Phone app bridge', () => {
expect(() => new Function(runtime)).not.toThrow() expect(() => new Function(runtime)).not.toThrow()
}) })
it('applies the LB iframe layout contract before revealing vendor apps', () => {
const document = createLbPhoneFrameDocument(
'<!doctype html><html><head></head><body style="visibility:hidden"></body></html>',
{
appName: 'radio-app',
localStorage: {},
resourceName: 'lb-radioapp',
settings: createLbPhoneHostSettings({
deviceName: 'Main phone',
isDarkMode: true,
language: 'en',
preferences: DEFAULT_PHONE_PREFERENCES,
securityEnabled: false,
}),
ui: 'https://cfx-nui-lb-radioapp/ui/dist/index.html',
},
)
const runtime = /<script>([\s\S]*?)<\/script>/.exec(document)?.[1]
expect(runtime).toBeTruthy()
const messageListeners: Array<(event: { data: unknown }) => void> = []
const readyListeners: Array<() => void> = []
const documentElement = { dataset: {}, style: {} }
const body = {
dataset: {},
style: { visibility: 'hidden' },
}
const sandbox = {
addEventListener(
eventName: string,
listener: (event: { data: unknown }) => void,
) {
if (eventName === 'message') messageListeners.push(listener)
},
componentsLoaded: undefined as boolean | undefined,
console,
document: {
addEventListener(eventName: string, listener: () => void) {
if (eventName === 'DOMContentLoaded') readyListeners.push(listener)
},
body,
documentElement,
},
parent: { postMessage() {} },
}
runInNewContext(runtime ?? '', sandbox)
expect(body.style.visibility).toBe('hidden')
expect(readyListeners).toHaveLength(1)
readyListeners[0]?.()
expect(documentElement.style).toMatchObject({
height: '100%',
margin: '0',
padding: '0',
width: '100%',
})
expect(body.dataset).toMatchObject({ device: 'phone', theme: 'dark' })
expect(body.style).toMatchObject({
height: '100%',
margin: '0',
padding: '0',
visibility: 'visible',
width: '100%',
})
body.style.visibility = 'hidden'
expect(messageListeners).toHaveLength(1)
messageListeners[0]?.({ data: 'componentsLoaded' })
expect(body.style.visibility).toBe('visible')
expect(sandbox.componentsLoaded).toBe(true)
})
it('persists isolated LB localStorage snapshots without app changes', () => { it('persists isolated LB localStorage snapshots without app changes', () => {
const values = new Map<string, string>() const values = new Map<string, string>()
const storage = { const storage = {
+28 -1
View File
@@ -130,6 +130,25 @@ function applySettings(nextSettings) {
if (document.body) document.body.dataset.theme = theme; if (document.body) document.body.dataset.theme = theme;
} }
function prepareDocument() {
Object.assign(document.documentElement.style, {
height: '100%',
margin: '0',
padding: '0',
width: '100%'
});
if (!document.body) return;
document.body.dataset.device = 'phone';
Object.assign(document.body.style, {
height: '100%',
margin: '0',
padding: '0',
visibility: 'visible',
width: '100%'
});
}
globalThis.resourceName = config.resourceName; globalThis.resourceName = config.resourceName;
globalThis.appName = config.appName; globalThis.appName = config.appName;
globalThis.components = globalThis.components ?? {}; globalThis.components = globalThis.components ?? {};
@@ -189,6 +208,11 @@ globalThis.getSettings = async () => globalThis.settings;
globalThis.addEventListener('message', (event) => { globalThis.addEventListener('message', (event) => {
const message = event.data; const message = event.data;
if (message === 'componentsLoaded') {
globalThis.componentsLoaded = true;
prepareDocument();
return;
}
if (!message || typeof message !== 'object') return; if (!message || typeof message !== 'object') return;
if (message.type === 'sky-phone:lb-settings') { if (message.type === 'sky-phone:lb-settings') {
@@ -210,7 +234,10 @@ globalThis.addEventListener('message', (event) => {
}); });
applySettings(config.settings); applySettings(config.settings);
document.addEventListener('DOMContentLoaded', () => applySettings(globalThis.settings), { once: true }); document.addEventListener('DOMContentLoaded', () => {
prepareDocument();
applySettings(globalThis.settings);
}, { once: true });
` `
function escapeAttribute(value: string): string { function escapeAttribute(value: string): string {