# Contributing
If you're not sure what to build or how to approach a change, [file an issue](https://github.com/carbon-design-system/carbon-components-svelte/issues) before opening a PR.
## Prerequisites
Codebase:
- [Bun](https://bun.com/docs/installation)
Use Bun as the package manager and task runner. Run package scripts with `bun
```
Avoid `import { ComboBox } from "carbon-components-svelte"` in fixtures. Direct paths skip transforming the entire [`src/index.js`](src/index.js) barrel and are faster under Vitest. The alias in [`vite.config.ts`](vite.config.ts) resolves `carbon-components-svelte` to `src/`.
In `.test.ts` files, import types from the direct path and render via the fixture:
```ts
import type ComboBoxComponent from "carbon-components-svelte/ComboBox/ComboBox.svelte";
import ComboBox from "./ComboBox.test.svelte";
```
Import shared utilities (for example exported helpers) from the component file or [`src/utils/`](src/utils/) as appropriate.
#### Queries and interactions
- Prefer accessible queries: `getByRole`, `getByLabelText`, `getByText` with `exact: true` when needed ([`ComboBox.test.ts`](tests/ComboBox/ComboBox.test.ts)).
- Use `data-testid` in unit tests only when roles are insufficient. E2E fixtures use `data-testid` routinely.
- Use the shared `user` helper from [`tests/utils/user.ts`](tests/utils/user.ts) for clicks and keyboard input.
- Type fixture props with `ComponentProps` from `svelte` where helpful.
#### What to test
Prioritize high-value coverage:
- Default render and primary user interactions (open, select, submit, keyboard)
- Accessibility roles, labels, and ARIA state changes
- Regressions for bugs you fix
- Boundary inputs to pure utils: zero or negative sizes, empty collections, divide-by-zero, and out-of-range indices. Assert the defined fallback (clamp, empty, identity) rather than only that it doesn't throw.
- Generic/type contracts when adding `@template` props (see below)
Skip or avoid:
- Tests that mirror implementation details without asserting user-visible behavior
- Redundant permutations of the same code path
- Large fixture setups when a focused unit test on a util suffices
Add tests proportional to the change. Not every prop variant needs its own case.
#### Generics and type tests
Vitest exposes `expectTypeOf` globally (see [`tests/utils/setup-globals.ts`](tests/utils/setup-globals.ts)). Use it in a `describe("Generics", …)` block to verify `@template` JSDoc flows through to consumer types. Pure type assertions do not require a runtime render.
Pattern from [`Button.test.ts`](tests/Button/Button.test.ts) and [`ComboBox.test.ts`](tests/ComboBox/ComboBox.test.ts):
```ts
import type ComboBoxComponent from "carbon-components-svelte/ComboBox/ComboBox.svelte";
import type { ComponentProps } from "svelte";
describe("Generics", () => {
it("should support custom item types with generics", () => {
type Product = { id: string; text: string; price: number };
type ComponentType = ComboBoxComponent;
type Props = ComponentProps;
expectTypeOf().toEqualTypeOf();
const itemToString = (item: Product) => item.text;
expectTypeOf(itemToString).parameter(0).toEqualTypeOf();
});
});
```
Use `ComponentProps` and `ComponentEvents` from `svelte` for props and event payloads. For runtime smoke tests with custom item shapes, add a `*Generics.test.svelte` fixture ([`ComboBoxGenerics.test.svelte`](tests/ComboBox/ComboBoxGenerics.test.svelte)).
Common `expectTypeOf` matchers: `.toEqualTypeOf`, `.toExtend`, `.parameter(n)`, `.returns`, `.toHaveProperty`.
#### Other patterns
- `beforeEach(() => vi.clearAllMocks())` when tests use spies.
- Scope runs: `bun run test ComboBox` (see [Checks](#checks)).
#### Svelte 3 and Svelte 4 compatibility tests
The default `bun run test` harness uses Svelte 5. Separate workspaces under `tests-svelte3/` and `tests-svelte4/` run the same tests against older Svelte versions.
You only need these when fixing a failure reported from `bun run test:svelte3`, `bun run test:svelte4`, or their type-check scripts. Most changes do not require them.
Before running a compatibility script, install that workspace's dependencies. Without a local `node_modules`, the run may fall back to the Svelte 5 harness:
```sh
cd tests-svelte3 && bun install
cd ../tests-svelte4 && bun install
```
Then from the repo root:
```sh
bun run test:svelte3
bun run test:svelte4
```
### E2E testing with Playwright
E2E tests run in a real browser against HTML fixtures served by Vite. The Playwright config is in `playwright.config.ts`; the Vite config for fixtures is in `e2e/vite.config.ts`.
Run the full suite:
```sh
bun run test:e2e
```
Run a focused component or pattern:
```sh
# Single test file
bunx playwright test e2e/breakpoint.test.ts
# Tests matching a grep pattern (for example "Breakpoint" or "sm breakpoint")
bunx playwright test --grep "Breakpoint"
```
#### How fixtures are served (local vs CI)
The fixtures are a Vite multi-page app, one `.html` entry per fixture. How they are served depends on the environment, controlled by the `CI` env var in `playwright.config.ts`:
- **Locally**: Playwright starts the Vite **dev server**. It transforms modules on demand, so there is no build step and edits show up immediately.
- **In CI**: Playwright runs `vite build` once and serves the static output with `vite preview`. Bundled assets load faster and with less run-to-run variance than the dev server, which transforms unbundled modules on demand through a single process that competes with the browser workers for CPU.
Neither path needs a manual build. To reproduce the CI build locally, for example to debug a build-only failure, build the fixtures once:
```sh
bunx vite build --config e2e/vite.config.ts
```
The output lands in `e2e/fixtures/dist/` (gitignored). Serve it with `bunx vite preview --config e2e/vite.config.ts`, or run the whole CI path in one command:
```sh
CI=true bunx playwright test --project=chromium
```
To add a new E2E test, copy the Breakpoint example. Create these files:
| File | Purpose |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `e2e/fixtures/MyComponentFixture.svelte` | Svelte component that renders the component under test. Use `data-testid` on elements you want to query. |
| `e2e/fixtures/my-component.ts` | Entry script that mounts the fixture into `#app`. Use the shared `mount()` utility from `./mount`. |
| `e2e/fixtures/my-component.html` | HTML page with `` and a script tag loading the entry module. |
| `e2e/my-component.test.ts` | Playwright tests. Use `page.goto("/my-component.html")` in `beforeEach`, then `page.getByTestId(...)` to assert. |
Example fixture mount (`my-component.ts`):
```ts
import MyComponentFixture from "./MyComponentFixture.svelte";
import { mount } from "./mount";
mount(MyComponentFixture);
```
A few gotchas:
For components that hide content with CSS (for example ComposedModal), `toBeVisible()` can lie because the element stays in the DOM. Prefer `toHaveClass(/is-visible/)` on the container, or assert on `aria-hidden` / `inert`.
`getByText("Row 1")` also matches "Row 10", "Row 11", and so on. Use `getByRole("cell", { name: "Row 1", exact: true })` or a more specific selector when content can overlap.
Add a link to `e2e/fixtures/index.html` so the fixture is reachable from the index page.
## Commit messages
Use [Conventional Commits](https://www.conventionalcommits.org/):
```
():
[optional body]
```
Subject line:
- Keep it concise: one line, imperative mood
- Common types: `fix`, `feat`, `docs`, `chore`, `test`, `refactor`
- Scope is the component or area; multi-word names are lowercase with dashes: `combo-box`, `code-snippet`, `data-table`, `ui-shell`, `accordion-item`
- Omit scope when the change spans many areas (`docs: …`, `chore: …`)
- Append `!` after the scope for breaking changes: `fix(accordion-item)!: …`
Examples:
```
fix(combo-box): close dropdown on outside click
docs(tree-view): fix slottable inline editing example
chore(deps-dev): bump vitest, svelte-check
```
Body (optional):
A body is not required. Add one when context helps reviewers or when closing an issue.
- Reference the issue: `Fixes #1000` or `Closes #1000` (GitHub auto-closes on merge)
- At most 2-3 sentences, full sentences, no bullet lists
- Wrap lines at 72 characters for readability in `git log`
Example with body:
```
fix(data-table): associate cells with column headers
Cells now set aria-labelledby to their column header id.
Fixes #3162
```
Avoid vague subjects (`fix bug`), PascalCase or camelCase scopes (`fix(ComboBox):`), and long subjects. Move detail to the body. Prefer `Fixes #N` in the body over PR numbers in the subject.
## Submit a pull request
Follow [Commit messages](#commit-messages) for each commit in your branch.
### Sync your fork
Before you open a PR, sync your fork with upstream:
```sh
git fetch upstream
git checkout master
git merge upstream/master
```
### Open a PR
Push your branch, then open a PR comparing `/feature` to `origin/master`.
## Maintainer guide
The following applies only to project maintainers.
### Release
This library publishes to NPM with [provenance](https://docs.npmjs.com/generating-provenance-statements) via a [GitHub workflow](https://github.com/carbon-design-system/carbon-components-svelte/blob/master/.github/workflows/release.yml).
Pushing a tag that starts with `v` (for example `v0.81.1`) triggers the workflow. It runs `bun ci`, `bun build:docs`, and `bunx culls --preserve=svelte` before publishing to NPM.
Maintainers still do a few things locally before tagging.
On a clean `master` branch, run `bun run release`. That will:
- Bump the semantic version in `package.json`
- Generate notes in `CHANGELOG.md`
- Run `bun run build:docs` to update generated documentation
It does not commit or tag. Do that manually:
```sh
# 1. Commit the changes using the new version as the commit message.
git commit -am "v0.81.1"
# 2. Create a tag.
git tag v0.81.1
# 3. Push the tag to the remote.
# This triggers the `release.yml` workflow to publish a new package to NPM (with provenance).
git push origin v0.81.1
```
If the workflow succeeds, the [`release.yml` workflow](https://github.com/carbon-design-system/carbon-components-svelte/actions/workflows/release.yml) publishes the new version to NPM.
### Post-release checklist
After the release is on NPM:
1. Create a [new release](https://github.com/carbon-design-system/carbon-components-svelte/releases/new) on GitHub. Click "Generate release notes" to list changes by commit with PR and author metadata. Drop notes that do not matter for the release (for example CI-only changes).
2. Mark it as the latest release.
3. On each related PR or issue, confirm the fix shipped. Future readers will want to know which version picked it up.
```md
Released in [v0.81.1](https://github.com/carbon-design-system/carbon-components-svelte/releases/tag/v0.81.1).
```