From 5a9290d3f24d0a86b0232fb769d4bff84ba7b940 Mon Sep 17 00:00:00 2001 From: Blake Thomas Williams Date: Fri, 20 Mar 2026 14:57:32 -0500 Subject: [PATCH 1/5] Updated processor to return only visible surfaces See https://github.com/google/A2UI/issues/898 for more details. --- .../src/v0_8/data/model-processor.test.ts | 30 +++++++++++++++++-- .../web_core/src/v0_8/data/model-processor.ts | 13 +++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/renderers/web_core/src/v0_8/data/model-processor.test.ts b/renderers/web_core/src/v0_8/data/model-processor.test.ts index a4d280f2a..b6f151237 100644 --- a/renderers/web_core/src/v0_8/data/model-processor.test.ts +++ b/renderers/web_core/src/v0_8/data/model-processor.test.ts @@ -29,12 +29,12 @@ describe("A2uiMessageProcessor", () => { it("handles beginRendering", () => { processor.processMessages([ { - beginRendering: { + beginRendering: { surfaceId: "s1", root: "root", - styles: { font: "Arial" }, - }, + styles: { font: "Arial" }, }, + }, ]); const surfaces = processor.getSurfaces(); @@ -79,6 +79,30 @@ describe("A2uiMessageProcessor", () => { assert.deepStrictEqual(root.properties.text, { literal: "Hello" }); }); + it("handles surfaceUpdate without beginRendering", () => { + const surfaceId = "s1"; + processor.processMessages([ + { + surfaceUpdate: { + surfaceId, + components: [ + { + id: "root", + component: { + Text: { text: { literal: "Hello" }, usageHint: "body" }, + } as any, + }, + ], + }, + }, + ]); + + // Should filter out the surface, as the processor treats it as not ready + // to render without a beginRendering message + const surface = processor.getSurfaces().get(surfaceId); + assert.equal(surface, undefined); + }); + it("handles dataModelUpdate", () => { processor.processMessages([ { diff --git a/renderers/web_core/src/v0_8/data/model-processor.ts b/renderers/web_core/src/v0_8/data/model-processor.ts index e5f264c99..2347ed59b 100644 --- a/renderers/web_core/src/v0_8/data/model-processor.ts +++ b/renderers/web_core/src/v0_8/data/model-processor.ts @@ -87,7 +87,18 @@ export class A2uiMessageProcessor implements MessageProcessor { } getSurfaces(): ReadonlyMap { - return this.surfaces; + const allSurfaces = this.surfaces; + // NOTE: If a message with a `surfaceUpdate` is processed prior to a + // `beginRendering` message, the surface is still returned, but it will + // throw an error when attempting to render it due to the missing + // `rootComponentId`. + const visibleSurfaces = new Map(); + for (const [surfaceId, surface] of allSurfaces) { + if (surface.rootComponentId) { + visibleSurfaces.set(surfaceId, surface); + } + } + return visibleSurfaces; } clearSurfaces() { From a5380bf2b9a8c6fc2cabf53ec3c202777da911a6 Mon Sep 17 00:00:00 2001 From: Blake Thomas Williams Date: Fri, 20 Mar 2026 15:28:45 -0500 Subject: [PATCH 2/5] Updated Angular message processor to use base processor's getSurfaces method With the base processor filtering surfaces now, this ensures the Angular implementation is consistent. --- .../angular/src/v0_8/data/processor.spec.ts | 41 +++++++++++++++++++ renderers/angular/src/v0_8/data/processor.ts | 18 +++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/renderers/angular/src/v0_8/data/processor.spec.ts b/renderers/angular/src/v0_8/data/processor.spec.ts index b2fa3f231..a3f664b40 100644 --- a/renderers/angular/src/v0_8/data/processor.spec.ts +++ b/renderers/angular/src/v0_8/data/processor.spec.ts @@ -121,4 +121,45 @@ describe('MessageProcessor', () => { expect(baseProcessor.clearSurfaces).toHaveBeenCalled(); }); + + it('should only return surfaces that are ready to render', () => { + // NOTE: This state can occur if a `surfaceUpdate` message is processed + // before a `beginRendering` message for the same surface. + const readySurfaceId = 'ready-surface-id'; + const readyComponentId = 'ready-component-id'; + const notReadySurfaceId = 'not-ready-surface-id'; + const readySurface: WebCore.Surface = { + rootComponentId: readyComponentId, + componentTree: { + id: readyComponentId, + type: 'Text', + properties: { + text: {literalString: 'Ready to render'}, + } + }, + dataModel: new Map(), + components: new Map(), + styles: {}, + }; + const notReadySurface: WebCore.Surface = { + rootComponentId: null, + componentTree: null, + dataModel: new Map(), + components: new Map(), + styles: {}, + }; + // Add both surfaces to the base processor's surfaces map + const baseProcessor = (service as any).baseProcessor; + const surfaces = new Map([ + [readySurfaceId, readySurface], + [notReadySurfaceId, notReadySurface], + ]); + baseProcessor.surfaces = surfaces; + + const returnedSurfaces = service.getSurfaces(); + + expect(returnedSurfaces.size).toBe(1); + expect(returnedSurfaces.get(readySurfaceId)).toBe(readySurface); + expect(returnedSurfaces.get(notReadySurfaceId)).toBeUndefined(); + }); }); diff --git a/renderers/angular/src/v0_8/data/processor.ts b/renderers/angular/src/v0_8/data/processor.ts index 55410bddb..504acabe8 100644 --- a/renderers/angular/src/v0_8/data/processor.ts +++ b/renderers/angular/src/v0_8/data/processor.ts @@ -90,10 +90,26 @@ export class MessageProcessor { } getSurfaces(): Map { - return (this.baseProcessor as any).surfaces || new Map(); + return this.baseProcessor.getSurfaces() as Map; } clearSurfaces() { this.baseProcessor.clearSurfaces(); } } + +/** Filters out surfaces that are not ready to render. */ +function filterToVisibleSurfaces( + surfaces: Map, +): Map { + // NOTE: If a message with a `surfaceUpdate` is processed prior to a + // `beginRendering` message, the surface is still returned, but it won't have + // a root component to render. + const visibleSurfaces = new Map(); + for (const [surfaceId, surface] of surfaces) { + if (surface.rootComponentId && surface.componentTree) { + visibleSurfaces.set(surfaceId, surface); + } + } + return visibleSurfaces; +} \ No newline at end of file From db6b401f13923f3200771601539edd4931dbce7f Mon Sep 17 00:00:00 2001 From: Blake Thomas Williams Date: Fri, 20 Mar 2026 15:35:36 -0500 Subject: [PATCH 3/5] Formatted corresponding documents --- renderers/angular/src/v0_8/data/processor.spec.ts | 2 +- renderers/web_core/src/v0_8/data/model-processor.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/renderers/angular/src/v0_8/data/processor.spec.ts b/renderers/angular/src/v0_8/data/processor.spec.ts index a3f664b40..4afc1a9e1 100644 --- a/renderers/angular/src/v0_8/data/processor.spec.ts +++ b/renderers/angular/src/v0_8/data/processor.spec.ts @@ -134,7 +134,7 @@ describe('MessageProcessor', () => { id: readyComponentId, type: 'Text', properties: { - text: {literalString: 'Ready to render'}, + text: { literalString: 'Ready to render' }, } }, dataModel: new Map(), diff --git a/renderers/web_core/src/v0_8/data/model-processor.test.ts b/renderers/web_core/src/v0_8/data/model-processor.test.ts index b6f151237..d7d60c0bd 100644 --- a/renderers/web_core/src/v0_8/data/model-processor.test.ts +++ b/renderers/web_core/src/v0_8/data/model-processor.test.ts @@ -29,12 +29,12 @@ describe("A2uiMessageProcessor", () => { it("handles beginRendering", () => { processor.processMessages([ { - beginRendering: { + beginRendering: { surfaceId: "s1", root: "root", - styles: { font: "Arial" }, + styles: { font: "Arial" }, + }, }, - }, ]); const surfaces = processor.getSurfaces(); From 8ad9d7908b72eb67f8f74d1ca9cab7b99576b8e1 Mon Sep 17 00:00:00 2001 From: Blake Thomas Williams Date: Fri, 20 Mar 2026 15:43:16 -0500 Subject: [PATCH 4/5] Fixed lit A2uiMessageProcessor tests temp --- renderers/lit/src/0.8/model.test.ts | 59 ++++++++++++++++++----------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/renderers/lit/src/0.8/model.test.ts b/renderers/lit/src/0.8/model.test.ts index 7703d11e7..f21772aff 100644 --- a/renderers/lit/src/0.8/model.test.ts +++ b/renderers/lit/src/0.8/model.test.ts @@ -95,15 +95,23 @@ describe("A2uiMessageProcessor", () => { }); it("should handle `surfaceUpdate` by adding components", () => { + const surfaceId = "@default"; + const rootComponentId = "comp-a"; const messages = [ + { + beginRendering: { + surfaceId, + root: rootComponentId, + }, + }, { surfaceUpdate: { - surfaceId: "@default", + surfaceId, components: [ { - id: "comp-a", + id: rootComponentId, component: { - Text: { usageHint: "body", text: { literalString: "Hi" } }, + Text: { usageHint: "body", text: { literalString: "Hi" } }, }, }, ], @@ -111,12 +119,12 @@ describe("A2uiMessageProcessor", () => { }, ]; processor.processMessages(messages); - const surface = processor.getSurfaces().get("@default"); + const surface = processor.getSurfaces().get(surfaceId); if (!surface) { assert.fail("No default surface"); } assert.strictEqual(surface!.components.size, 1); - assert.ok(surface!.components.has("comp-a")); + assert.ok(surface!.components.has(rootComponentId)); }); it("should handle `deleteSurface`", () => { @@ -353,7 +361,7 @@ describe("A2uiMessageProcessor", () => { { id: "child", component: { - Text: { usageHint: "body", text: { literalString: "Hello" } }, + Text: { usageHint: "body", text: { literalString: "Hello" } }, }, }, ], @@ -444,7 +452,7 @@ describe("A2uiMessageProcessor", () => { }, { id: "item-template", - component: { Text: { usageHint: "body", text: { path: "/name" } } }, + component: { Text: { usageHint: "body", text: { path: "/name" } } }, }, ], }, @@ -498,7 +506,7 @@ describe("A2uiMessageProcessor", () => { }, { id: "item-template", - component: { Text: { usageHint: "body", text: { path: "/name" } } }, + component: { Text: { usageHint: "body", text: { path: "/name" } } }, }, ], }, @@ -576,7 +584,7 @@ describe("A2uiMessageProcessor", () => { // These paths would are typical when a databinding is used. { id: "item-template", - component: { Text: { usageHint: "body", text: { path: "./item/name" } } }, + component: { Text: { usageHint: "body", text: { path: "./item/name" } } }, }, ], }, @@ -634,7 +642,7 @@ describe("A2uiMessageProcessor", () => { // These paths would are typical when a databinding is used. { id: "item-template", - component: { Text: { usageHint: "body", text: { path: "./name" } } }, + component: { Text: { usageHint: "body", text: { path: "./name" } } }, }, ], }, @@ -731,7 +739,8 @@ describe("A2uiMessageProcessor", () => { { id: "title-heading", component: { - Text: { usageHint: "body", + Text: { + usageHint: "body", text: { literalString: "Top Restaurants", }, @@ -774,7 +783,8 @@ describe("A2uiMessageProcessor", () => { id: "template-image", weight: 1, component: { - Image: { usageHint: "largeFeature", + Image: { + usageHint: "largeFeature", url: { path: "imageUrl", }, @@ -801,7 +811,8 @@ describe("A2uiMessageProcessor", () => { { id: "template-name", component: { - Text: { usageHint: "body", + Text: { + usageHint: "body", text: { path: "name", }, @@ -811,7 +822,8 @@ describe("A2uiMessageProcessor", () => { { id: "template-rating", component: { - Text: { usageHint: "body", + Text: { + usageHint: "body", text: { path: "rating", }, @@ -821,7 +833,8 @@ describe("A2uiMessageProcessor", () => { { id: "template-detail", component: { - Text: { usageHint: "body", + Text: { + usageHint: "body", text: { path: "detail", }, @@ -831,7 +844,8 @@ describe("A2uiMessageProcessor", () => { { id: "template-link", component: { - Text: { usageHint: "body", + Text: { + usageHint: "body", text: { path: "infoLink", }, @@ -872,7 +886,8 @@ describe("A2uiMessageProcessor", () => { { id: "book-now-text", component: { - Text: { usageHint: "body", + Text: { + usageHint: "body", text: { literalString: "Book Now", }, @@ -1133,7 +1148,7 @@ describe("A2uiMessageProcessor", () => { { id: "day-title", component: { - Text: { usageHint: "body", text: { path: "title" } }, + Text: { usageHint: "body", text: { path: "title" } }, }, }, { @@ -1151,7 +1166,7 @@ describe("A2uiMessageProcessor", () => { }, { id: "activity-text", - component: { Text: { usageHint: "body", text: { path: "." } } }, + component: { Text: { usageHint: "body", text: { path: "." } } }, }, ], }, @@ -1228,7 +1243,7 @@ describe("A2uiMessageProcessor", () => { }, }, }, - { id: "tag", component: { Text: { usageHint: "body", text: { path: "." } } } }, + { id: "tag", component: { Text: { usageHint: "body", text: { path: "." } } } }, ], }, }, @@ -1269,7 +1284,7 @@ describe("A2uiMessageProcessor", () => { components: [ { id: "comp-a", - component: { Text: { usageHint: "body", text: { path: "/name" } } }, + component: { Text: { usageHint: "body", text: { path: "/name" } } }, }, ], }, @@ -1289,7 +1304,7 @@ describe("A2uiMessageProcessor", () => { components: [ { id: "comp-b", - component: { Text: { usageHint: "body", text: { path: "/name" } } }, + component: { Text: { usageHint: "body", text: { path: "/name" } } }, }, ], }, From d6d03afb8ba0023a1d2f6c74e3ca15e07ef777d7 Mon Sep 17 00:00:00 2001 From: Blake Thomas Williams Date: Fri, 20 Mar 2026 15:49:13 -0500 Subject: [PATCH 5/5] Removed unused function --- renderers/angular/src/v0_8/data/processor.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/renderers/angular/src/v0_8/data/processor.ts b/renderers/angular/src/v0_8/data/processor.ts index 504acabe8..bdca636a8 100644 --- a/renderers/angular/src/v0_8/data/processor.ts +++ b/renderers/angular/src/v0_8/data/processor.ts @@ -97,19 +97,3 @@ export class MessageProcessor { this.baseProcessor.clearSurfaces(); } } - -/** Filters out surfaces that are not ready to render. */ -function filterToVisibleSurfaces( - surfaces: Map, -): Map { - // NOTE: If a message with a `surfaceUpdate` is processed prior to a - // `beginRendering` message, the surface is still returned, but it won't have - // a root component to render. - const visibleSurfaces = new Map(); - for (const [surfaceId, surface] of surfaces) { - if (surface.rootComponentId && surface.componentTree) { - visibleSurfaces.set(surfaceId, surface); - } - } - return visibleSurfaces; -} \ No newline at end of file