diff --git a/README.md b/README.md index c4ff28a5c..0fe53a4e6 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ Currently, the [Intro to Storybook tutorial](https://storybook.js.org/tutorials/ | | Spanish | ❌ | | | Portuguese | ❌ | | | Japanese | ✅ | -| Svelte | English | ❌ | +| Svelte | English | ✅ | | | Spanish | ❌ | | Ember | English | ❌ | diff --git a/content/intro-to-storybook/svelte/en/composite-component.md b/content/intro-to-storybook/svelte/en/composite-component.md index 4f337f04f..97b84fcb7 100644 --- a/content/intro-to-storybook/svelte/en/composite-component.md +++ b/content/intro-to-storybook/svelte/en/composite-component.md @@ -25,13 +25,18 @@ Start with a rough implementation of the `TaskList`. You’ll need to import the ```html:title=src/components/TaskList.svelte + {#if loading}
loading
{/if} @@ -41,6 +46,7 @@ Start with a rough implementation of the `TaskList`. You’ll need to import the {#each tasks as task} {/each} + ``` Next, create `MarginDecorator` with the following inside: @@ -68,72 +74,75 @@ import * as TaskStories from './Task.stories'; export default { component: TaskList, + title: 'TaskList', + tags: ['autodocs'], //👇 The auxiliary component will be added as a decorator to help show the UI correctly decorators: [() => MarginDecorator], - title: 'TaskList', - argTypes: { - onPinTask: { action: 'onPinTask' }, - onArchiveTask: { action: 'onArchiveTask' }, - }, + render: (args) => ({ + Component: TaskList, + props: args, + on: { + ...TaskStories.actionsData, + }, + }), }; -const Template = args => ({ - Component: TaskList, - props: args, - on: { - ...TaskStories.actionsData, +export const Default = { + args: { + // Shaping the stories through args composition. + // The data was inherited from the Default story in task.stories.js. + tasks: [ + { ...TaskStories.Default.args.task, id: '1', title: 'Task 1' }, + { ...TaskStories.Default.args.task, id: '2', title: 'Task 2' }, + { ...TaskStories.Default.args.task, id: '3', title: 'Task 3' }, + { ...TaskStories.Default.args.task, id: '4', title: 'Task 4' }, + { ...TaskStories.Default.args.task, id: '5', title: 'Task 5' }, + { ...TaskStories.Default.args.task, id: '6', title: 'Task 6' }, + ], }, -}); -export const Default = Template.bind({}); -Default.args = { - // Shaping the stories through args composition. - // The data was inherited from the Default story in task.stories.js. - tasks: [ - { ...TaskStories.Default.args.task, id: '1', title: 'Task 1' }, - { ...TaskStories.Default.args.task, id: '2', title: 'Task 2' }, - { ...TaskStories.Default.args.task, id: '3', title: 'Task 3' }, - { ...TaskStories.Default.args.task, id: '4', title: 'Task 4' }, - { ...TaskStories.Default.args.task, id: '5', title: 'Task 5' }, - { ...TaskStories.Default.args.task, id: '6', title: 'Task 6' }, - ], }; -export const WithPinnedTasks = Template.bind({}); -WithPinnedTasks.args = { - // Shaping the stories through args composition. - // Inherited data coming from the Default story. - tasks: [ - ...Default.args.tasks.slice(0, 5), - { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, - ], +export const WithPinnedTasks = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Default story. + tasks: [ + ...Default.args.tasks.slice(0, 5), + { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, + ], + }, }; -export const Loading = Template.bind({}); -Loading.args = { - tasks: [], - loading: true, +export const Loading = { + args: { + tasks: [], + loading: true, + }, }; -export const Empty = Template.bind({}); -Empty.args = { - // Shaping the stories through args composition. - // Inherited data coming from the Loading story. - ...Loading.args, - loading: false, +export const Empty = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Loading story. + ...Loading.args, + loading: false, + }, }; ```
-💡 Decorators are a way to provide arbitrary wrappers to stories. In this case we’re using a decorator `key` on the default export to add styling around the rendered component. They can also be used to add other context to components. + +[**Decorators**](https://storybook.js.org/docs/writing-stories/decorators) are a way to provide arbitrary wrappers to stories. In this case we’re using a decorator `key` on the default export to add styling around the rendered component. They can also be used to add other context to components. +
-By importing `TaskStories`, we were able to [compose](https://storybook.js.org/docs/svelte/writing-stories/args#args-composition) the arguments (args for short) in our stories with minimal effort. That way, the data and actions (mocked callbacks) expected by both components are preserved. +By importing `TaskStories`, we were able to [compose](https://storybook.js.org/docs/writing-stories/args#args-composition) the arguments (args for short) in our stories with minimal effort. That way, the data and actions (mocked callbacks) expected by both components are preserved. Now check Storybook for the new `TaskList` stories. @@ -163,15 +172,19 @@ And update `TaskList.svelte` to the following: @@ -202,7 +215,7 @@ The added markup results in the following UI: diff --git a/content/intro-to-storybook/svelte/en/conclusion.md b/content/intro-to-storybook/svelte/en/conclusion.md index 8657149a5..178b47a69 100644 --- a/content/intro-to-storybook/svelte/en/conclusion.md +++ b/content/intro-to-storybook/svelte/en/conclusion.md @@ -7,16 +7,15 @@ Congratulations! You created your first UI in Storybook. Along the way you learn [📕 **GitHub repo: chromaui/learnstorybook-code**](https://github.com/chromaui/learnstorybook-code)
- [🌎 **Deployed Storybook**](https://master--5ccbe484c994280020b6d128.chromatic.com) -Storybook is a powerful tool for React, React Native, Vue, and Angular. It has a thriving developer community and a wealth of addons. This introduction scratches the surface of what’s possible. We’re confident that once you adopt Storybook, you’ll be impressed by how productive it is to build durable UIs. +Storybook is a powerful tool for React, React Native, Vue, Angular, Svelte and many other frameworks. It has a thriving developer community and a wealth of addons. This introduction scratches the surface of what’s possible. We’re confident that once you adopt Storybook, you’ll be impressed by how productive it is to build durable UIs. ## Learn more Want to dive deeper? Here are helpful resources. -- [**Official Storybook documentation**](https://storybook.js.org/docs/react/get-started/introduction) has API documentation, community links, and the addon gallery. +- [**Official Storybook documentation**](https://storybook.js.org/docs/get-started/install) has API documentation, community links, and the addon gallery. - [**UI Testing Playbook**](https://storybook.js.org/blog/ui-testing-playbook/) highlights workflow best practices used by high-velocity teams at Twilio, Adobe, Peloton, and Shopify. diff --git a/content/intro-to-storybook/svelte/en/data.md b/content/intro-to-storybook/svelte/en/data.md index 491a376c8..03740ef72 100644 --- a/content/intro-to-storybook/svelte/en/data.md +++ b/content/intro-to-storybook/svelte/en/data.md @@ -12,7 +12,7 @@ This tutorial doesn’t focus on the particulars of building an app, so we won Our `TaskList` component as currently written is “presentational” in that it doesn’t talk to anything external to its own implementation. To get data into it, we need a “container”. -This example uses [Svelte's Stores](https://svelte.dev/docs#svelte_store), Svelte's default data management API, to build a simple data model for our app. However, the pattern used here applies just as well to other data management libraries like [Apollo](https://www.apollographql.com/client/) and [MobX](https://mobx.js.org/). +This example uses [Svelte's Stores](https://svelte.dev/docs/svelte-store), Svelte's default data management API, to build a simple data model for our app. However, the pattern used here applies just as well to other data management libraries like [Apollo](https://www.apollographql.com/client/) and [MobX](https://mobx.js.org/). First, we’ll construct a simple Svelte store that responds to actions that change the state of tasks in a file called `store.js` in the `src` directory (intentionally kept simple): @@ -21,34 +21,48 @@ First, we’ll construct a simple Svelte store that responds to actions that cha // A true app would be more complex and separated into different files. import { writable } from 'svelte/store'; +/* + * The initial state of our store when the app loads. + * Usually, you would fetch this from a server. Let's not worry about that now + */ +const defaultTasks = [ + { id: '1', title: 'Something', state: 'TASK_INBOX' }, + { id: '2', title: 'Something more', state: 'TASK_INBOX' }, + { id: '3', title: 'Something else', state: 'TASK_INBOX' }, + { id: '4', title: 'Something again', state: 'TASK_INBOX' }, +]; const TaskBox = () => { // Creates a new writable store populated with some initial data - const { subscribe, update } = writable([ - { id: '1', title: 'Something', state: 'TASK_INBOX' }, - { id: '2', title: 'Something more', state: 'TASK_INBOX' }, - { id: '3', title: 'Something else', state: 'TASK_INBOX' }, - { id: '4', title: 'Something again', state: 'TASK_INBOX' }, - ]); + const { subscribe, update } = writable({ + tasks: defaultTasks, + status: 'idle', + error: false, + }); return { subscribe, // Method to archive a task, think of a action with redux or Pinia archiveTask: (id) => - update((tasks) => - tasks + update((store) => { + const filteredTasks = store.tasks .map((task) => task.id === id ? { ...task, state: 'TASK_ARCHIVED' } : task ) - .filter((t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED') - ), + .filter((t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED'); + + return { ...store, tasks: filteredTasks }; + }), // Method to archive a task, think of a action with redux or Pinia - pinTask: (id) => - update((tasks) => - tasks.map((task) => - task.id === id ? { ...task, state: 'TASK_PINNED' } : task - ) - ), + pinTask: (id) => { + update((store) => { + const task = store.tasks.find((t) => t.id === id); + if (task) { + task.state = 'TASK_PINNED'; + } + return store; + }); + }, }; }; export const taskStore = TaskBox(); @@ -63,10 +77,14 @@ In `src/components/PureTaskList.svelte`: @@ -131,64 +148,65 @@ import * as TaskStories from './Task.stories'; export default { component: PureTaskList, + title: 'PureTaskList', + tags: ['autodocs'], //👇 The auxiliary component will be added as a decorator to help show the UI correctly decorators: [() => MarginDecorator], - title: 'PureTaskList', - argTypes: { - onPinTask: { action: 'onPinTask' }, - onArchiveTask: { action: 'onArchiveTask' }, - }, + render: (args) => ({ + Component: PureTaskList, + props: args, + on: { + ...TaskStories.actionsData, + }, + }), }; -const Template = args => ({ - Component: PureTaskList, - props: args, - on: { - ...TaskStories.actionsData, +export const Default = { + args: { + // Shaping the stories through args composition. + // The data was inherited from the Default story in task.stories.js. + tasks: [ + { ...TaskStories.Default.args.task, id: '1', title: 'Task 1' }, + { ...TaskStories.Default.args.task, id: '2', title: 'Task 2' }, + { ...TaskStories.Default.args.task, id: '3', title: 'Task 3' }, + { ...TaskStories.Default.args.task, id: '4', title: 'Task 4' }, + { ...TaskStories.Default.args.task, id: '5', title: 'Task 5' }, + { ...TaskStories.Default.args.task, id: '6', title: 'Task 6' }, + ], }, -}); -export const Default = Template.bind({}); -Default.args = { - // Shaping the stories through args composition. - // The data was inherited from the Default story in task.stories.js. - tasks: [ - { ...TaskStories.Default.args.task, id: '1', title: 'Task 1' }, - { ...TaskStories.Default.args.task, id: '2', title: 'Task 2' }, - { ...TaskStories.Default.args.task, id: '3', title: 'Task 3' }, - { ...TaskStories.Default.args.task, id: '4', title: 'Task 4' }, - { ...TaskStories.Default.args.task, id: '5', title: 'Task 5' }, - { ...TaskStories.Default.args.task, id: '6', title: 'Task 6' }, - ], }; -export const WithPinnedTasks = Template.bind({}); -WithPinnedTasks.args = { - // Shaping the stories through args composition. - // Inherited data coming from the Default story. - tasks: [ - ...Default.args.tasks.slice(0, 5), - { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, - ], +export const WithPinnedTasks = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Default story. + tasks: [ + ...Default.args.tasks.slice(0, 5), + { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, + ], + }, }; -export const Loading = Template.bind({}); -Loading.args = { - tasks: [], - loading: true, +export const Loading = { + args: { + tasks: [], + loading: true, + }, }; -export const Empty = Template.bind({}); -Empty.args = { - // Shaping the stories through args composition. - // Inherited data coming from the Loading story. - ...Loading.args, - loading: false, +export const Empty = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Loading story. + ...Loading.args, + loading: false, + }, }; ``` diff --git a/content/intro-to-storybook/svelte/en/deploy.md b/content/intro-to-storybook/svelte/en/deploy.md index 9e5526e00..0fba7ca65 100644 --- a/content/intro-to-storybook/svelte/en/deploy.md +++ b/content/intro-to-storybook/svelte/en/deploy.md @@ -15,7 +15,7 @@ Running `yarn build-storybook` will output a static Storybook in the `storybook- ## Publish Storybook -This tutorial uses Chromatic, a free publishing service made by the Storybook maintainers. It allows us to deploy and host our Storybook safely and securely in the cloud. +This tutorial uses [Chromatic](https://www.chromatic.com/?utm_source=storybook_website&utm_medium=link&utm_campaign=storyboo), a free publishing service made by the Storybook maintainers. It allows us to deploy and host our Storybook safely and securely in the cloud. ### Set up a repository in GitHub @@ -66,7 +66,7 @@ yarn chromatic --project-token= When finished, you'll get a link `https://random-uuid.chromatic.com` to your published Storybook. Share the link with your team to get feedback. -![Storybook deployed with chromatic package](/intro-to-storybook/chromatic-manual-storybook-deploy-6-0.png) +![Storybook deployed with chromatic package](/intro-to-storybook/chromatic-manual-storybook-deploy.png) Hooray! We published Storybook with one command, but manually running a command every time we want to get feedback on UI implementation is repetitive. Ideally, we'd publish the latest version of components whenever we push code. We'll need to continuously deploy Storybook. @@ -82,7 +82,7 @@ Create a new file called `chromatic.yml` like the one below. ```yaml:title=.github/workflows/chromatic.yml # Workflow name -name: 'Chromatic Deployment' +name: "Chromatic Deployment" # Event for the workflow on: push @@ -94,10 +94,12 @@ jobs: runs-on: ubuntu-latest # Job steps steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - run: yarn #👇 Adds Chromatic as a step in the workflow - - uses: chromaui/action@v1 + - uses: chromaui/action@latest # Options required for Chromatic's GitHub Action with: #👇 Chromatic projectToken, see https://storybook.js.org/tutorials/intro-to-storybook/svelte/en/deploy/ to obtain it @@ -105,7 +107,11 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ``` -

💡 For brevity purposes GitHub secrets weren't mentioned. Secrets are secure environment variables provided by GitHub so that you don't need to hard code the project-token.

+
+ +💡 For brevity purposes [GitHub secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) weren't mentioned. Secrets are secure environment variables provided by GitHub so that you don't need to hard code the `project-token`. + +
### Commit the action diff --git a/content/intro-to-storybook/svelte/en/get-started.md b/content/intro-to-storybook/svelte/en/get-started.md index 7127e7860..34755ab56 100644 --- a/content/intro-to-storybook/svelte/en/get-started.md +++ b/content/intro-to-storybook/svelte/en/get-started.md @@ -31,17 +31,16 @@ yarn Now we can quickly check that the various environments of our application are working properly: ```shell:clipboard=false -# Run the test runner (Jest) in a terminal: -yarn test - # Start the component explorer on port 6006: yarn storybook -# Run the frontend app proper on port 5000: +# Run the frontend app proper on port 5173: yarn dev ``` -Our three frontend app modalities: automated test (Jest), component development (Storybook), and the app itself. +Our main frontend app modalities: component development (Storybook), and the application itself. + + ![3 modalities](/intro-to-storybook/app-three-modalities-svelte.png) diff --git a/content/intro-to-storybook/svelte/en/screen.md b/content/intro-to-storybook/svelte/en/screen.md index a809a7841..eeac4cdfa 100644 --- a/content/intro-to-storybook/svelte/en/screen.md +++ b/content/intro-to-storybook/svelte/en/screen.md @@ -18,50 +18,60 @@ Let's start by updating our Svelte store (in `src/store.js`) to include our new // A simple Svelte store implementation with update methods and initial data. // A true app would be more complex and separated into different files. -import { writable } from 'svelte/store'; +import { writable } from "svelte/store"; + +/* + * The initial state of our store when the app loads. + * Usually, you would fetch this from a server. Let's not worry about that now + */ +const defaultTasks = [ + { id: '1', title: 'Something', state: 'TASK_INBOX' }, + { id: '2', title: 'Something more', state: 'TASK_INBOX' }, + { id: '3', title: 'Something else', state: 'TASK_INBOX' }, + { id: '4', title: 'Something again', state: 'TASK_INBOX' }, +]; const TaskBox = () => { // Creates a new writable store populated with some initial data - const { subscribe, update } = writable([ - { id: '1', title: 'Something', state: 'TASK_INBOX' }, - { id: '2', title: 'Something more', state: 'TASK_INBOX' }, - { id: '3', title: 'Something else', state: 'TASK_INBOX' }, - { id: '4', title: 'Something again', state: 'TASK_INBOX' }, - ]); - + const { subscribe, update } = writable({ + tasks: defaultTasks, + status: 'idle', + error: false, + }); return { subscribe, // Method to archive a task, think of a action with redux or Pinia archiveTask: (id) => - update((tasks) => - tasks.map(task => (task.id === id ? { ...task, state: 'TASK_ARCHIVED' } : task)).filter((t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED') - ), + update((store) => { + const filteredTasks = store.tasks + .map((task) => + task.id === id ? { ...task, state: 'TASK_ARCHIVED' } : task + ) + .filter((t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED'); + + return { ...store, tasks: filteredTasks }; + }), // Method to archive a task, think of a action with redux or Pinia - pinTask: (id) => - update((tasks) => - tasks.map(task => (task.id === id ? { ...task, state: 'TASK_PINNED' } : task)) - ), + pinTask: (id) => { + update((store) => { + const task = store.tasks.find((t) => t.id === id); + if (task) { + task.state = 'TASK_PINNED'; + } + return store; + }); + }, ++ isError: () => update((store) => ({ ...store, error: true })), }; }; export const taskStore = TaskBox(); - -+ // Store to handle the app state -+ const AppState = () => { -+ const { subscribe, update } = writable(false); -+ return { -+ subscribe, -+ error: () => update(error => !error), -+ }; -+ }; - -+ export const AppStore = AppState(); ``` Now that we have the store updated with the new field. Let's create `InboxScreen.svelte` in your `components` directory: ```html:title=src/components/InboxScreen.svelte @@ -70,16 +80,14 @@ Now that we have the store updated with the new field. Let's create `InboxScreen
-
Oh no!
-
Something went wrong
+

Oh no!

+

Something went wrong

{:else}
@@ -91,12 +99,25 @@ We also need to change the `App` component to render the `InboxScreen` (eventual ```html:title=src/App.svelte - + +``` + +And finally, the `src/main.js`: + +```diff:title=src/main.js +- import './app.css'; ++ import './index.css'; +import App from './App.svelte'; + +const app = new App({ + target: document.getElementById("app"), +}); + +export default app; ``` However, where things get interesting is in rendering the story in Storybook. @@ -113,33 +134,30 @@ import InboxScreen from './InboxScreen.svelte'; export default { component: InboxScreen, title: 'InboxScreen', + tags: ['autodocs'], }; -const Template = args => ({ - Component: InboxScreen, - props: args, -}); - -export const Default = Template.bind({}); +export const Default = {}; -export const Error = Template.bind({}); -Error.args = { - error: true, +export const Error = { + args: { error: true }, }; ``` We see that both the `Error` and `Default` stories work just fine.
-💡 As an aside, passing data down the hierarchy is a legitimate approach, especially when using GraphQL. It’s how we have built Chromatic alongside 800+ stories. + +💡 As an aside, passing data down the hierarchy is a legitimate approach, especially when using [GraphQL](http://graphql.org/). It’s how we have built [Chromatic](https://www.chromatic.com/?utm_source=storybook_website&utm_medium=link&utm_campaign=storybook) alongside 800+ stories. +stories. +
Cycling through states in Storybook makes it easy to test we’ve done this correctly: @@ -152,7 +170,7 @@ Can't we automate this workflow and test our component interactions automaticall ### Write an interaction test using the play function -Storybook's [`play`](https://storybook.js.org/docs/svelte/writing-stories/play-function) and [`@storybook/addon-interactions`](https://storybook.js.org/docs/svelte/writing-tests/interaction-testing) help us with that. A play function includes small snippets of code that run after the story renders. +Storybook's [`play`](https://storybook.js.org/docs/writing-stories/play-function) and [`@storybook/addon-interactions`](https://storybook.js.org/docs/writing-tests/interaction-testing) help us with that. A play function includes small snippets of code that run after the story renders. The play function helps us verify what happens to the UI when tasks are updated. It uses framework-agnostic DOM APIs, which means we can write stories with the play function to interact with the UI and simulate human behavior no matter the frontend framework. @@ -161,41 +179,44 @@ The `@storybook/addon-interactions` helps us visualize our tests in Storybook, p Let's see it in action! Update your newly created `PureInboxScreen` story, and set up component interactions by adding the following: ```diff:title=src/components/InboxScreen.stories.js -+ import { fireEvent, within } from '@storybook/testing-library'; import InboxScreen from './InboxScreen.svelte'; ++ import { fireEvent, within } from '@storybook/test'; + export default { component: InboxScreen, title: 'InboxScreen', + tags: ['autodocs'], }; -const Template = args => ({ - Component: InboxScreen, - props: args, -}); +export const Default = {}; -export const Default = Template.bind({}); - -export const Error = Template.bind({}); -Error.args = { - error: true, +export const Error = { + args: { error: true }, }; -+ export const WithInteractions = Template.bind({}); -+ WithInteractions.play = async ({ canvasElement }) => { -+ const canvas = within(canvasElement); -+ // Simulates pinning the first task -+ await fireEvent.click(canvas.getByLabelText("pinTask-1")); -+ // Simulates pinning the third task -+ await fireEvent.click(canvas.getByLabelText("pinTask-3")); ++ export const WithInteractions = { ++ play: async ({ canvasElement }) => { ++ const canvas = within(canvasElement); ++ // Simulates pinning the first task ++ await fireEvent.click(canvas.getByLabelText('pinTask-1')); ++ // Simulates pinning the third task ++ await fireEvent.click(canvas.getByLabelText('pinTask-3')); ++ }, + }; ``` +
+ +💡 The `@storybook/test` package replaces the `@storybook/jest` and `@storybook/testing-library` testing packages, offering a smaller bundle size and a more straightforward API based on the [Vitest](https://vitest.dev/) package. + +
+ Check your newly created story. Click the `Interactions` panel to see the list of interactions inside the story's play function. @@ -206,7 +227,7 @@ With Storybook's play function, we were able to sidestep our problem, allowing u But, if we take a closer look at our Storybook, we can see that it only runs the interaction tests when viewing the story. Therefore, we'd still have to go through each story to run all checks if we make a change. Couldn't we automate it? -The good news is that we can! Storybook's [test runner](https://storybook.js.org/docs/vue/writing-tests/test-runner) allows us to do just that. It's a standalone utility—powered by [Playwright](https://playwright.dev/)—that runs all our interactions tests and catches broken stories. +The good news is that we can! Storybook's [test runner](https://storybook.js.org/docs/writing-tests/test-runner) allows us to do just that. It's a standalone utility—powered by [Playwright](https://playwright.dev/)—that runs all our interactions tests and catches broken stories. Let's see how it works! Run the following command to install it: @@ -231,9 +252,11 @@ yarn test-storybook --watch ```
-💡 Interaction testing with the play function is a fantastic way to test your UI components. It can do much more than we've seen here; we recommend reading the official documentation to learn more about it. -
-For an even deeper dive into testing, check out the Testing Handbook. It covers testing strategies used by scaled-front-end teams to supercharge your development workflow. + +💡 Interaction testing with the play function is a fantastic way to test your UI components. It can do much more than we've seen here; we recommend reading the [official documentation](https://storybook.js.org/docs/writing-tests/interaction-testing) to learn more about it. + +For an even deeper dive into testing, check out the [Testing Handbook](/ui-testing-handbook). It covers testing strategies used by scaled-front-end teams to supercharge your development workflow. +
![Storybook test runner successfully runs all tests](/intro-to-storybook/storybook-test-runner-execution.png) diff --git a/content/intro-to-storybook/svelte/en/simple-component.md b/content/intro-to-storybook/svelte/en/simple-component.md index 7651200c9..b39cc5377 100644 --- a/content/intro-to-storybook/svelte/en/simple-component.md +++ b/content/intro-to-storybook/svelte/en/simple-component.md @@ -29,21 +29,21 @@ We’ll begin with a baseline implementation of the `Task`, simply taking in the const dispatch = createEventDispatcher(); - // event handler for Pin Task + /** Event handler for the Pin Task */ function PinTask() { dispatch('onPinTask', { id: task.id, }); } - // event handler for Archive Task + /** Event handler for the Archive Task */ function ArchiveTask() { dispatch('onArchiveTask', { id: task.id, }); } - // Task props + /** Composition of the task */ export let task = { id: '', title: '', @@ -75,47 +75,53 @@ export const actionsData = { export default { component: Task, title: 'Task', + tags: ['autodocs'], + //👇 Our exports that end in "Data" are not stories. excludeStories: /.*Data$/, - //👇 The argTypes are included so that they are properly displayed in the Actions Panel - argTypes: { - onPinTask: { action: 'onPinTask' }, - onArchiveTask: { action: 'onArchiveTask' }, - }, + render: (args) => ({ + Component: Task, + props: args, + on: { + ...actionsData, + }, + }), }; -const Template = ({ onArchiveTask, onPinTask, ...args }) => ({ - Component: Task, - props: args, - on: { - ...actionsData, - }, -}); - -export const Default = Template.bind({}); -Default.args = { - task: { - id: '1', - title: 'Test Task', - state: 'TASK_INBOX', +export const Default = { + args: { + task: { + id: "1", + title: "Test Task", + state: "TASK_INBOX", + }, }, }; -export const Pinned = Template.bind({}); -Pinned.args = { - task: { - ...Default.args.task, - state: 'TASK_PINNED', + +export const Pinned = { + args: { + task: { + ...Default.args.task, + state: "TASK_PINNED", + }, }, }; -export const Archived = Template.bind({}); -Archived.args = { - task: { - ...Default.args.task, - state: 'TASK_ARCHIVED', +export const Archived = { + args: { + task: { + ...Default.args.task, + state: "TASK_ARCHIVED", + }, }, }; ``` +
+ +💡 [**Actions**](https://storybook.js.org/docs/svelte/essentials/actions) help you verify interactions when building UI components in isolation. Oftentimes you won't have access to the functions and state you have in context of the app. Use `action()` to stub them in. + +
+ There are two basic levels of organization in Storybook: the component and its child stories. Think of each story as a permutation of a component. You can have as many stories per component as you need. - **Component** @@ -125,32 +131,21 @@ There are two basic levels of organization in Storybook: the component and its c To tell Storybook about the component we are documenting, we create a `default` export that contains: -- `component`--the component itself, -- `title`--how to refer to the component in the sidebar of the Storybook app, -- `excludeStories`--information required by the story but should not be rendered by the Storybook app. -- `argTypes`--specify the [args](https://storybook.js.org/docs/svelte/api/argtypes) behavior in each story. - -To define our stories, we export a function for each of our test states to generate a story. The story is a function that returns a rendered element (i.e., a component class with a set of props) in a given state. +- `component` -- the component itself +- `title` -- how to refer to the component in the sidebar of the Storybook app +- `excludeStories` -- information required by the story but should not be rendered by the Storybook app +- `tags` -- to automatically generate documentation for our components +- `render` -- a function that gives additional control over how the story is rendered -As we have multiple permutations of our component, assigning it to a `Template` variable is convenient. Introducing this pattern in your stories will reduce the amount of code you need to write and maintain. +To define our stories, we'll use Component Story Format 3 (also known as [CSF3](https://storybook.js.org/docs/api/csf) ) to build out each of our test cases. This format is designed to build out each of our test cases in a concise way. By exporting an object containing each component state, we can define our tests more intuitively and author and reuse stories more efficiently. -
-💡 Template.bind({}) is a standard JavaScript technique for making a copy of a function. We use this technique to allow each exported story to set its own properties, but use the same implementation. -
- -Arguments or [`args`](https://storybook.js.org/docs/react/writing-stories/args) for short, allow us to live-edit our components with the controls addon without restarting Storybook. Once an [`args`](https://storybook.js.org/docs/react/writing-stories/args) value changes, so does the component. - -When creating a story, we use a base `task` arg to build out the shape of the task the component expects, typically modeled from what the actual data looks like. +Arguments or [`args`](https://storybook.js.org/docs/writing-stories/args) for short, allow us to live-edit our components with the controls addon without restarting Storybook. Once an [`args`](https://storybook.js.org/docs/writing-stories/args) value changes, so does the component. `action()` allows us to create a callback that appears in the **actions** panel of the Storybook UI when clicked. So when we build a pin button, we’ll be able to determine if a button click is successful in the UI. -As we need to pass the same set of actions to all permutations of our component, it is convenient to bundle them up into a single `actionsData` variable and pass them into our story definition each time. +As we need to pass the same set of actions to all permutations of our component, it is convenient to bundle them up into a single `actionsData` variable and pass them into our story definition each time. Another nice thing about bundling the `actionsData` that a component needs is that you can `export` them and use them in stories for components that reuse this component, as we'll see later. -Another nice thing about bundling the `actionsData` that a component needs is that you can `export` them and use them in stories for components that reuse this component, as we'll see later. - -
-💡 Actions help you verify interactions when building UI components in isolation. Oftentimes you won't have access to the functions and state you have in context of the app. Use action() to stub them in. -
+When creating a story, we use a base `task` arg to build out the shape of the task the component expects. Typically modeled from what the actual data looks like. Again, `export`-ing this shape will enable us to reuse it in later stories, as we'll see. ## Config @@ -159,9 +154,8 @@ We'll need to make a couple of changes to Storybook's configuration files so it Start by changing your Storybook configuration file (`.storybook/main.js`) to the following: ```diff:title=.storybook/main.js -// .storybook/main.js - -module.exports = { +/** @type { import('@storybook/svelte-vite').StorybookConfig } */ +const config = { - stories: [ - '../src/**/*.stories.mdx', - '../src/**/*.stories.@(js|jsx|ts|tsx)' @@ -171,18 +165,14 @@ module.exports = { addons: [ '@storybook/addon-links', '@storybook/addon-essentials', - '@storybook/addon-svelte-csf', '@storybook/addon-interactions', ], - features: { - postcss: false, - interactionsDebugger: true, - }, - framework: '@storybook/svelte', - core: { - builder: '@storybook/builder-webpack4', + framework: { + name: '@storybook/svelte-vite', + options: {}, }, }; +export default config; ``` After completing the change above, inside the `.storybook` folder, change your `preview.js` to the following: @@ -191,26 +181,31 @@ After completing the change above, inside the `.storybook` folder, change your ` + import '../src/index.css'; //👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI. -export const parameters = { - actions: { argTypesRegex: '^on[A-Z].*' }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, +/** @type { import('@storybook/svelte').Preview } */ +const preview = { + actions: { argTypesRegex: "^on.*" }, + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, }, }, }; + +export default preview; ``` -[`parameters`](https://storybook.js.org/docs/svelte/writing-stories/parameters) are typically used to control the behavior of Storybook's features and addons. In our case, we're going to use them to configure how the `actions` (mocked callbacks) are handled. +[`parameters`](https://storybook.js.org/docs/writing-stories/parameters) are typically used to control the behavior of Storybook's features and addons. In our case, we're going to use them to configure how the `actions` (mocked callbacks) are handled. -`actions` allows us to create callbacks that appear in the **actions** panel of the Storybook UI when clicked. So when we build a pin button, we’ll be able to determine if a button click is successful in the UI. +`actions` allows us to create callbacks that appear in the **Actions** panel of the Storybook UI when clicked. So when we build a pin button, we’ll be able to determine if a button click is successful in the UI. Once we’ve done this, restarting the Storybook server should yield test cases for the three Task states: @@ -223,55 +218,63 @@ The component is still rudimentary at the moment. First, write the code that ach ```html:title=src/components/Task.svelte
-