-
Notifications
You must be signed in to change notification settings - Fork 6.1k
feat(skills): add component-testing agent skill #41738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pavelfeldman
wants to merge
2
commits into
microsoft:main
Choose a base branch
from
pavelfeldman:component-testing-skill
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,9 @@ | |
|
|
||
| import 'playwright-core/lib/bootstrap'; | ||
|
|
||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
|
|
||
| import { libCli, tools } from 'playwright-core/lib/coreBundle'; | ||
| import { program } from 'commander'; | ||
| import { setBoxedStackPrefixes } from '@utils/stackTrace'; | ||
|
|
@@ -27,7 +30,7 @@ import { runTests, clearCache, runTestServerAction } from './cli/testActions'; | |
| import { showReport, mergeReports } from './cli/reportActions'; | ||
| import { TestServerBackend, testServerBackendTools } from './mcp/test/testBackend'; | ||
| import { ClaudeGenerator, CodexGenerator, OpencodeGenerator, VSCodeGenerator, CopilotGenerator } from './agents/generateAgents'; | ||
| import { packageRoot, packageJSON } from './package'; | ||
| import { libPath, packageRoot, packageJSON } from './package'; | ||
|
|
||
| export { program }; | ||
|
|
||
|
|
@@ -187,15 +190,19 @@ function addInitAgentsCommand(program: Command) { | |
|
|
||
| function addInitSkillsCommand(program: Command) { | ||
| const command = program.command('init-skills'); | ||
| command.description('Initialize the workspace and install Playwright CLI skills'); | ||
| command.description('Initialize the workspace and install Playwright skills'); | ||
| const option = command.createOption('--loop <loop>', 'Agentic loop provider'); | ||
| option.choices(['claude', 'generic']); | ||
| option.default('generic'); | ||
| command.addOption(option); | ||
| command.action(async opts => { | ||
| // Claude Code only reads skills from `.claude/skills`, every other agent reads | ||
| // them from the universal `.agents/skills` folder. | ||
| await tools.initWorkspace(opts.loop === 'claude' ? 'claude' : 'agents'); | ||
| const target = opts.loop === 'claude' ? 'claude' : 'agents'; | ||
| await tools.initWorkspace(target); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why u added one more line? |
||
| const skillDestDir = path.join(process.cwd(), `.${target}`, 'skills', 'playwright-component-testing'); | ||
| await fs.promises.cp(libPath('skills', 'component-testing'), skillDestDir, { recursive: true }); | ||
| console.log(`✅ Skills installed to \`${path.relative(process.cwd(), skillDestDir)}\`.`); | ||
| }); | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| --- | ||
| name: playwright-component-testing | ||
| description: Set up component testing with Playwright using a story gallery — scaffold stories, a gallery dev page and a mount fixture, no dedicated component-testing runtime. Use when asked to test React or Vue components in isolation with Playwright, or to migrate off @playwright/experimental-ct-react / -vue. | ||
| --- | ||
|
|
||
| # Component Testing with Playwright | ||
|
|
||
| Test components with regular Playwright e2e tests against a small **story gallery** page hosted by the app's own dev server. No extra test runner, bundler integration or npm packages are required. | ||
|
|
||
| ## Concept | ||
|
|
||
| - A **story** is a tiny wrapper component that embeds the component under test in one specific scenario: hard-coded props, mock data, providers, recorded callbacks. Stories live next to the component in `*.story.tsx` (or `.ts`/`.jsx`/`.js`/`.vue`) files; each named export is one story. | ||
| - The **gallery** is a single page (`playwright/gallery/index.html` + `main.tsx`) that discovers all story files with `import.meta.glob` and renders the story named by the `?story=<id>` URL parameter. Without the parameter it renders a browsable index of all stories. | ||
| - Tests are plain Playwright tests. A **`mount` fixture** navigates to a story's URL, waits for it to render, and returns a `Locator` for the story root — so specs read just like the old component tests. | ||
|
|
||
| Everything the component needs must be set up *inside the story* (it runs in the browser); everything the test asserts must be observable *through the page* (DOM, URL, network). There is no marshalling of props or callbacks between test and browser. | ||
|
|
||
| ## Setup workflow | ||
|
|
||
| 1. **Detect the framework and bundler.** React vs Vue decides which templates to use. Then: | ||
| - **App runs on Vite** (has `vite.config.*`): the gallery is served by the existing dev server at `/playwright/gallery/index.html` — Vite serves any `.html` file under the project root, the app's plugins/aliases/CSS apply automatically, and `vite build` ignores it. No extra server needed. | ||
| - **Anything else** (Next.js, webpack, no dev server): copy `templates/common/vite.config.ts` to `playwright/vite.config.ts` — a standalone Vite server whose root is the gallery directory. Requires `vite` and the framework plugin as devDependencies. | ||
| 2. **Copy the gallery**: `templates/<react|vue>/index.html` and `main.tsx` (Vue: `main.ts`) into `<project>/playwright/gallery/`. Adjust the `import.meta.glob` pattern in `main.*` if components do not live under `src/`. Import the app's global CSS into `index.html` or `main.*` the same way the app's own entry does. | ||
| 3. **Copy the fixture**: `templates/common/fixtures.ts` into `<project>/playwright/fixtures.ts`. If using the standalone gallery server, change `galleryUrl` to `'/'`. | ||
| 4. **Configure Playwright** — add to `playwright.config.ts`: | ||
|
|
||
| ```ts | ||
| projects: [ | ||
| { | ||
| name: 'components', | ||
| testDir: './tests/components', | ||
| use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173' }, | ||
| }, | ||
| ], | ||
| webServer: { | ||
| command: 'npm run dev', // or: npx vite --config playwright/vite.config.ts | ||
| url: 'http://localhost:5173/playwright/gallery/index.html', // standalone server: http://localhost:3100/ | ||
| reuseExistingServer: !process.env.CI, | ||
| }, | ||
| ``` | ||
|
|
||
| Match the port to the dev server. If the config already has projects/webServer, merge instead of replacing. | ||
| 5. **Write a first story** next to an existing component, modeled on `templates/<react|vue>/Button.story.*`. | ||
| 6. **Write a first spec**, modeled on `templates/react/button.spec.ts`, importing `test`/`expect` from the fixtures file. | ||
| 7. **Run**: `npx playwright test --project=components`. Open `http://localhost:5173/playwright/gallery/index.html` in a browser to eyeball all stories. | ||
|
|
||
| ## Conventions | ||
|
|
||
| - Story id: path under `src/` without the `.story.*` extension, plus the export name — `src/components/Button.story.tsx` export `Primary` → `components/Button/Primary`. Any unique suffix works too: `mount('Button/Primary')`. A `.story.vue` single-file component is one story, addressable by its path alone (its `default` export). | ||
| - One export per scenario. Prefer a new story export over parameterizing an existing one — stories are greppable, reviewable documentation of component states. | ||
| - Wire callbacks inside the story and record their effect into the DOM (e.g. `<output data-testid="...">`), then assert with locators. See `references/patterns.md`. | ||
| - Each `mount()` performs a fresh navigation, so tests are fully isolated; multiple mounts in one test are fine. | ||
|
|
||
| ## Decision points | ||
|
|
||
| - **Monorepos / non-`src` layouts**: change the glob in `main.*` and the `baseId` prefix stripping to match. | ||
| - **Global providers** (theme, i18n, store, router): create a shared `decorator` helper next to the gallery and wrap components in stories; see `references/react.md` / `references/vue.md`. | ||
| - **Network**: mock with `page.route()` in the test, or start MSW inside a story; see `references/patterns.md`. | ||
| - **Viewport, color scheme, locale**: standard Playwright — `test.use({ viewport, colorScheme, locale })`. | ||
|
|
||
| ## References | ||
|
|
||
| - `references/react.md` — React walkthrough: providers, StrictMode, CSS. | ||
| - `references/vue.md` — Vue walkthrough: `.story.ts` and `.story.vue` stories, plugins. | ||
| - `references/patterns.md` — callbacks and events, per-test args, network mocking, visual testing. | ||
| - `references/migration.md` — migrating from `@playwright/experimental-ct-react` / `-vue`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # Migrating from @playwright/experimental-ct-react / -vue | ||
|
|
||
| The CT packages mounted JSX from the test over a serialization boundary. The gallery pattern moves that setup into story files that run natively in the browser. Migration is mechanical: every distinct `mount(...)` call site becomes a story export. | ||
|
|
||
| ## Concept mapping | ||
|
|
||
| | `@playwright/experimental-ct-*` | Gallery pattern | | ||
| |---|---| | ||
| | `mount(<Button title="…" />)` | Story export `Primary = () => <Button title="…" />` + `mount('Button/Primary')` | | ||
| | Props from the test | Hard-coded in the story; `args` option for genuinely parametric cases | | ||
| | Callback props (`onClick={spy}`) | Wire the callback in the story, record into the DOM or `window.__events`, assert with locators (`references/patterns.md`) | | ||
| | `component.update(<Button … />)` | Separate story per state, or a story with in-page controls that switch state | | ||
| | `component.unmount()` | Story with a toggle that unmounts the subtree; assert after clicking | | ||
| | `hooksConfig` + `beforeMount`/`afterMount` in `playwright/index.ts` | Providers wrapped in the story or a shared decorator helper (`references/react.md` / `vue.md`) | | ||
| | `router` fixture / MSW handlers | `page.route()` in the test, or MSW `setupWorker` started inside the story | | ||
| | `playwright/index.html` (styles, fonts, theme) | `playwright/gallery/index.html` | | ||
| | `ctViteConfig` | The gallery runs through the app's own Vite config; for a standalone gallery server, edit `playwright/vite.config.ts` | | ||
| | `ctPort` | The dev server port in `webServer` + `baseURL` | | ||
| | `ctTemplateDir` | The `playwright/gallery/` directory location | | ||
| | `defineConfig` from `@playwright/experimental-ct-react` | Plain `defineConfig` from `@playwright/test` | | ||
|
|
||
| ## Steps | ||
|
|
||
| 1. Set up the gallery, fixture and config per `SKILL.md` (keep the old CT project running while migrating). | ||
| 2. For each CT spec file, collect the distinct `mount(<…/>)` calls and turn each into a named story export next to the component. Test-specific data stays inline in the story; shared mock data can move to a `fixtures`/`mocks` module imported by stories. | ||
| 3. Rewrite specs: import `test`/`expect` from `playwright/fixtures.ts`, replace `mount(<X …/>)` with `mount('X/StoryName')`. Assertions on the returned locator usually survive unchanged. | ||
| 4. Replace callback-spy assertions (`expect(spy).toHaveBeenCalled…`) with DOM/`window.__events` assertions per `references/patterns.md`. | ||
| 5. Port `beforeMount` hooks into a decorator used by the affected stories. | ||
| 6. Delete the CT project from `playwright.config.ts`, remove `@playwright/experimental-ct-*` from dependencies, delete `playwright/index.html`/`index.ts*` once all specs are migrated. | ||
|
|
||
| ## Before / after | ||
|
|
||
| ```tsx | ||
| // Before (CT) | ||
| import { test, expect } from '@playwright/experimental-ct-react'; | ||
| import { Button } from '../src/components/Button'; | ||
|
|
||
| test('click', async ({ mount }) => { | ||
| const messages: string[] = []; | ||
| const component = await mount(<Button title="Submit" onClick={data => messages.push(data)} />); | ||
| await component.click(); | ||
| expect(messages).toEqual(['hello']); | ||
| }); | ||
| ``` | ||
|
|
||
| ```tsx | ||
| // After: src/components/Button.story.tsx | ||
| export const RecordsClickData = () => { | ||
| const [messages, setMessages] = useState<string[]>([]); | ||
| return <> | ||
| <Button title="Submit" onClick={data => setMessages(m => [...m, data])} /> | ||
| <output data-testid="messages">{messages.join(',')}</output> | ||
| </>; | ||
| }; | ||
| ``` | ||
|
|
||
| ```ts | ||
| // After: tests/components/button.spec.ts | ||
| import { test, expect } from '../../playwright/fixtures'; | ||
|
|
||
| test('click', async ({ mount, page }) => { | ||
| const component = await mount('components/Button/RecordsClickData'); | ||
| await component.getByRole('button').click(); | ||
| await expect(page.getByTestId('messages')).toHaveText('hello'); | ||
| }); | ||
| ``` |
100 changes: 100 additions & 0 deletions
100
packages/skills/component-testing/references/patterns.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| # Testing patterns | ||
|
|
||
| Recipes for common component-testing needs. All examples are React; the Vue equivalents differ only in story syntax. | ||
|
|
||
| ## Callbacks and events | ||
|
|
||
| The test cannot receive a callback directly — record the callback's effect inside the story and assert it through the page. | ||
|
|
||
| **Visible output (preferred)** — renders state the test can assert with normal locators and that a human can see in the gallery: | ||
|
|
||
| ```tsx | ||
| export const CountsClicks = () => { | ||
| const [clicks, setClicks] = useState(0); | ||
| return <> | ||
| <Button title="Submit" onClick={() => setClicks(c => c + 1)} /> | ||
| <output data-testid="click-count">{clicks}</output> | ||
| </>; | ||
| }; | ||
| ``` | ||
|
|
||
| **Event log on window** — for non-visual events or when payloads matter: | ||
|
|
||
| ```tsx | ||
| declare global { interface Window { __events: any[] } } | ||
|
|
||
| export const RecordsSelection = () => { | ||
| window.__events = []; | ||
| return <Select options={mockOptions} onChange={value => window.__events.push({ change: value })} />; | ||
| }; | ||
| ``` | ||
|
|
||
| ```ts | ||
| await component.getByRole('option', { name: 'Two' }).click(); | ||
| await expect.poll(() => page.evaluate(() => window.__events)).toEqual([{ change: 'two' }]); | ||
| ``` | ||
|
|
||
| ## Per-test arguments | ||
|
|
||
| Prefer one story export per scenario. When a scenario is genuinely parametric (e.g. a boundary-value sweep), `mount` can pass JSON-serializable args; the gallery hands them to the story as an `args` prop: | ||
|
|
||
| ```tsx | ||
| export const WithArgs = ({ args }: { args?: { title: string } }) => | ||
| <Button title={args?.title ?? 'Default'} />; | ||
| ``` | ||
|
|
||
| ```ts | ||
| const component = await mount('components/Button/WithArgs', { args: { title: 'Hello' } }); | ||
| ``` | ||
|
|
||
| Args travel through a URL query parameter — keep them small and serializable. | ||
|
|
||
| ## Network mocking | ||
|
|
||
| Standard Playwright routing works — set up routes before mounting: | ||
|
|
||
| ```ts | ||
| test('shows user data', async ({ mount, page }) => { | ||
| await page.route('**/api/user/*', route => route.fulfill({ json: { name: 'Test User' } })); | ||
| const component = await mount('components/UserCard/Default'); | ||
| await expect(component).toContainText('Test User'); | ||
| }); | ||
| ``` | ||
|
|
||
| For teams already invested in MSW, start a worker inside the story (or a decorator) instead: `setupWorker(...handlers).start()` before rendering. `page.route()` is simpler and needs no extra dependency. | ||
|
|
||
| ## Viewport, color scheme, locale, timezone | ||
|
|
||
| Standard Playwright configuration applies per test file or block: | ||
|
|
||
| ```ts | ||
| test.use({ viewport: { width: 400, height: 800 }, colorScheme: 'dark', locale: 'de-DE' }); | ||
| ``` | ||
|
|
||
| Fake time works too: `await page.clock.setFixedTime(new Date('2024-02-02T10:00:00'))` before mounting. | ||
|
|
||
| ## Visual comparison | ||
|
|
||
| ```ts | ||
| const component = await mount('components/Card/Default'); | ||
| await expect(component).toHaveScreenshot('card.png'); | ||
| ``` | ||
|
|
||
| Screenshot the component locator, not the page, to avoid asserting on gallery chrome. | ||
|
|
||
| ## Multiple states in one test | ||
|
|
||
| Each `mount()` navigates fresh, so mounting several stories in one test is cheap and isolated: | ||
|
|
||
| ```ts | ||
| test('button variants', async ({ mount }) => { | ||
| await expect(await mount('Button/Primary')).toHaveScreenshot('primary.png'); | ||
| await expect(await mount('Button/Disabled')).toHaveScreenshot('disabled.png'); | ||
| }); | ||
| ``` | ||
|
|
||
| There is no `update()` — a story that needs to transition between states should render its own controls (a button that flips a prop) and the test clicks them. | ||
|
|
||
| ## Debugging stories | ||
|
|
||
| Open the gallery index in a browser (`/playwright/gallery/index.html`) to browse all stories, or a story URL directly. Render failures show the error and stack in the page and mark `#story-root` with `data-story-error`, which the `mount` fixture converts into a test failure with the same message. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # React setup | ||
|
|
||
| Follow the setup workflow in `SKILL.md`, using `templates/react/`. This page covers React-specific details. | ||
|
|
||
| ## Files | ||
|
|
||
| - `playwright/gallery/index.html` — from `templates/react/index.html`. | ||
| - `playwright/gallery/main.tsx` — from `templates/react/main.tsx`. Requires `react` and `react-dom` 18+ (uses `createRoot`). | ||
| - Stories: `src/**/*.story.tsx` (the glob also picks up `.story.jsx`). | ||
|
|
||
| ## StrictMode | ||
|
|
||
| The gallery wraps stories in `<React.StrictMode>` to match how most apps render. In development builds StrictMode intentionally double-invokes render functions and effects. This matters for stories that record events with counters set in effects — recording via state updates from event handlers (like the `CountsClicks` template story) is unaffected. If a story misbehaves under StrictMode, that is usually a real finding about the component; only remove the wrapper from `main.tsx` if the app itself does not use StrictMode. | ||
|
|
||
| ## Global providers | ||
|
|
||
| If components require context (theme, store, i18n, router), create one shared decorator and use it in stories, so each story states its scenario and nothing more: | ||
|
|
||
| ```tsx | ||
| // src/stories/decorators.tsx | ||
| import { ThemeProvider } from '../theme'; | ||
| import { MemoryRouter } from 'react-router-dom'; | ||
|
|
||
| export function AppScaffold({ children, route = '/' }: { children: React.ReactNode, route?: string }) { | ||
| return ( | ||
| <ThemeProvider theme="light"> | ||
| <MemoryRouter initialEntries={[route]}>{children}</MemoryRouter> | ||
| </ThemeProvider> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ```tsx | ||
| // src/components/ProfilePage.story.tsx | ||
| export const LoggedIn = () => ( | ||
| <AppScaffold route="/profile/42"> | ||
| <ProfilePage user={{ id: 42, name: 'Test User' }} /> | ||
| </AppScaffold> | ||
| ); | ||
| ``` | ||
|
|
||
| Do not build the decorator into `main.tsx` — keeping it in story files makes the wrapping visible and lets stories opt out. | ||
|
|
||
| ## CSS | ||
|
|
||
| - Global stylesheets: import them in `playwright/gallery/main.tsx` (`import '../../src/index.css'`) or link them from `index.html`, mirroring the app's own entry point. | ||
| - Tailwind: importing the app's main CSS file is enough — the gallery runs through the app's Vite config, so the Tailwind plugin processes story files like any other source. If content scanning is path-based, make sure `*.story.tsx` files are covered. | ||
| - CSS modules and component-level imports need no extra setup. | ||
|
|
||
| ## Data fetching | ||
|
|
||
| Components that fetch on mount work fine — mock the network with `page.route()` in the test (see `references/patterns.md`). For libraries with client objects (React Query, Apollo), create the client inside the story or decorator so each navigation starts fresh. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Vue setup | ||
|
|
||
| Follow the setup workflow in `SKILL.md`, using `templates/vue/`. This page covers Vue-specific details. | ||
|
|
||
| ## Files | ||
|
|
||
| - `playwright/gallery/index.html` — from `templates/vue/index.html`. | ||
| - `playwright/gallery/main.ts` — from `templates/vue/main.ts`. Requires Vue 3. | ||
| - Stories: `src/**/*.story.{ts,js,vue}`. | ||
|
|
||
| ## Two ways to write stories | ||
|
|
||
| **Render-function stories** (`Button.story.ts`) — several scenarios per file, one named export each; see `templates/vue/Button.story.ts`. Uses `defineComponent` + `h()`, no SFC compilation involved. | ||
|
|
||
| **Single-file-component stories** (`Button.primary.story.vue`) — one story per file, full template syntax: | ||
|
|
||
| ```vue | ||
| <script setup lang="ts"> | ||
| import { ref } from 'vue'; | ||
| import Button from './Button.vue'; | ||
| const clicks = ref(0); | ||
| </script> | ||
|
|
||
| <template> | ||
| <Button title="Submit" @click="clicks++" /> | ||
| <output data-testid="click-count">{{ clicks }}</output> | ||
| </template> | ||
| ``` | ||
|
|
||
| An SFC story is addressed by its path without the extension: `mount('components/Button.primary')`. Prefer SFC stories when the scenario needs slots or non-trivial templates. | ||
|
|
||
| ## Global plugins | ||
|
|
||
| Apps that rely on plugins (Pinia, vue-router, i18n) should wrap components with a decorator story helper that creates a fresh instance per story: | ||
|
|
||
| ```ts | ||
| // src/stories/decorators.ts | ||
| import { defineComponent, h, type Component } from 'vue'; | ||
| import { createPinia } from 'pinia'; | ||
|
|
||
| export function withStore(story: Component) { | ||
| return defineComponent(() => { | ||
| const pinia = createPinia(); | ||
| return () => h(story, { pinia }); | ||
| }); | ||
| } | ||
| ``` | ||
|
|
||
| For plugins that must be installed on the app instance (`app.use(...)`), add them in `playwright/gallery/main.ts` right after `createApp(host)` — that is the equivalent of the app's own bootstrap. | ||
|
|
||
| ## CSS | ||
|
|
||
| Import global stylesheets in `playwright/gallery/main.ts` (`import '../../src/assets/main.css'`) or link them from `index.html`, mirroring the app's entry point. Scoped styles in `.vue` components need no setup. | ||
|
|
||
| ## Typechecking | ||
|
|
||
| `.story.vue` files typecheck via `vue-tsc` like any SFC. For `.story.ts` files nothing special is needed. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.