Skip to content
Open
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
bdc2876
feat: add TestBedComponentBuilder.ts for component creation
christoph-rogalla Mar 13, 2025
7dfc0d5
feat: add dependency at component level fix WIP
christoph-rogalla Mar 18, 2025
df1923d
feat: add dependency at component level fix WIP
christoph-rogalla Mar 18, 2025
2dc32db
fix: error on reset/init testbed module
christoph-rogalla Apr 3, 2025
5e90152
refactor: testbed builder pattern and prop assignment
christoph-rogalla Apr 4, 2025
87466cf
fix: not standalone angular schematics
christoph-rogalla Apr 14, 2025
6937f05
fix: multiple component declaration by wrapper module
christoph-rogalla Apr 14, 2025
2f920fb
WIP: multiple import of some components causes error
christoph-rogalla Apr 14, 2025
c15a3fb
refactor: testbed creation to constructor
christoph-rogalla Apr 15, 2025
a217ee2
refactor: override meta data generation to file
christoph-rogalla Apr 28, 2025
c952ddd
remove: console.log
christoph-rogalla Apr 29, 2025
6809141
Merge branch 'next' into angular-dependencies
christoph-rogalla Apr 30, 2025
98bcd54
refactor: @angular/animations dependency back to dev-dependencies
christoph-rogalla Apr 30, 2025
2feab65
refactor: split throw on null required values into specific functions
christoph-rogalla May 2, 2025
e736144
refactor: angular sandbox dependencies and yarn.lock
christoph-rogalla May 2, 2025
93cd136
fix: yarn lock diff on install
christoph-rogalla May 2, 2025
df4753f
feat: add feature switch useTestBedRenderer in parameters
christoph-rogalla May 2, 2025
a4d719f
fix: preview didnt visualized correctly
christoph-rogalla May 3, 2025
7c48733
refactor: render function to move duplicate code into own function
christoph-rogalla May 3, 2025
df05233
fix: RendererFactory.test.ts due new parameter
christoph-rogalla May 3, 2025
3922240
Update code/frameworks/angular/src/client/angular-beta/utils/TestBedO…
valentinpalkovic May 9, 2025
c3b955f
Merge branch 'next' into angular-dependencies
valentinpalkovic May 9, 2025
c06bce6
Introduce experimental_afterEach wrapper for testing purposes
valentinpalkovic May 9, 2025
1c67e3a
Use beforeEach hook instead of afterEach
valentinpalkovic May 9, 2025
529255a
feat: add new testcases for error with testbed rendering
christoph-rogalla May 12, 2025
9e3dc8b
Merge remote-tracking branch 'origin/angular-dependencies' into angul…
christoph-rogalla May 12, 2025
f4c7209
refactor: testbed renderer error tests
christoph-rogalla May 12, 2025
1f7f262
fix: multiple declaration in modules
christoph-rogalla May 12, 2025
ab11710
fix: prop assignment did not trigger angular lifecycle hooks
christoph-rogalla May 13, 2025
a084f95
[Angular] feat: add rendering bug as test story
christoph-rogalla May 29, 2025
d07394b
[Angular] feat: add routes as parameter attributes for story for spec…
christoph-rogalla May 29, 2025
f4f82f2
[Angular] refactor: routing attributes to moduleMedatada and Applicat…
christoph-rogalla May 29, 2025
e645640
[Angular] feat: add routing functionalities for testbed rendering
christoph-rogalla May 30, 2025
07862e5
Merge branch 'next' into angular-dependencies
christoph-rogalla Jun 3, 2025
360d73c
Merge remote-tracking branch 'origin/angular-dependencies' into angul…
christoph-rogalla Jun 3, 2025
fc1824d
Merge branch 'next' into angular-dependencies
christoph-rogalla Jun 6, 2025
d3ac778
Merge remote-tracking branch 'origin/angular-dependencies' into angul…
christoph-rogalla Jun 6, 2025
68853bc
[Angular] fix: router dependency
christoph-rogalla Jun 6, 2025
b6fb228
[Angular] fix: changed lockfile
christoph-rogalla Jun 6, 2025
686f5dc
[Angular] fix: dependency for pipeline
christoph-rogalla Jun 6, 2025
c67765c
[Angular] fix: circular dependency and tests
christoph-rogalla Jun 6, 2025
595db12
[Angular] fix: angular versions
christoph-rogalla Jun 6, 2025
5e39790
[Angular] fix: angular versions
christoph-rogalla Jun 6, 2025
ced20e3
[Angular] fix: angular versions
christoph-rogalla Jun 6, 2025
88ad304
[Angular] fix: angular versions
christoph-rogalla Jun 6, 2025
3d64710
[Angular] fix: angular versions
christoph-rogalla Jun 6, 2025
3b2808d
[Angular] fix: angular router versions
christoph-rogalla Jun 6, 2025
6080907
[Angular] remove: provideExperimentalZonelessChangeDetection in Abstr…
christoph-rogalla Jun 6, 2025
155a2a2
[Angular] fix: versioning
christoph-rogalla Jun 6, 2025
d350fd2
[Angular] fix: modfing lock file
christoph-rogalla Jun 6, 2025
933fd0c
[Angular] feat: add destroy platform on before-each
christoph-rogalla Jun 7, 2025
6aa3c90
Merge remote-tracking branch 'origin/next' into pr/christoph-rogalla/…
valentinpalkovic Jul 21, 2025
f4ae462
Make @angular/router optional
valentinpalkovic Jul 22, 2025
2a51649
[Angular] fix: add missing await statements
christoph-rogalla Jul 22, 2025
2d6b193
Introduce previewTestBedRenderer feature for enhanced component rende…
valentinpalkovic Jul 23, 2025
49c603c
Remove previewTestBedRenderer feature from Angular templates
valentinpalkovic Jul 23, 2025
04efc4f
Refactor TestBedDocsRenderer to remove forced rendering option
valentinpalkovic Jul 23, 2025
fc7e574
Angular: Disable prod mode if new TestBed renderer is used
valentinpalkovic Jul 23, 2025
63a06a3
Update sandbox-templates to enable previewTestBedRenderer feature for…
valentinpalkovic Jul 23, 2025
927762c
Fix linting
valentinpalkovic Jul 23, 2025
2506aed
[Angular] fix: story html width
christoph-rogalla Jul 26, 2025
4840d2c
Merge remote-tracking branch 'origin/angular-dependencies' into angul…
christoph-rogalla Jul 26, 2025
2c58729
[Angular] fix: chip module story
christoph-rogalla Jul 26, 2025
37604c9
[Angular] fix: getting provider from ModuleWithProviders
christoph-rogalla Jul 26, 2025
3b5df69
[Angular] fix: getting provider from ModuleWithProviders
christoph-rogalla Jul 28, 2025
474b9ee
[Angular] fix: input signals error due invalid value set
christoph-rogalla Jul 29, 2025
7dbd6a3
[Angular] fix: circular dependency wrapper module
christoph-rogalla Jul 29, 2025
e418f78
[Angular] fix: declarations
christoph-rogalla Jul 30, 2025
1ed7890
[Angular] feat: add reactive stories for debugging
christoph-rogalla Aug 11, 2025
2286ce1
remove: debug stories
christoph-rogalla Aug 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions code/frameworks/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"prep": "rimraf dist && jiti ../../../scripts/prepare/tsc.ts"
},
"dependencies": {
"@angular/platform-browser-dynamic": "^20.0.0",
"@angular/router": "^20.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to remove these from the dev-dependencies. Do you need them because of some stories in our sandboxes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider moving @angular/platform-browser-dynamic and @angular/router to peerDependencies since they should be provided by the host application

"@storybook/builder-webpack5": "workspace:*",
"@storybook/core-webpack": "workspace:*",
"@storybook/global": "^5.0.0",
Expand All @@ -65,17 +67,18 @@
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^1.12.1",
"@angular-devkit/architect": "^0.1901.1",
"@angular-devkit/build-angular": "^19.1.1",
"@angular-devkit/core": "^19.1.1",
"@angular/animations": "^19.1.1",
"@angular/common": "^19.1.1",
"@angular/compiler": "^19.1.1",
"@angular/compiler-cli": "^19.1.1",
"@angular/core": "^19.1.1",
"@angular/forms": "^19.1.1",
"@angular/platform-browser": "^19.1.1",
"@angular/platform-browser-dynamic": "^19.1.1",
"@angular-devkit/architect": "^0.2000.1",
"@angular-devkit/build-angular": "^20.0.0",
"@angular-devkit/core": "^20.0.0",
"@angular/animations": "^20.0.0",
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/platform-browser": "^20.0.0",
"@angular/platform-browser-dynamic": "^20.0.0",
"@angular/router": "^20.0.0",
"@types/node": "^22.0.0",
"rimraf": "^6.0.1",
"typescript": "^5.8.3",
Expand All @@ -95,6 +98,7 @@
"@angular/forms": ">=18.0.0 < 21.0.0",
"@angular/platform-browser": ">=18.0.0 < 21.0.0",
"@angular/platform-browser-dynamic": ">=18.0.0 < 21.0.0",
"@angular/router": ">=18.0.0 < 21.0.0",
"rxjs": "^6.5.3 || ^7.4.0",
"storybook": "workspace:^",
"typescript": "^4.9.0 || ^5.0.0",
Expand Down
131 changes: 95 additions & 36 deletions code/frameworks/angular/src/client/angular-beta/AbstractRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { ApplicationRef, NgModule } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { BehaviorSubject, Subject } from 'rxjs';
import { stringify } from 'telejson';

import { ICollection, StoryFnAngularReturnType } from '../types';
import { getApplication } from './StorybookModule';
import { storyPropsProvider } from './StorybookProvider';
import { queueBootstrapping } from './utils/BootstrapQueue';
import { PropertyExtractor } from './utils/PropertyExtractor';
import { TestBedComponentBuilder } from './utils/TestBedComponentBuilder';
import { queueBootstrapping } from './utils/BootstrapQueue';
import { bootstrapApplication } from '@angular/platform-browser';
import { getWrapperComponent } from './TestBedWrapperComponent';
import { storyPropsProvider } from './StorybookProvider';
import { getApplication } from './StorybookModule';

type StoryRenderInfo = {
storyFnAngular: StoryFnAngularReturnType;
Expand All @@ -21,7 +22,6 @@ declare global {
}

const applicationRefs = new Map<HTMLElement, ApplicationRef>();

/**
* Attribute name for the story UID that may be written to the targetDOMNode.
*
Expand Down Expand Up @@ -71,8 +71,6 @@ export abstract class AbstractRenderer {
component?: any;
targetDOMNode: HTMLElement;
}) {
const targetSelector = this.generateTargetSelectorFromStoryId(targetDOMNode.id);

const newStoryProps$ = new BehaviorSubject<ICollection>(storyFnAngular.props);

if (
Expand All @@ -90,14 +88,87 @@ export abstract class AbstractRenderer {
return;
}

await this.beforeFullRender(targetDOMNode);
const { environmentProviders, componentSelector, analyzedMetadata } =
await this.prepareMetaData(storyFnAngular, targetDOMNode, component);
environmentProviders.push(storyPropsProvider(newStoryProps$));

// Complete last BehaviorSubject and set a new one for the current module
if (this.storyProps$) {
this.storyProps$.complete();
}
this.storyProps$ = newStoryProps$;

const application = getApplication({
storyFnAngular,
component,
targetSelector: componentSelector,
analyzedMetadata,
});

const applicationRef = await queueBootstrapping(() => {
return bootstrapApplication(application, {
...storyFnAngular.applicationConfig,
providers: environmentProviders,
});
});

this.setApplicationRef(targetDOMNode, applicationRef);
}

/**
* Bootstrap main angular module with main component with testbed api
*
* @param storyFnAngular {StoryFnAngularReturnType}
* @param forced {boolean}
* @param component {Component}
*/
public async renderWithTestBed({
storyFnAngular,
forced,
component,
targetDOMNode,
}: {
storyFnAngular: StoryFnAngularReturnType;
forced: boolean;
component?: any;
targetDOMNode: HTMLElement;
}) {
const { environmentProviders, componentSelector, analyzedMetadata } =
await this.prepareMetaData(storyFnAngular, targetDOMNode, component);

if (storyFnAngular.userDefinedTemplate) {
component = getWrapperComponent(storyFnAngular.template);
}

const componentBuilder = await new TestBedComponentBuilder()
.setComponent(component)
.setSelector(componentSelector)
.setStoryFn(storyFnAngular)
.setMetaData(analyzedMetadata)
.setTargetNode(targetDOMNode)
.setEnvironmentProviders(environmentProviders)
.configure()
.initRouter()
.compileComponents();

componentBuilder.copyComponentIntoTargetNode();

this.setApplicationRef(targetDOMNode, componentBuilder.getApplicationRef());
}

public setApplicationRef(targetDOMNode: HTMLElement, applicationRef: ApplicationRef) {
applicationRefs.set(targetDOMNode, applicationRef);
}

private async prepareMetaData(
storyFnAngular: StoryFnAngularReturnType,
targetDOMNode: HTMLElement,
component?: any
) {
const targetSelector = this.generateTargetSelectorFromStoryId(targetDOMNode.id);

await this.beforeFullRender();

this.initAngularRootElement(targetDOMNode, targetSelector);

const analyzedMetadata = new PropertyExtractor(storyFnAngular.moduleMetadata, component);
Expand All @@ -112,36 +183,25 @@ export abstract class AbstractRenderer {
element.toggleAttribute(storyUid, true);
}

const application = getApplication({
storyFnAngular,
component,
targetSelector: componentSelector,
analyzedMetadata,
});

const providers = [
storyPropsProvider(newStoryProps$),
const environmentProviders = [
...analyzedMetadata.applicationProviders,
...(storyFnAngular.applicationConfig?.providers ?? []),
];

if (STORYBOOK_ANGULAR_OPTIONS?.experimentalZoneless) {
const { provideExperimentalZonelessChangeDetection } = await import('@angular/core');
if (!provideExperimentalZonelessChangeDetection) {
throw new Error('Experimental zoneless change detection requires Angular 18 or higher');
} else {
providers.unshift(provideExperimentalZonelessChangeDetection());
}
}

const applicationRef = await queueBootstrapping(() => {
return bootstrapApplication(application, {
...storyFnAngular.applicationConfig,
providers,
});
});

applicationRefs.set(targetDOMNode, applicationRef);
// if (STORYBOOK_ANGULAR_OPTIONS?.experimentalZoneless) {
// const { provideExperimentalZonelessChangeDetection } = await import('@angular/core');
// if (!provideExperimentalZonelessChangeDetection) {
// throw new Error('Experimental zoneless change detection requires Angular 18 or higher');
// } else {
// environmentProviders.unshift(provideExperimentalZonelessChangeDetection());
// }
// }

return {
environmentProviders,
componentSelector,
analyzedMetadata,
};
}

/**
Expand Down Expand Up @@ -202,7 +262,6 @@ export abstract class AbstractRenderer {
forced: boolean;
}) {
const previousStoryRenderInfo = this.previousStoryRenderInfo.get(targetDOMNode);

const currentStoryRender = {
storyFnAngular,
moduleMetadataSnapshot: stringify(moduleMetadata, { maxDepth: 50 }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ describe('RendererFactory', () => {

describe('CanvasRenderer', () => {
it('should get CanvasRenderer instance', async () => {
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode, false);
expect(render).toBeInstanceOf(CanvasRenderer);
});

it('should render my-story for story template', async () => {
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode, false);
await render?.render({
storyFnAngular: {
template: '🦊',
Expand All @@ -62,7 +62,7 @@ describe('RendererFactory', () => {
@Component({ selector: 'foo', template: '🦊' })
class FooComponent {}

const render = await rendererFactory.getRendererInstance(rootTargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode, false);
await render?.render({
storyFnAngular: {
props: {},
Expand All @@ -87,7 +87,7 @@ describe('RendererFactory', () => {
}
const token = new Thing();

const render = await rendererFactory.getRendererInstance(rootTargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode, false);

await render?.render({
storyFnAngular: {
Expand All @@ -105,7 +105,7 @@ describe('RendererFactory', () => {
describe('when forced=true', () => {
beforeEach(async () => {
// Init first render
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode, false);
await render?.render({
storyFnAngular: {
template: '{{ logo }}: {{ name }}',
Expand All @@ -125,7 +125,7 @@ describe('RendererFactory', () => {

it('should not be re-rendered when only props change', async () => {
// only props change
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode, false);
await render?.render({
storyFnAngular: {
props: {
Expand All @@ -140,7 +140,7 @@ describe('RendererFactory', () => {
});

it('should be re-rendered when template change', async () => {
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode, false);
await render?.render({
storyFnAngular: {
template: '{{ beer }}',
Expand All @@ -161,7 +161,7 @@ describe('RendererFactory', () => {
describe('when canvas render is done before', () => {
beforeEach(async () => {
// Init first Canvas render
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootTargetDOMNode, false);
await render?.render({
storyFnAngular: {
template: 'Canvas 🖼',
Expand All @@ -177,13 +177,13 @@ describe('RendererFactory', () => {
.appendChild(global.document.createElement('👾'));

expect(global.document.getElementById('storybook-root').innerHTML).toContain('Canvas 🖼');
await rendererFactory.getRendererInstance(rootDocstargetDOMNode);
await rendererFactory.getRendererInstance(rootDocstargetDOMNode, false);
expect(global.document.getElementById('storybook-root').innerHTML).toBe('');
});
});

it('should get DocsRenderer instance', async () => {
const render = await rendererFactory.getRendererInstance(rootDocstargetDOMNode);
const render = await rendererFactory.getRendererInstance(rootDocstargetDOMNode, false);
expect(render).toBeInstanceOf(DocsRenderer);
});

Expand All @@ -193,7 +193,8 @@ describe('RendererFactory', () => {
class FooComponent {}

const render = await rendererFactory.getRendererInstance(
global.document.getElementById('storybook-docs')
global.document.getElementById('storybook-docs'),
false
);

const targetDOMNode1 = global.document.createElement('div');
Expand Down Expand Up @@ -223,7 +224,7 @@ describe('RendererFactory', () => {
expect(global.document.querySelectorAll('#story-1 > story-1')[0].innerHTML).toBe(
'<foo>🦊</foo><!--container-->'
);
expect(global.document.querySelectorAll('#story-1 > story-1')[1].innerHTML).toBe(
expect(global.document.querySelectorAll('#story-1 > story-1')[0].innerHTML).toBe(
'<foo>🦊</foo><!--container-->'
);
Comment on lines 224 to 229
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Duplicate assertion using same selector '[0]' - second assertion is testing the exact same element as the first

Suggested change
expect(global.document.querySelectorAll('#story-1 > story-1')[0].innerHTML).toBe(
'<foo>🦊</foo><!--container-->'
);
expect(global.document.querySelectorAll('#story-1 > story-1')[1].innerHTML).toBe(
expect(global.document.querySelectorAll('#story-1 > story-1')[0].innerHTML).toBe(
'<foo>🦊</foo><!--container-->'
);
expect(global.document.querySelectorAll('#story-1 > story-1')[0].innerHTML).toBe(
'<foo>🦊</foo><!--container-->'
);
expect(global.document.querySelectorAll('#story-1 > story-1')[1].innerHTML).toBe(
'<foo>🦊</foo><!--container-->'
);

});
Expand All @@ -235,7 +236,8 @@ describe('RendererFactory', () => {
class FooComponent {}

const render = await rendererFactory.getRendererInstance(
global.document.getElementById('storybook-docs')
global.document.getElementById('storybook-docs'),
false
);

const targetDOMNode1 = global.document.createElement('div');
Expand Down
Loading