diff --git a/renderers/angular/src/v0_8/data/processor.spec.ts b/renderers/angular/src/v0_8/data/processor.spec.ts index b2fa3f231..4afc1a9e1 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..bdca636a8 100644 --- a/renderers/angular/src/v0_8/data/processor.ts +++ b/renderers/angular/src/v0_8/data/processor.ts @@ -90,7 +90,7 @@ export class MessageProcessor { } getSurfaces(): Map { - return (this.baseProcessor as any).surfaces || new Map(); + return this.baseProcessor.getSurfaces() as Map; } clearSurfaces() { 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" } } }, }, ], }, 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..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 @@ -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() {