@aurelia/storybook 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/__tests__/render.test.ts +188 -0
- package/dist/__tests__/example.test.d.ts +0 -0
- package/dist/__tests__/example.test.js +6 -0
- package/dist/__tests__/render.test.d.ts +1 -0
- package/dist/__tests__/render.test.js +156 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/preset.d.ts +8 -0
- package/dist/preset.js +11 -0
- package/dist/preview/render.d.ts +17 -0
- package/dist/preview/render.js +106 -0
- package/dist/preview/render.test.d.ts +1 -0
- package/dist/preview/render.test.js +126 -0
- package/dist/preview/types.d.ts +5 -0
- package/dist/preview/types.js +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/preset.d.ts +8 -0
- package/dist/src/preset.js +11 -0
- package/dist/src/preview/render.d.ts +17 -0
- package/dist/src/preview/render.js +109 -0
- package/dist/src/preview/types.d.ts +5 -0
- package/dist/src/preview/types.js +1 -0
- package/jest.config.js +9 -0
- package/package.json +35 -0
- package/preset.js +1 -0
- package/src/index.ts +4 -0
- package/src/preset.ts +14 -0
- package/src/preview/render.ts +173 -0
- package/src/preview/types.ts +7 -0
- package/tsconfig.json +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dwayne Charrington
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @aurelia/storybook
|
|
2
|
+
|
|
3
|
+
> **Note:** Storybook support is currently in an early stage, and there may be bugs, issues, or unsupported features in this plugin. The intention is to make this plugin more production-ready when Aurelia 2 reaches stable release. Currently, it only works with Vite, with Webpack support planned shortly.
|
|
4
|
+
|
|
5
|
+
This package provides an integration between Aurelia 2 and Storybook 8 using Vite. It lets you write and render Aurelia 2 components as Storybook stories with full support for Storybook controls, actions, and interactive testing.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Vite-Powered Build**: Uses Vite (via the provided preset) to bundle your stories.
|
|
10
|
+
- **Aurelia Enhancement**: Renders Aurelia 2 components using Aurelia's `enhance()` API.
|
|
11
|
+
- **Storybook 8 Compatibility**: Fully compatible with Storybook 8's new rendering API.
|
|
12
|
+
- **Arg & Action Support**: Use story args and actions as you would with any Storybook story.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install the plugin as a dev dependency:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install --save-dev @aurelia/storybook
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Also, make sure to have the required dependencies installed in your project:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install --save-dev @storybook/addons @storybook/core-events @storybook/builder-vite @storybook/core-common @storybook/preview-api @storybook/types
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> **Tip:** Check your existing Aurelia 2 app for already installed versions. The peer dependencies are expected to be compatible with Aurelia 2 beta releases (see `package.json` for version details).
|
|
29
|
+
|
|
30
|
+
## Getting Started
|
|
31
|
+
|
|
32
|
+
### Storybook Configuration
|
|
33
|
+
|
|
34
|
+
To integrate Aurelia 2 with your Storybook instance, follow these steps:
|
|
35
|
+
|
|
36
|
+
1. **Preset Setup**:
|
|
37
|
+
The package comes with a minimal Storybook preset (see [src/preset.ts](src/preset.ts)) that allows you to adjust Vite's configuration if needed. Storybook will use this preset to set up the build system for your Aurelia stories.
|
|
38
|
+
|
|
39
|
+
2. **Framework Setup**:
|
|
40
|
+
For a full Aurelia 2 integration with Vite and a TypeScript configuration, ensure that your Storybook configuration files are set up as follows:
|
|
41
|
+
|
|
42
|
+
- **.storybook/main.ts**
|
|
43
|
+
Create or update your `.storybook/main.ts` file with the following contents:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import type { StorybookConfig } from '@storybook/core-common';
|
|
47
|
+
import { mergeConfig } from 'vite';
|
|
48
|
+
|
|
49
|
+
const config: StorybookConfig & { viteFinal?: (config: any, options: any) => any } = {
|
|
50
|
+
stories: ['../src/stories/**/*.stories.@(ts|tsx|js|jsx|mdx)'],
|
|
51
|
+
addons: [
|
|
52
|
+
// Addons like:
|
|
53
|
+
// '@storybook/addon-links',
|
|
54
|
+
// '@storybook/addon-essentials',
|
|
55
|
+
// '@storybook/addon-interactions'
|
|
56
|
+
],
|
|
57
|
+
framework: {
|
|
58
|
+
name: '@aurelia/storybook',
|
|
59
|
+
options: {},
|
|
60
|
+
},
|
|
61
|
+
core: {
|
|
62
|
+
builder: '@storybook/builder-vite',
|
|
63
|
+
},
|
|
64
|
+
viteFinal: async (viteConfig) => {
|
|
65
|
+
viteConfig.optimizeDeps = viteConfig.optimizeDeps || {};
|
|
66
|
+
viteConfig.optimizeDeps.exclude = viteConfig.optimizeDeps.exclude || [];
|
|
67
|
+
if (!viteConfig.optimizeDeps.exclude.includes('@aurelia/runtime-html')) {
|
|
68
|
+
viteConfig.optimizeDeps.exclude.push('@aurelia/runtime-html');
|
|
69
|
+
}
|
|
70
|
+
return mergeConfig(viteConfig, {
|
|
71
|
+
// ...any additional Vite configuration
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default config as any;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- **.storybook/preview.ts**
|
|
80
|
+
Next, update or create your `.storybook/preview.ts` file with the following code to import the render functions from the Aurelia Storybook plugin:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// .storybook/preview.ts
|
|
84
|
+
// Import the render function from the plugin package.
|
|
85
|
+
export { render, renderToCanvas } from '@aurelia/storybook';
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> **Note:** Addons such as `@storybook/addon-links`, `@storybook/addon-essentials`, and `@storybook/addon-interactions` are supported by installing them and adding them to the `addons` array in your configuration.
|
|
89
|
+
|
|
90
|
+
3. **Add scripts to your package.json**:
|
|
91
|
+
Add the following scripts to your `package.json` file to work with Storybook:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
"scripts": {
|
|
95
|
+
"storybook": "storybook dev -p 6006",
|
|
96
|
+
"build-storybook": "storybook build"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
These scripts will allow you to start Storybook in development mode and build it for production.
|
|
100
|
+
|
|
101
|
+
### Writing Stories
|
|
102
|
+
|
|
103
|
+
Aurelia 2 stories are written similarly to standard Storybook stories, with a few Aurelia-specific details. Below is an example story file (`hello-world.stories.ts`) that demonstrates various scenarios:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { HelloWorld } from '../hello-world';
|
|
107
|
+
import { action } from '@storybook/addon-actions';
|
|
108
|
+
import { userEvent, within } from '@storybook/testing-library';
|
|
109
|
+
|
|
110
|
+
const meta = {
|
|
111
|
+
title: 'Example/HelloWorld',
|
|
112
|
+
component: HelloWorld,
|
|
113
|
+
render: (args) => ({
|
|
114
|
+
template: <hello-world message.bind="message" on-increment.bind="onIncrement"></hello-world>,
|
|
115
|
+
}),
|
|
116
|
+
argTypes: {
|
|
117
|
+
message: { control: 'text' },
|
|
118
|
+
onIncrement: { action: 'increment' }
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default meta;
|
|
123
|
+
|
|
124
|
+
export const DefaultHelloWorld = {
|
|
125
|
+
args: {
|
|
126
|
+
message: "Hello from Storybook!",
|
|
127
|
+
onIncrement: action('increment')
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const InteractiveHelloWorld = {
|
|
132
|
+
args: {
|
|
133
|
+
message: "Try clicking the button!",
|
|
134
|
+
onIncrement: action('increment')
|
|
135
|
+
},
|
|
136
|
+
play: async ({ canvasElement }) => {
|
|
137
|
+
const canvas = within(canvasElement);
|
|
138
|
+
const button = canvas.getByRole('button');
|
|
139
|
+
// Simulate three button clicks
|
|
140
|
+
await userEvent.click(button);
|
|
141
|
+
await userEvent.click(button);
|
|
142
|
+
await userEvent.click(button);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const NoArgs = {
|
|
147
|
+
render: () => ({
|
|
148
|
+
template: <hello-world></hello-world>
|
|
149
|
+
})
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const WithCustomTemplate = {
|
|
153
|
+
render: (args) => ({
|
|
154
|
+
template: <hello-world message.bind="message">Click me!</hello-world>
|
|
155
|
+
}),
|
|
156
|
+
args: {
|
|
157
|
+
message: "This is a custom message"
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### How It Works
|
|
163
|
+
|
|
164
|
+
- **Render Function**:
|
|
165
|
+
The integration exports a render function (`renderToCanvas`) that Storybook calls to mount your Aurelia component on the preview canvas. It clears the canvas, enhances it with Aurelia, and notifies Storybook when rendering is complete.
|
|
166
|
+
|
|
167
|
+
- **Aurelia Enhancement**:
|
|
168
|
+
Once the canvas is cleared, the integration instantiates a new Aurelia instance, registers your component (and any additional Aurelia modules you may specify), and calls the Aurelia `enhance()` API to bind your component's view to the DOM.
|
|
169
|
+
|
|
170
|
+
- **Arg Integration and Interactions**:
|
|
171
|
+
Just like with other Storybook frameworks, you can make your stories interactive by defining args and using the testing library's `play` function to simulate user interactions.
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
If you wish to contribute or modify the integration:
|
|
176
|
+
|
|
177
|
+
1. **Build the package** using TypeScript:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm run build
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
2. **Watch for changes** during development:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
npm run watch
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
3. **Link the package** in your Aurelia project:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npm link @aurelia/storybook
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
4. **Run Storybook** in your local Aurelia application to see the integration in action.
|
|
196
|
+
|
|
197
|
+
## Contributing
|
|
198
|
+
|
|
199
|
+
Contributions, bug reports, and feature requests are welcome. Please open an issue or submit a pull request on the project repository.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
[MIT](LICENSE)
|
|
204
|
+
|
|
205
|
+
## Acknowledgements
|
|
206
|
+
|
|
207
|
+
Special shout out to Dmitry (@ekzobrain on GitHub) for the work he did on Storybook support for earlier versions of Storybook, which helped lay some of the groundwork for this implementation [https://github.com/ekzobrain/storybook](https://github.com/ekzobrain/storybook).
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { STORY_CHANGED } from '@storybook/core-events';
|
|
2
|
+
import { CustomElement } from 'aurelia';
|
|
3
|
+
import {
|
|
4
|
+
render,
|
|
5
|
+
renderToCanvas,
|
|
6
|
+
bootstrapAureliaApp,
|
|
7
|
+
createComponentTemplate,
|
|
8
|
+
} from '../src/preview/render';
|
|
9
|
+
|
|
10
|
+
// Add this at the very top of the file, before any imports.
|
|
11
|
+
jest.mock('aurelia', () => {
|
|
12
|
+
const actual = jest.requireActual('aurelia');
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
CustomElement: {
|
|
16
|
+
...actual.CustomElement,
|
|
17
|
+
getDefinition: jest.fn().mockReturnValue({
|
|
18
|
+
name: 'dummy-comp',
|
|
19
|
+
bindables: { prop: { attribute: 'prop', name: 'prop' } },
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('render', () => {
|
|
26
|
+
it('throws an error when no component is provided', () => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
render({}, { id: 'story-1', component: undefined as any } as any)
|
|
29
|
+
).toThrowError(
|
|
30
|
+
'Unable to render story story-1 as the component annotation is missing from the default export'
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns the expected object when a component is provided', () => {
|
|
35
|
+
const DummyComponent = () => {};
|
|
36
|
+
const args = { foo: 'bar' };
|
|
37
|
+
const result = render(args, { id: 'story-1', component: DummyComponent as any } as any);
|
|
38
|
+
expect(result).toEqual({ Component: DummyComponent, props: args, template: '' });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('renderToCanvas', () => {
|
|
43
|
+
let canvas: HTMLElement;
|
|
44
|
+
let dummyChannel: { on: jest.Mock; off: jest.Mock };
|
|
45
|
+
const DummyComponent = class {};
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
canvas = document.createElement('div');
|
|
49
|
+
dummyChannel = { on: jest.fn(), off: jest.fn() } as any;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('calls showError when the story function returns a falsy value', async () => {
|
|
53
|
+
const storyFn = jest.fn(() => null) as any;
|
|
54
|
+
const showError = jest.fn() as any;
|
|
55
|
+
const showMain = jest.fn() as any;
|
|
56
|
+
const context = {
|
|
57
|
+
storyFn,
|
|
58
|
+
title: 'Test Title',
|
|
59
|
+
name: 'Test Story',
|
|
60
|
+
showMain,
|
|
61
|
+
showError,
|
|
62
|
+
storyContext: {
|
|
63
|
+
parameters: {},
|
|
64
|
+
component: DummyComponent as any,
|
|
65
|
+
args: {},
|
|
66
|
+
viewMode: 'story',
|
|
67
|
+
channel: dummyChannel,
|
|
68
|
+
},
|
|
69
|
+
forceRemount: false,
|
|
70
|
+
} as any;
|
|
71
|
+
|
|
72
|
+
const cleanup = await renderToCanvas(context, canvas);
|
|
73
|
+
expect(showError).toHaveBeenCalled();
|
|
74
|
+
expect(typeof cleanup).toBe('function');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('bootstraps an Aurelia app when none exists or forceRemount is true', async () => {
|
|
78
|
+
const fakeAurelia = {
|
|
79
|
+
start: jest.fn().mockResolvedValue(undefined),
|
|
80
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
81
|
+
root: { controller: { viewModel: {} } },
|
|
82
|
+
} as any;
|
|
83
|
+
const story = { template: '<div></div>', props: { test: 'value' } } as any;
|
|
84
|
+
const storyFn = jest.fn(() => story) as any;
|
|
85
|
+
const showError = jest.fn() as any;
|
|
86
|
+
const showMain = jest.fn() as any;
|
|
87
|
+
|
|
88
|
+
// Spy on bootstrapAureliaApp to simulate app creation.
|
|
89
|
+
const bootstrapSpy = jest
|
|
90
|
+
.spyOn(require('../src/preview/render'), 'bootstrapAureliaApp')
|
|
91
|
+
.mockReturnValue(fakeAurelia);
|
|
92
|
+
|
|
93
|
+
const context = {
|
|
94
|
+
storyFn,
|
|
95
|
+
title: 'Test Title',
|
|
96
|
+
name: 'Test Story',
|
|
97
|
+
showMain,
|
|
98
|
+
showError,
|
|
99
|
+
storyContext: {
|
|
100
|
+
parameters: { args: { param: 'foo' } },
|
|
101
|
+
component: DummyComponent as any,
|
|
102
|
+
args: { test: 'bar' },
|
|
103
|
+
viewMode: 'story',
|
|
104
|
+
channel: dummyChannel,
|
|
105
|
+
},
|
|
106
|
+
forceRemount: false,
|
|
107
|
+
} as any;
|
|
108
|
+
|
|
109
|
+
const cleanup = await renderToCanvas(context, canvas, bootstrapAureliaApp);
|
|
110
|
+
expect(showError).not.toHaveBeenCalled();
|
|
111
|
+
expect(showMain).toHaveBeenCalled();
|
|
112
|
+
expect(bootstrapSpy).toHaveBeenCalled();
|
|
113
|
+
|
|
114
|
+
// Simulate cleanup (which should remove the STORY_CHANGED listener)
|
|
115
|
+
await cleanup();
|
|
116
|
+
expect(dummyChannel.off).toHaveBeenCalledWith(
|
|
117
|
+
STORY_CHANGED,
|
|
118
|
+
expect.any(Function)
|
|
119
|
+
);
|
|
120
|
+
bootstrapSpy.mockRestore();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('updates the existing app viewModel when re-rendering without forceRemount', async () => {
|
|
124
|
+
// Create a fake Aurelia app with a mutable viewModel.
|
|
125
|
+
const fakeViewModel: Record<string, any> = {};
|
|
126
|
+
const fakeAurelia = {
|
|
127
|
+
start: jest.fn().mockResolvedValue(undefined),
|
|
128
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
129
|
+
root: { controller: { viewModel: fakeViewModel } },
|
|
130
|
+
} as any;
|
|
131
|
+
|
|
132
|
+
const story = { template: '<div></div>', props: { test: 'initial' } } as any;
|
|
133
|
+
const storyFn = jest.fn(() => story) as any;
|
|
134
|
+
const showError = jest.fn() as any;
|
|
135
|
+
const showMain = jest.fn() as any;
|
|
136
|
+
|
|
137
|
+
const bootstrapSpy = jest
|
|
138
|
+
.spyOn(require('../src/preview/render'), 'bootstrapAureliaApp')
|
|
139
|
+
.mockReturnValue(fakeAurelia);
|
|
140
|
+
|
|
141
|
+
// First render: bootstrap the app.
|
|
142
|
+
const context = {
|
|
143
|
+
storyFn,
|
|
144
|
+
title: 'Title',
|
|
145
|
+
name: 'Name',
|
|
146
|
+
showMain,
|
|
147
|
+
showError,
|
|
148
|
+
storyContext: {
|
|
149
|
+
parameters: { args: { param: 'foo' } },
|
|
150
|
+
component: DummyComponent as any,
|
|
151
|
+
args: { test: 'bar' },
|
|
152
|
+
viewMode: 'story',
|
|
153
|
+
channel: dummyChannel,
|
|
154
|
+
},
|
|
155
|
+
forceRemount: false,
|
|
156
|
+
} as any;
|
|
157
|
+
await renderToCanvas(context, canvas, bootstrapAureliaApp);
|
|
158
|
+
expect(bootstrapSpy).toHaveBeenCalledTimes(1);
|
|
159
|
+
|
|
160
|
+
// Second render with new args; should update viewModel instead of re-bootstrap.
|
|
161
|
+
const newStory = { template: '<div></div>', props: { test: 'updated' } } as any;
|
|
162
|
+
storyFn.mockReturnValueOnce(newStory);
|
|
163
|
+
const newContext = {
|
|
164
|
+
...context,
|
|
165
|
+
storyContext: {
|
|
166
|
+
...context.storyContext,
|
|
167
|
+
parameters: { args: { param: 'baz' } },
|
|
168
|
+
args: { test: 'qux' },
|
|
169
|
+
},
|
|
170
|
+
} as any;
|
|
171
|
+
await renderToCanvas(newContext, canvas, bootstrapAureliaApp);
|
|
172
|
+
expect(bootstrapSpy).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(fakeViewModel).toEqual({ param: 'baz', test: 'qux' });
|
|
174
|
+
bootstrapSpy.mockRestore();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('createComponentTemplate', () => {
|
|
179
|
+
it('generates the correct template string', () => {
|
|
180
|
+
const DummyComponent = class {};
|
|
181
|
+
// The definition is already provided via module mocking.
|
|
182
|
+
|
|
183
|
+
const template = createComponentTemplate(DummyComponent as any, '<span>inner</span>');
|
|
184
|
+
expect(template).toBe(
|
|
185
|
+
'<dummy-comp prop.bind="prop"><span>inner</span></dummy-comp>'
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { STORY_CHANGED } from '@storybook/core-events';
|
|
2
|
+
import { render, renderToCanvas, bootstrapAureliaApp, createComponentTemplate, } from '../src/preview/render';
|
|
3
|
+
// Add this at the very top of the file, before any imports.
|
|
4
|
+
jest.mock('aurelia', () => {
|
|
5
|
+
const actual = jest.requireActual('aurelia');
|
|
6
|
+
return {
|
|
7
|
+
...actual,
|
|
8
|
+
CustomElement: {
|
|
9
|
+
...actual.CustomElement,
|
|
10
|
+
getDefinition: jest.fn().mockReturnValue({
|
|
11
|
+
name: 'dummy-comp',
|
|
12
|
+
bindables: { prop: { attribute: 'prop', name: 'prop' } },
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
describe('render', () => {
|
|
18
|
+
it('throws an error when no component is provided', () => {
|
|
19
|
+
expect(() => render({}, { id: 'story-1', component: undefined })).toThrowError('Unable to render story story-1 as the component annotation is missing from the default export');
|
|
20
|
+
});
|
|
21
|
+
it('returns the expected object when a component is provided', () => {
|
|
22
|
+
const DummyComponent = () => { };
|
|
23
|
+
const args = { foo: 'bar' };
|
|
24
|
+
const result = render(args, { id: 'story-1', component: DummyComponent });
|
|
25
|
+
expect(result).toEqual({ Component: DummyComponent, props: args, template: '' });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('renderToCanvas', () => {
|
|
29
|
+
let canvas;
|
|
30
|
+
let dummyChannel;
|
|
31
|
+
const DummyComponent = class {
|
|
32
|
+
};
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
canvas = document.createElement('div');
|
|
35
|
+
dummyChannel = { on: jest.fn(), off: jest.fn() };
|
|
36
|
+
});
|
|
37
|
+
it('calls showError when the story function returns a falsy value', async () => {
|
|
38
|
+
const storyFn = jest.fn(() => null);
|
|
39
|
+
const showError = jest.fn();
|
|
40
|
+
const showMain = jest.fn();
|
|
41
|
+
const context = {
|
|
42
|
+
storyFn,
|
|
43
|
+
title: 'Test Title',
|
|
44
|
+
name: 'Test Story',
|
|
45
|
+
showMain,
|
|
46
|
+
showError,
|
|
47
|
+
storyContext: {
|
|
48
|
+
parameters: {},
|
|
49
|
+
component: DummyComponent,
|
|
50
|
+
args: {},
|
|
51
|
+
viewMode: 'story',
|
|
52
|
+
channel: dummyChannel,
|
|
53
|
+
},
|
|
54
|
+
forceRemount: false,
|
|
55
|
+
};
|
|
56
|
+
const cleanup = await renderToCanvas(context, canvas);
|
|
57
|
+
expect(showError).toHaveBeenCalled();
|
|
58
|
+
expect(typeof cleanup).toBe('function');
|
|
59
|
+
});
|
|
60
|
+
it('bootstraps an Aurelia app when none exists or forceRemount is true', async () => {
|
|
61
|
+
const fakeAurelia = {
|
|
62
|
+
start: jest.fn().mockResolvedValue(undefined),
|
|
63
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
64
|
+
root: { controller: { viewModel: {} } },
|
|
65
|
+
};
|
|
66
|
+
const story = { template: '<div></div>', props: { test: 'value' } };
|
|
67
|
+
const storyFn = jest.fn(() => story);
|
|
68
|
+
const showError = jest.fn();
|
|
69
|
+
const showMain = jest.fn();
|
|
70
|
+
// Spy on bootstrapAureliaApp to simulate app creation.
|
|
71
|
+
const bootstrapSpy = jest
|
|
72
|
+
.spyOn(require('../src/preview/render'), 'bootstrapAureliaApp')
|
|
73
|
+
.mockReturnValue(fakeAurelia);
|
|
74
|
+
const context = {
|
|
75
|
+
storyFn,
|
|
76
|
+
title: 'Test Title',
|
|
77
|
+
name: 'Test Story',
|
|
78
|
+
showMain,
|
|
79
|
+
showError,
|
|
80
|
+
storyContext: {
|
|
81
|
+
parameters: { args: { param: 'foo' } },
|
|
82
|
+
component: DummyComponent,
|
|
83
|
+
args: { test: 'bar' },
|
|
84
|
+
viewMode: 'story',
|
|
85
|
+
channel: dummyChannel,
|
|
86
|
+
},
|
|
87
|
+
forceRemount: false,
|
|
88
|
+
};
|
|
89
|
+
const cleanup = await renderToCanvas(context, canvas, bootstrapAureliaApp);
|
|
90
|
+
expect(showError).not.toHaveBeenCalled();
|
|
91
|
+
expect(showMain).toHaveBeenCalled();
|
|
92
|
+
expect(bootstrapSpy).toHaveBeenCalled();
|
|
93
|
+
// Simulate cleanup (which should remove the STORY_CHANGED listener)
|
|
94
|
+
await cleanup();
|
|
95
|
+
expect(dummyChannel.off).toHaveBeenCalledWith(STORY_CHANGED, expect.any(Function));
|
|
96
|
+
bootstrapSpy.mockRestore();
|
|
97
|
+
});
|
|
98
|
+
it('updates the existing app viewModel when re-rendering without forceRemount', async () => {
|
|
99
|
+
// Create a fake Aurelia app with a mutable viewModel.
|
|
100
|
+
const fakeViewModel = {};
|
|
101
|
+
const fakeAurelia = {
|
|
102
|
+
start: jest.fn().mockResolvedValue(undefined),
|
|
103
|
+
stop: jest.fn().mockResolvedValue(undefined),
|
|
104
|
+
root: { controller: { viewModel: fakeViewModel } },
|
|
105
|
+
};
|
|
106
|
+
const story = { template: '<div></div>', props: { test: 'initial' } };
|
|
107
|
+
const storyFn = jest.fn(() => story);
|
|
108
|
+
const showError = jest.fn();
|
|
109
|
+
const showMain = jest.fn();
|
|
110
|
+
const bootstrapSpy = jest
|
|
111
|
+
.spyOn(require('../src/preview/render'), 'bootstrapAureliaApp')
|
|
112
|
+
.mockReturnValue(fakeAurelia);
|
|
113
|
+
// First render: bootstrap the app.
|
|
114
|
+
const context = {
|
|
115
|
+
storyFn,
|
|
116
|
+
title: 'Title',
|
|
117
|
+
name: 'Name',
|
|
118
|
+
showMain,
|
|
119
|
+
showError,
|
|
120
|
+
storyContext: {
|
|
121
|
+
parameters: { args: { param: 'foo' } },
|
|
122
|
+
component: DummyComponent,
|
|
123
|
+
args: { test: 'bar' },
|
|
124
|
+
viewMode: 'story',
|
|
125
|
+
channel: dummyChannel,
|
|
126
|
+
},
|
|
127
|
+
forceRemount: false,
|
|
128
|
+
};
|
|
129
|
+
await renderToCanvas(context, canvas, bootstrapAureliaApp);
|
|
130
|
+
expect(bootstrapSpy).toHaveBeenCalledTimes(1);
|
|
131
|
+
// Second render with new args; should update viewModel instead of re-bootstrap.
|
|
132
|
+
const newStory = { template: '<div></div>', props: { test: 'updated' } };
|
|
133
|
+
storyFn.mockReturnValueOnce(newStory);
|
|
134
|
+
const newContext = {
|
|
135
|
+
...context,
|
|
136
|
+
storyContext: {
|
|
137
|
+
...context.storyContext,
|
|
138
|
+
parameters: { args: { param: 'baz' } },
|
|
139
|
+
args: { test: 'qux' },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
await renderToCanvas(newContext, canvas, bootstrapAureliaApp);
|
|
143
|
+
expect(bootstrapSpy).toHaveBeenCalledTimes(1);
|
|
144
|
+
expect(fakeViewModel).toEqual({ param: 'baz', test: 'qux' });
|
|
145
|
+
bootstrapSpy.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('createComponentTemplate', () => {
|
|
149
|
+
it('generates the correct template string', () => {
|
|
150
|
+
const DummyComponent = class {
|
|
151
|
+
};
|
|
152
|
+
// The definition is already provided via module mocking.
|
|
153
|
+
const template = createComponentTemplate(DummyComponent, '<span>inner</span>');
|
|
154
|
+
expect(template).toBe('<dummy-comp prop.bind="prop"><span>inner</span></dummy-comp>');
|
|
155
|
+
});
|
|
156
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/preset.d.ts
ADDED
package/dist/preset.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/preset.ts
|
|
2
|
+
// Minimal preset for Storybook-Aurelia2
|
|
3
|
+
/**
|
|
4
|
+
* Optionally adjust the Vite configuration.
|
|
5
|
+
*/
|
|
6
|
+
export async function viteFinal(config) {
|
|
7
|
+
// For now, return the config unchanged.
|
|
8
|
+
return config;
|
|
9
|
+
}
|
|
10
|
+
// Export a default for compatibility.
|
|
11
|
+
export default { viteFinal };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RenderContext, ArgsStoryFn } from '@storybook/types';
|
|
2
|
+
import type { AureliaRenderer } from './types';
|
|
3
|
+
import Aurelia, { Constructable } from 'aurelia';
|
|
4
|
+
interface AureliaStoryResult {
|
|
5
|
+
template: string;
|
|
6
|
+
components?: unknown[];
|
|
7
|
+
Component?: unknown;
|
|
8
|
+
container?: any;
|
|
9
|
+
items?: unknown[];
|
|
10
|
+
innerHtml?: string;
|
|
11
|
+
props?: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
export declare const render: ArgsStoryFn<AureliaRenderer>;
|
|
14
|
+
export declare function renderToCanvas({ storyFn, title, name, showMain, showError, storyContext, forceRemount, }: RenderContext<AureliaRenderer>, canvasElement: HTMLElement): Promise<() => void>;
|
|
15
|
+
export declare function bootstrapAureliaApp(story: AureliaStoryResult, args: Record<string, any>, domElement: HTMLElement, component?: Constructable): Omit<Aurelia, "enhance" | "register" | "app">;
|
|
16
|
+
export declare function createComponentTemplate(component: Constructable, innerHtml?: string): string;
|
|
17
|
+
export {};
|