From 50c5a4b1e6ebe4ef597ef31eee4ab556b50bff7e Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 17 Mar 2026 10:04:52 -0700 Subject: [PATCH 1/3] Refactor v0.8 Angular renderer into versioned directory --- renderers/angular/src/public-api.ts | 22 +--- .../default.ts => v0_8/catalog/index.ts} | 0 .../lib/catalog => v0_8/components}/audio.ts | 0 .../lib/catalog => v0_8/components}/button.ts | 0 .../lib/catalog => v0_8/components}/card.ts | 0 .../catalog => v0_8/components}/checkbox.ts | 0 .../lib/catalog => v0_8/components}/column.ts | 0 .../components}/datetime-input.ts | 0 .../catalog => v0_8/components}/divider.ts | 0 .../lib/catalog => v0_8/components}/icon.ts | 0 .../lib/catalog => v0_8/components}/image.ts | 0 .../lib/catalog => v0_8/components}/list.ts | 0 .../lib/catalog => v0_8/components}/modal.ts | 0 .../components}/multiple-choice.ts | 0 .../lib/catalog => v0_8/components}/row.ts | 0 .../lib/catalog => v0_8/components}/slider.ts | 0 .../catalog => v0_8/components}/surface.ts | 0 .../lib/catalog => v0_8/components}/tabs.ts | 0 .../catalog => v0_8/components}/text-field.ts | 0 .../lib/catalog => v0_8/components}/text.ts | 0 .../lib/catalog => v0_8/components}/video.ts | 0 renderers/angular/{src/lib => v0_8}/config.ts | 0 .../angular/{src/lib => v0_8}/data/index.ts | 0 .../{src/lib => v0_8}/data/markdown.ts | 0 .../{src/lib => v0_8}/data/processor.ts | 0 .../angular/{src/lib => v0_8}/data/types.ts | 0 renderers/angular/v0_8/ng-package.json | 6 + renderers/angular/v0_8/public-api.ts | 24 ++++ .../{src/lib => v0_8}/rendering/catalog.ts | 0 .../rendering/dynamic-component.ts | 0 .../{src/lib => v0_8}/rendering/index.ts | 0 .../{src/lib => v0_8}/rendering/renderer.ts | 0 .../{src/lib => v0_8}/rendering/theming.ts | 0 renderers/angular/v0_8/types.ts | 109 ++++++++++++++++++ renderers/web_core/package-lock.json | 4 +- 35 files changed, 142 insertions(+), 23 deletions(-) rename renderers/angular/{src/lib/catalog/default.ts => v0_8/catalog/index.ts} (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/audio.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/button.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/card.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/checkbox.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/column.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/datetime-input.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/divider.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/icon.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/image.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/list.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/modal.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/multiple-choice.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/row.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/slider.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/surface.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/tabs.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/text-field.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/text.ts (100%) rename renderers/angular/{src/lib/catalog => v0_8/components}/video.ts (100%) rename renderers/angular/{src/lib => v0_8}/config.ts (100%) rename renderers/angular/{src/lib => v0_8}/data/index.ts (100%) rename renderers/angular/{src/lib => v0_8}/data/markdown.ts (100%) rename renderers/angular/{src/lib => v0_8}/data/processor.ts (100%) rename renderers/angular/{src/lib => v0_8}/data/types.ts (100%) create mode 100644 renderers/angular/v0_8/ng-package.json create mode 100644 renderers/angular/v0_8/public-api.ts rename renderers/angular/{src/lib => v0_8}/rendering/catalog.ts (100%) rename renderers/angular/{src/lib => v0_8}/rendering/dynamic-component.ts (100%) rename renderers/angular/{src/lib => v0_8}/rendering/index.ts (100%) rename renderers/angular/{src/lib => v0_8}/rendering/renderer.ts (100%) rename renderers/angular/{src/lib => v0_8}/rendering/theming.ts (100%) create mode 100644 renderers/angular/v0_8/types.ts diff --git a/renderers/angular/src/public-api.ts b/renderers/angular/src/public-api.ts index 07f26a00d..5fea6506b 100644 --- a/renderers/angular/src/public-api.ts +++ b/renderers/angular/src/public-api.ts @@ -1,21 +1 @@ -/* - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export * from './lib/rendering/index'; -export * from './lib/data/index'; -export * from './lib/config'; -export * from './lib/catalog/default'; -export { Surface } from './lib/catalog/surface'; +export * from '../v0_8/public-api'; diff --git a/renderers/angular/src/lib/catalog/default.ts b/renderers/angular/v0_8/catalog/index.ts similarity index 100% rename from renderers/angular/src/lib/catalog/default.ts rename to renderers/angular/v0_8/catalog/index.ts diff --git a/renderers/angular/src/lib/catalog/audio.ts b/renderers/angular/v0_8/components/audio.ts similarity index 100% rename from renderers/angular/src/lib/catalog/audio.ts rename to renderers/angular/v0_8/components/audio.ts diff --git a/renderers/angular/src/lib/catalog/button.ts b/renderers/angular/v0_8/components/button.ts similarity index 100% rename from renderers/angular/src/lib/catalog/button.ts rename to renderers/angular/v0_8/components/button.ts diff --git a/renderers/angular/src/lib/catalog/card.ts b/renderers/angular/v0_8/components/card.ts similarity index 100% rename from renderers/angular/src/lib/catalog/card.ts rename to renderers/angular/v0_8/components/card.ts diff --git a/renderers/angular/src/lib/catalog/checkbox.ts b/renderers/angular/v0_8/components/checkbox.ts similarity index 100% rename from renderers/angular/src/lib/catalog/checkbox.ts rename to renderers/angular/v0_8/components/checkbox.ts diff --git a/renderers/angular/src/lib/catalog/column.ts b/renderers/angular/v0_8/components/column.ts similarity index 100% rename from renderers/angular/src/lib/catalog/column.ts rename to renderers/angular/v0_8/components/column.ts diff --git a/renderers/angular/src/lib/catalog/datetime-input.ts b/renderers/angular/v0_8/components/datetime-input.ts similarity index 100% rename from renderers/angular/src/lib/catalog/datetime-input.ts rename to renderers/angular/v0_8/components/datetime-input.ts diff --git a/renderers/angular/src/lib/catalog/divider.ts b/renderers/angular/v0_8/components/divider.ts similarity index 100% rename from renderers/angular/src/lib/catalog/divider.ts rename to renderers/angular/v0_8/components/divider.ts diff --git a/renderers/angular/src/lib/catalog/icon.ts b/renderers/angular/v0_8/components/icon.ts similarity index 100% rename from renderers/angular/src/lib/catalog/icon.ts rename to renderers/angular/v0_8/components/icon.ts diff --git a/renderers/angular/src/lib/catalog/image.ts b/renderers/angular/v0_8/components/image.ts similarity index 100% rename from renderers/angular/src/lib/catalog/image.ts rename to renderers/angular/v0_8/components/image.ts diff --git a/renderers/angular/src/lib/catalog/list.ts b/renderers/angular/v0_8/components/list.ts similarity index 100% rename from renderers/angular/src/lib/catalog/list.ts rename to renderers/angular/v0_8/components/list.ts diff --git a/renderers/angular/src/lib/catalog/modal.ts b/renderers/angular/v0_8/components/modal.ts similarity index 100% rename from renderers/angular/src/lib/catalog/modal.ts rename to renderers/angular/v0_8/components/modal.ts diff --git a/renderers/angular/src/lib/catalog/multiple-choice.ts b/renderers/angular/v0_8/components/multiple-choice.ts similarity index 100% rename from renderers/angular/src/lib/catalog/multiple-choice.ts rename to renderers/angular/v0_8/components/multiple-choice.ts diff --git a/renderers/angular/src/lib/catalog/row.ts b/renderers/angular/v0_8/components/row.ts similarity index 100% rename from renderers/angular/src/lib/catalog/row.ts rename to renderers/angular/v0_8/components/row.ts diff --git a/renderers/angular/src/lib/catalog/slider.ts b/renderers/angular/v0_8/components/slider.ts similarity index 100% rename from renderers/angular/src/lib/catalog/slider.ts rename to renderers/angular/v0_8/components/slider.ts diff --git a/renderers/angular/src/lib/catalog/surface.ts b/renderers/angular/v0_8/components/surface.ts similarity index 100% rename from renderers/angular/src/lib/catalog/surface.ts rename to renderers/angular/v0_8/components/surface.ts diff --git a/renderers/angular/src/lib/catalog/tabs.ts b/renderers/angular/v0_8/components/tabs.ts similarity index 100% rename from renderers/angular/src/lib/catalog/tabs.ts rename to renderers/angular/v0_8/components/tabs.ts diff --git a/renderers/angular/src/lib/catalog/text-field.ts b/renderers/angular/v0_8/components/text-field.ts similarity index 100% rename from renderers/angular/src/lib/catalog/text-field.ts rename to renderers/angular/v0_8/components/text-field.ts diff --git a/renderers/angular/src/lib/catalog/text.ts b/renderers/angular/v0_8/components/text.ts similarity index 100% rename from renderers/angular/src/lib/catalog/text.ts rename to renderers/angular/v0_8/components/text.ts diff --git a/renderers/angular/src/lib/catalog/video.ts b/renderers/angular/v0_8/components/video.ts similarity index 100% rename from renderers/angular/src/lib/catalog/video.ts rename to renderers/angular/v0_8/components/video.ts diff --git a/renderers/angular/src/lib/config.ts b/renderers/angular/v0_8/config.ts similarity index 100% rename from renderers/angular/src/lib/config.ts rename to renderers/angular/v0_8/config.ts diff --git a/renderers/angular/src/lib/data/index.ts b/renderers/angular/v0_8/data/index.ts similarity index 100% rename from renderers/angular/src/lib/data/index.ts rename to renderers/angular/v0_8/data/index.ts diff --git a/renderers/angular/src/lib/data/markdown.ts b/renderers/angular/v0_8/data/markdown.ts similarity index 100% rename from renderers/angular/src/lib/data/markdown.ts rename to renderers/angular/v0_8/data/markdown.ts diff --git a/renderers/angular/src/lib/data/processor.ts b/renderers/angular/v0_8/data/processor.ts similarity index 100% rename from renderers/angular/src/lib/data/processor.ts rename to renderers/angular/v0_8/data/processor.ts diff --git a/renderers/angular/src/lib/data/types.ts b/renderers/angular/v0_8/data/types.ts similarity index 100% rename from renderers/angular/src/lib/data/types.ts rename to renderers/angular/v0_8/data/types.ts diff --git a/renderers/angular/v0_8/ng-package.json b/renderers/angular/v0_8/ng-package.json new file mode 100644 index 000000000..ed278942e --- /dev/null +++ b/renderers/angular/v0_8/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/renderers/angular/v0_8/public-api.ts b/renderers/angular/v0_8/public-api.ts new file mode 100644 index 000000000..5e7041bf4 --- /dev/null +++ b/renderers/angular/v0_8/public-api.ts @@ -0,0 +1,24 @@ +export * from './components/surface'; +export * from './components/audio'; +export * from './components/button'; +export * from './components/card'; +export * from './components/checkbox'; +export * from './components/column'; +export * from './components/datetime-input'; +export * from './components/divider'; +export * from './components/icon'; +export * from './components/image'; +export * from './components/list'; +export * from './components/modal'; +export * from './components/multiple-choice'; +export * from './components/row'; +export * from './components/slider'; +export * from './components/tabs'; +export * from './components/text-field'; +export * from './components/text'; +export * from './components/video'; +export * from './types'; +export * from './catalog/index'; +export * from './data/index'; +export * from './rendering/index'; +export * from './config'; diff --git a/renderers/angular/src/lib/rendering/catalog.ts b/renderers/angular/v0_8/rendering/catalog.ts similarity index 100% rename from renderers/angular/src/lib/rendering/catalog.ts rename to renderers/angular/v0_8/rendering/catalog.ts diff --git a/renderers/angular/src/lib/rendering/dynamic-component.ts b/renderers/angular/v0_8/rendering/dynamic-component.ts similarity index 100% rename from renderers/angular/src/lib/rendering/dynamic-component.ts rename to renderers/angular/v0_8/rendering/dynamic-component.ts diff --git a/renderers/angular/src/lib/rendering/index.ts b/renderers/angular/v0_8/rendering/index.ts similarity index 100% rename from renderers/angular/src/lib/rendering/index.ts rename to renderers/angular/v0_8/rendering/index.ts diff --git a/renderers/angular/src/lib/rendering/renderer.ts b/renderers/angular/v0_8/rendering/renderer.ts similarity index 100% rename from renderers/angular/src/lib/rendering/renderer.ts rename to renderers/angular/v0_8/rendering/renderer.ts diff --git a/renderers/angular/src/lib/rendering/theming.ts b/renderers/angular/v0_8/rendering/theming.ts similarity index 100% rename from renderers/angular/src/lib/rendering/theming.ts rename to renderers/angular/v0_8/rendering/theming.ts diff --git a/renderers/angular/v0_8/types.ts b/renderers/angular/v0_8/types.ts new file mode 100644 index 000000000..970d365a4 --- /dev/null +++ b/renderers/angular/v0_8/types.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Action as WebCoreAction, + ServerToClientMessage as WebCoreServerToClientMessage, + ButtonNode, + TextNode, + ImageNode, + IconNode, + AudioPlayerNode, + VideoNode, + CardNode, + DividerNode, + RowNode, + ColumnNode, + ListNode, + TextFieldNode, + CheckboxNode, + SliderNode, + MultipleChoiceNode, + DateTimeInputNode, + ModalNode, + TabsNode, +} from '@a2ui/web_core/v0_8'; + +export namespace Types { + export type Action = WebCoreAction; + export type FunctionCall = any; // v0.8 might not have FunctionCall or structure differs + export type SurfaceID = string; + + export interface ClientToServerMessage { + action: Action; + version: string; + surfaceId?: string; + } + export type A2UIClientEventMessage = ClientToServerMessage; + + export interface Component

> { + id: string; + type: string; + properties: P; + [key: string]: any; + } + + export type AnyComponentNode = Component; + export type CustomNode = AnyComponentNode; + + export type ServerToClientMessage = WebCoreServerToClientMessage; + + export interface Theme { + components?: any; + additionalStyles?: any; + [key: string]: any; + } + + // Aliases + export type Row = RowNode; + export type Column = ColumnNode; + export type Text = TextNode; + export type List = ListNode; + export type Image = ImageNode; + export type Icon = IconNode; + export type Video = VideoNode; + export type Audio = AudioPlayerNode; + export type Button = ButtonNode; + export type Divider = DividerNode; + export type MultipleChoice = MultipleChoiceNode; + export type TextField = TextFieldNode; + export type Checkbox = CheckboxNode; + export type Slider = SliderNode; + export type DateTimeInput = DateTimeInputNode; + export type Tabs = TabsNode; + export type Modal = ModalNode; + + // Explicit Node exports + export type RowNode = import('@a2ui/web_core/v0_8').RowNode; + export type ColumnNode = import('@a2ui/web_core/v0_8').ColumnNode; + export type TextNode = import('@a2ui/web_core/v0_8').TextNode; + export type ListNode = import('@a2ui/web_core/v0_8').ListNode; + export type ImageNode = import('@a2ui/web_core/v0_8').ImageNode; + export type IconNode = import('@a2ui/web_core/v0_8').IconNode; + export type VideoNode = import('@a2ui/web_core/v0_8').VideoNode; + export type AudioPlayerNode = import('@a2ui/web_core/v0_8').AudioPlayerNode; + export type ButtonNode = import('@a2ui/web_core/v0_8').ButtonNode; + export type DividerNode = import('@a2ui/web_core/v0_8').DividerNode; + export type MultipleChoiceNode = import('@a2ui/web_core/v0_8').MultipleChoiceNode; + export type TextFieldNode = import('@a2ui/web_core/v0_8').TextFieldNode; + export type CheckboxNode = import('@a2ui/web_core/v0_8').CheckboxNode; + export type SliderNode = import('@a2ui/web_core/v0_8').SliderNode; + export type DateTimeInputNode = import('@a2ui/web_core/v0_8').DateTimeInputNode; + export type TabsNode = import('@a2ui/web_core/v0_8').TabsNode; + export type ModalNode = import('@a2ui/web_core/v0_8').ModalNode; + + export type CardNode = import('@a2ui/web_core/v0_8').CardNode; +} diff --git a/renderers/web_core/package-lock.json b/renderers/web_core/package-lock.json index 721db3ea5..b836b2246 100644 --- a/renderers/web_core/package-lock.json +++ b/renderers/web_core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@a2ui/web_core", - "version": "0.8.5", + "version": "0.8.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@a2ui/web_core", - "version": "0.8.5", + "version": "0.8.6", "license": "Apache-2.0", "dependencies": { "@preact/signals-core": "^1.13.0", From fbe43bccaad45911d3573bd7f45ba13caaa09a78 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 17 Mar 2026 10:07:49 -0700 Subject: [PATCH 2/3] Update v0.8 Angular renderer to use OnPush and add unit tests --- renderers/angular/package-lock.json | 2 +- .../angular/v0_8/catalog/catalog.spec.ts | 325 ++++++++++++++++++ renderers/angular/v0_8/catalog/index.ts | 88 +++-- .../angular/v0_8/components/audio.spec.ts | 106 ++++++ renderers/angular/v0_8/components/audio.ts | 2 +- .../angular/v0_8/components/button.spec.ts | 99 ++++++ renderers/angular/v0_8/components/button.ts | 5 +- .../angular/v0_8/components/card.spec.ts | 101 ++++++ renderers/angular/v0_8/components/card.ts | 2 +- .../angular/v0_8/components/checkbox.spec.ts | 99 ++++++ renderers/angular/v0_8/components/checkbox.ts | 2 +- .../angular/v0_8/components/column.spec.ts | 96 ++++++ renderers/angular/v0_8/components/column.ts | 2 +- .../v0_8/components/datetime-input.spec.ts | 134 ++++++++ .../angular/v0_8/components/datetime-input.ts | 5 +- .../angular/v0_8/components/divider.spec.ts | 91 +++++ .../angular/v0_8/components/icon.spec.ts | 119 +++++++ renderers/angular/v0_8/components/icon.ts | 12 +- .../angular/v0_8/components/image.spec.ts | 113 ++++++ renderers/angular/v0_8/components/image.ts | 7 +- .../angular/v0_8/components/list.spec.ts | 106 ++++++ renderers/angular/v0_8/components/list.ts | 2 +- .../angular/v0_8/components/modal.spec.ts | 156 +++++++++ renderers/angular/v0_8/components/modal.ts | 4 +- .../v0_8/components/multiple-choice.spec.ts | 135 ++++++++ .../v0_8/components/multiple-choice.ts | 8 +- renderers/angular/v0_8/components/row.spec.ts | 96 ++++++ renderers/angular/v0_8/components/row.ts | 2 +- .../angular/v0_8/components/slider.spec.ts | 143 ++++++++ renderers/angular/v0_8/components/slider.ts | 4 +- .../angular/v0_8/components/surface.spec.ts | 127 +++++++ renderers/angular/v0_8/components/surface.ts | 4 +- .../angular/v0_8/components/tabs.spec.ts | 156 +++++++++ renderers/angular/v0_8/components/tabs.ts | 10 +- .../v0_8/components/text-field.spec.ts | 108 ++++++ .../angular/v0_8/components/text-field.ts | 6 +- .../angular/v0_8/components/text.spec.ts | 222 ++++++++++++ renderers/angular/v0_8/components/text.ts | 18 +- .../angular/v0_8/components/video.spec.ts | 106 ++++++ renderers/angular/v0_8/components/video.ts | 2 +- renderers/angular/v0_8/config.ts | 19 +- renderers/angular/v0_8/data/markdown.spec.ts | 77 +++++ renderers/angular/v0_8/data/markdown.ts | 7 +- renderers/angular/v0_8/data/processor.spec.ts | 82 +++++ renderers/angular/v0_8/public-api.ts | 18 +- renderers/angular/v0_8/rendering/catalog.ts | 6 +- .../v0_8/rendering/dynamic-component.spec.ts | 129 +++++++ .../v0_8/rendering/dynamic-component.ts | 2 +- .../angular/v0_8/rendering/renderer.spec.ts | 113 ++++++ renderers/angular/v0_8/rendering/renderer.ts | 77 +++-- 50 files changed, 3214 insertions(+), 141 deletions(-) create mode 100644 renderers/angular/v0_8/catalog/catalog.spec.ts create mode 100644 renderers/angular/v0_8/components/audio.spec.ts create mode 100644 renderers/angular/v0_8/components/button.spec.ts create mode 100644 renderers/angular/v0_8/components/card.spec.ts create mode 100644 renderers/angular/v0_8/components/checkbox.spec.ts create mode 100644 renderers/angular/v0_8/components/column.spec.ts create mode 100644 renderers/angular/v0_8/components/datetime-input.spec.ts create mode 100644 renderers/angular/v0_8/components/divider.spec.ts create mode 100644 renderers/angular/v0_8/components/icon.spec.ts create mode 100644 renderers/angular/v0_8/components/image.spec.ts create mode 100644 renderers/angular/v0_8/components/list.spec.ts create mode 100644 renderers/angular/v0_8/components/modal.spec.ts create mode 100644 renderers/angular/v0_8/components/multiple-choice.spec.ts create mode 100644 renderers/angular/v0_8/components/row.spec.ts create mode 100644 renderers/angular/v0_8/components/slider.spec.ts create mode 100644 renderers/angular/v0_8/components/surface.spec.ts create mode 100644 renderers/angular/v0_8/components/tabs.spec.ts create mode 100644 renderers/angular/v0_8/components/text-field.spec.ts create mode 100644 renderers/angular/v0_8/components/text.spec.ts create mode 100644 renderers/angular/v0_8/components/video.spec.ts create mode 100644 renderers/angular/v0_8/data/markdown.spec.ts create mode 100644 renderers/angular/v0_8/data/processor.spec.ts create mode 100644 renderers/angular/v0_8/rendering/dynamic-component.spec.ts create mode 100644 renderers/angular/v0_8/rendering/renderer.spec.ts diff --git a/renderers/angular/package-lock.json b/renderers/angular/package-lock.json index 7af8037d8..9f0839cc8 100644 --- a/renderers/angular/package-lock.json +++ b/renderers/angular/package-lock.json @@ -48,7 +48,7 @@ }, "../web_core": { "name": "@a2ui/web_core", - "version": "0.8.5", + "version": "0.8.6", "license": "Apache-2.0", "dependencies": { "@preact/signals-core": "^1.13.0", diff --git a/renderers/angular/v0_8/catalog/catalog.spec.ts b/renderers/angular/v0_8/catalog/catalog.spec.ts new file mode 100644 index 000000000..319b2d6c9 --- /dev/null +++ b/renderers/angular/v0_8/catalog/catalog.spec.ts @@ -0,0 +1,325 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, Input, computed, inputBinding } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { Row } from '../components/row'; +import { Column } from '../components/column'; +import { Text as TextComponent } from '../components/text'; +import { Button } from '../components/button'; +import { List } from '../components/list'; +import { TextField } from '../components/text-field'; + +import { DynamicComponent } from '../rendering/dynamic-component'; +import { Renderer } from '../rendering/renderer'; +import { Types } from '../types'; +import { MarkdownRenderer } from '../data/markdown'; + +import { Theme } from '../rendering/theming'; +import { MessageProcessor } from '../data/processor'; +import { Catalog } from '../rendering/catalog'; + +// Mock context will be handled by MessageProcessor mock +const mockContext = { + resolveData: (path: string) => { + if (path === '/data/text') return 'Dynamic Text'; + if (path === '/data/label') return 'Dynamic Label'; + return null; + }, +}; + +@Component({ + selector: 'test-host', + imports: [Row, Column, TextComponent, Button, List, TextField], + template: ` + @if (type === 'Row') { + + } @else if (type === 'Column') { + + } @else if (type === 'Text') { + + } @else if (type === 'Button') { + + } @else if (type === 'List') { + + } @else if (type === 'TextField') { + + } + `, +}) +class TestHostComponent { + @Input() type = 'Row'; + @Input() componentData: any; +} + +describe('Catalog Components', () => { + let fixture: ComponentFixture; + let host: TestHostComponent; + let mockMessageProcessor: any; + let mockDataModel: any; + let mockSurfaceModel: any; + + beforeEach(async () => { + mockDataModel = { + get: jasmine.createSpy('get').and.callFake((path: string) => { + if (path === '/data/text') return 'Dynamic Text'; + if (path === '/data/label') return 'Dynamic Label'; + if (path === '/data/items') return ['Item 1', 'Item 2']; + return null; + }), + subscribe: jasmine.createSpy('subscribe').and.returnValue({ + unsubscribe: () => {}, + }), + }; + + mockSurfaceModel = { + dataModel: mockDataModel, + componentsModel: new Map([ + ['child1', { id: 'child1', type: 'Text', properties: { text: { literal: 'Child Text' } } }], + ['item1', { id: 'item1', type: 'Text', properties: { text: { literal: 'Item 1' } } }], + ['item2', { id: 'item2', type: 'Text', properties: { text: { literal: 'Item 2' } } }], + ]), + }; + + const surfaceSignal = () => mockSurfaceModel; + + mockMessageProcessor = { + model: { + getSurface: (id: string) => mockSurfaceModel, + }, + getDataModel: jasmine.createSpy('getDataModel').and.returnValue(mockDataModel), + getData: (node: any, path: string) => mockDataModel.get(path), + getSurfaceSignal: () => surfaceSignal, + sendAction: jasmine.createSpy('sendAction'), + getSurfaces: () => new Map([['test-surface', mockSurfaceModel]]), + }; + + await TestBed.configureTestingModule({ + imports: [TestHostComponent, Row, Column, TextComponent, Button, List], + providers: [ + { provide: MarkdownRenderer, useValue: { render: (s: string) => Promise.resolve(s) } }, + + { provide: MessageProcessor, useValue: mockMessageProcessor }, + { + provide: Theme, + useValue: { + components: { + Text: { all: {}, h1: { 'h1-class': true }, body: { 'body-class': true } }, + Row: { 'row-class': true }, + Column: { 'column-class': true }, + Button: { 'button-class': true }, + List: { 'list-class': true }, + TextField: { + container: { 'tf-container': true }, + label: { 'tf-label': true }, + element: { 'tf-element': true }, + }, + }, + additionalStyles: {}, + }, + }, + { provide: MessageProcessor, useValue: mockMessageProcessor }, + { + provide: Catalog, + useValue: { + Text: { + type: async () => TextComponent, + bindings: (node: any) => [ + inputBinding('text', () => node.properties.text), + inputBinding('usageHint', () => node.properties.usageHint || null), + ], + }, + Row: { type: async () => Row, bindings: () => [] }, + Column: { type: async () => Column, bindings: () => [] }, + Button: { type: async () => Button, bindings: () => [] }, + List: { type: async () => List, bindings: () => [] }, + TextField: { type: async () => TextField, bindings: () => [] }, + } as any, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + host = fixture.componentInstance; + }); + + describe('Row', () => { + it('should map justify and align properties correctly', () => { + host.type = 'Row'; + host.componentData = { + id: 'row1', + type: 'Row', + properties: { + children: [], + justify: 'spaceBetween', + align: 'center', + }, + } as Types.RowNode; + fixture.detectChanges(); + + const rowEl = fixture.debugElement.query(By.css('a2ui-row')); + const section = rowEl.query(By.css('section')); + expect(section.classes['distribute-spaceBetween']).toBeTrue(); + expect(section.classes['align-center']).toBeTrue(); + }); + }); + + describe('Column', () => { + it('should map justify and align properties correctly', () => { + host.type = 'Column'; + host.componentData = { + id: 'col1', + type: 'Column', + properties: { + children: [], + justify: 'end', + align: 'start', + }, + } as Types.ColumnNode; + fixture.detectChanges(); + + const colEl = fixture.debugElement.query(By.css('a2ui-column')); + const section = colEl.query(By.css('section')); + expect(section.classes['distribute-end']).toBeTrue(); + expect(section.classes['align-start']).toBeTrue(); + }); + }); + + describe('Text', () => { + it('should resolve text content', async () => { + host.type = 'Text'; + host.componentData = { + id: 'txt1', + type: 'Text', + properties: { + text: { literal: 'Hello World' }, + usageHint: 'h1', + }, + } as Types.TextNode; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const textEl = fixture.debugElement.query(By.css('a2ui-text')); + expect(textEl.nativeElement.innerHTML).toContain('# Hello World'); + }); + }); + + describe('Button', () => { + it('should render child component', () => { + // Mock Renderer Service/Context because Button uses a2ui-renderer for child + // For this unit test, we might just check if it tries to resolve the child. + // But Button uses + // We need to provide a mock SurfaceModel to the Button via the Context? + // Actually DynamicComponent uses `inject(ElementRef)` etc. + // Let's keep it simple for now and verify existence. + host.type = 'Button'; + host.componentData = { + id: 'btn1', + type: 'Button', + properties: { + child: 'child1', + label: 'Legacy Label', + }, + } as any; + fixture.detectChanges(); + const btnEl = fixture.debugElement.query(By.css('button')); + expect(btnEl).toBeTruthy(); + }); + }); + + describe('List', () => { + it('should render items', async () => { + host.type = 'List'; + host.componentData = { + id: 'list1', + type: 'List', + properties: { + children: [ + { + id: '1', + type: 'Text', + properties: { text: { literal: 'Item 1' }, variant: { literal: 'body' } }, + }, + { + id: '2', + type: 'Text', + properties: { text: { literal: 'Item 2' }, variant: { literal: 'body' } }, + }, + ], + }, + } as any; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const listEl = fixture.debugElement.query(By.css('a2ui-list')); + const items = listEl.queryAll(By.css('a2ui-text')); // Assuming items render as Text + expect(items.length).toBe(2); + expect(items[0].nativeElement.textContent).toContain('Item 1'); + }); + }); + + describe('TextField', () => { + it('should render input with value', () => { + host.type = 'TextField'; + host.componentData = { + id: 'tf1', + type: 'TextField', + properties: { + label: { literal: 'My Input' }, + value: { path: '/data/text' }, + }, + } as any; + fixture.detectChanges(); + + const inputEl = fixture.debugElement.query(By.css('input')); + // Component might use [value] or ngModel + // Let's check native element value if bound + // If it uses Custom Input implementation, check that. + // TextField usually has a label and an input. + expect(inputEl.nativeElement.value).toBe('Dynamic Text'); + }); + }); +}); diff --git a/renderers/angular/v0_8/catalog/index.ts b/renderers/angular/v0_8/catalog/index.ts index a794a323a..1cd6d1318 100644 --- a/renderers/angular/v0_8/catalog/index.ts +++ b/renderers/angular/v0_8/catalog/index.ts @@ -15,16 +15,13 @@ */ import { inputBinding } from '@angular/core'; -import * as Types from '@a2ui/web_core/types/types'; +import { Types } from '../types'; import { Catalog } from '../rendering/catalog'; -import { Row } from './row'; -import { Column } from './column'; -import { Text } from './text'; -export const DEFAULT_CATALOG: Catalog = { +export const CATALOG: Catalog = { Row: { - type: () => Row, - bindings: (node) => { + type: () => import('../components/row').then((r) => r.Row), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.RowNode).properties; return [ inputBinding('alignment', () => properties.alignment ?? 'stretch'), @@ -34,8 +31,8 @@ export const DEFAULT_CATALOG: Catalog = { }, Column: { - type: () => Column, - bindings: (node) => { + type: () => import('../components/column').then((r) => r.Column), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.ColumnNode).properties; return [ inputBinding('alignment', () => properties.alignment ?? 'stretch'), @@ -45,18 +42,18 @@ export const DEFAULT_CATALOG: Catalog = { }, List: { - type: () => import('./list').then((r) => r.List), - bindings: (node) => { + type: () => import('../components/list').then((r) => r.List), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.ListNode).properties; return [inputBinding('direction', () => properties.direction ?? 'vertical')]; }, }, - Card: () => import('./card').then((r) => r.Card), + Card: () => import('../components/card').then((r) => r.Card), Image: { - type: () => import('./image').then((r) => r.Image), - bindings: (node) => { + type: () => import('../components/image').then((r) => r.Image), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.ImageNode).properties; return [ inputBinding('url', () => properties.url), @@ -67,80 +64,77 @@ export const DEFAULT_CATALOG: Catalog = { }, Icon: { - type: () => import('./icon').then((r) => r.Icon), - bindings: (node) => { + type: () => import('../components/icon').then((r) => r.Icon), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.IconNode).properties; return [inputBinding('name', () => properties.name)]; }, }, Video: { - type: () => import('./video').then((r) => r.Video), - bindings: (node) => { + type: () => import('../components/video').then((r) => r.Video), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.VideoNode).properties; return [inputBinding('url', () => properties.url)]; }, }, AudioPlayer: { - type: () => import('./audio').then((r) => r.Audio), - bindings: (node) => { + type: () => import('../components/audio').then((r) => r.Audio), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.AudioPlayerNode).properties; return [inputBinding('url', () => properties.url)]; }, }, Text: { - type: () => Text, - bindings: (node) => { + type: () => import('../components/text').then((r) => r.Text), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.TextNode).properties; return [ inputBinding('text', () => properties.text), - inputBinding('usageHint', () => properties.usageHint || null), + inputBinding('usageHint', () => properties.usageHint), ]; }, }, Button: { - type: () => import('./button').then((r) => r.Button), - bindings: (node) => { + type: () => import('../components/button').then((r) => r.Button), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.ButtonNode).properties; - return [ - inputBinding('action', () => properties.action), - inputBinding('primary', () => properties.primary), - ]; + return [inputBinding('action', () => properties.action)]; }, }, - Divider: () => import('./divider').then((r) => r.Divider), + Divider: () => import('../components/divider').then((r) => r.Divider), MultipleChoice: { - type: () => import('./multiple-choice').then((r) => r.MultipleChoice), - bindings: (node) => { + type: () => import('../components/multiple-choice').then((r) => r.MultipleChoice), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.MultipleChoiceNode).properties; return [ inputBinding('options', () => properties.options || []), inputBinding('value', () => properties.selections), - inputBinding('description', () => 'Select an item'), // TODO: this should be defined in the properties + inputBinding('description', () => 'Select an item'), ]; }, }, TextField: { - type: () => import('./text-field').then((r) => r.TextField), - bindings: (node) => { + type: () => import('../components/text-field').then((r) => r.TextField), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.TextFieldNode).properties; return [ inputBinding('text', () => properties.text ?? null), inputBinding('label', () => properties.label), - inputBinding('textFieldType', () => properties.textFieldType), + inputBinding('inputType', () => properties.textFieldType), ]; }, }, DateTimeInput: { - type: () => import('./datetime-input').then((r) => r.DatetimeInput), - bindings: (node) => { + type: () => import('../components/datetime-input').then((r) => r.DatetimeInput), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.DateTimeInputNode).properties; return [ inputBinding('enableDate', () => properties.enableDate), @@ -151,8 +145,8 @@ export const DEFAULT_CATALOG: Catalog = { }, CheckBox: { - type: () => import('./checkbox').then((r) => r.Checkbox), - bindings: (node) => { + type: () => import('../components/checkbox').then((r) => r.Checkbox), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.CheckboxNode).properties; return [ inputBinding('label', () => properties.label), @@ -162,28 +156,30 @@ export const DEFAULT_CATALOG: Catalog = { }, Slider: { - type: () => import('./slider').then((r) => r.Slider), - bindings: (node) => { + type: () => import('../components/slider').then((r) => r.Slider), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.SliderNode).properties; return [ inputBinding('value', () => properties.value), inputBinding('minValue', () => properties.minValue), inputBinding('maxValue', () => properties.maxValue), - inputBinding('label', () => ''), // TODO: this should be defined in the properties + inputBinding('label', () => ''), ]; }, }, Tabs: { - type: () => import('./tabs').then((r) => r.Tabs), - bindings: (node) => { + type: () => import('../components/tabs').then((r) => r.Tabs), + bindings: (node: Types.AnyComponentNode) => { const properties = (node as Types.TabsNode).properties; return [inputBinding('tabs', () => properties.tabItems)]; }, }, Modal: { - type: () => import('./modal').then((r) => r.Modal), + type: () => import('../components/modal').then((r) => r.Modal), bindings: () => [], }, }; + +export const V0_8_CATALOG = CATALOG; diff --git a/renderers/angular/v0_8/components/audio.spec.ts b/renderers/angular/v0_8/components/audio.spec.ts new file mode 100644 index 000000000..9ddaf92ec --- /dev/null +++ b/renderers/angular/v0_8/components/audio.spec.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Audio } from './audio'; +import { MessageProcessor } from '../data/processor'; +import { Theme } from '../rendering/theming'; +import { Catalog } from '../rendering/catalog'; +import { MarkdownRenderer } from '../data/markdown'; +import { PLATFORM_ID } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +describe('Audio', () => { + let component: Audio; + let fixture: ComponentFixture