Testing in svelte
The following post will get updated with all some common test cases
When scaffolding a svelteapp you choose whether to use the playwright and vitest to be installed additionally
Well they dont work out of the box for more complex test use cases
Things we will need
- @testing-library/svelte
- jsdom
- @sveltejs/vite-plugin-svelte
- @testing-library/jest-dom and its type @types/testing-library__jest-dom
optional if you want a web ui to check the unit tests install @vitest/ui
# Install
pnpm add -D @vitest/ui
# Run test with the following commadnd
vitest --ui
as of this writing 18-07-23 your pacakge.json
should look like this
{
"name": "app",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"test": "npm run test:integration && npm run test:unit",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"test:integration": "playwright test",
"test:unit": "vitest --ui"
},
"devDependencies": {
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.20.4",
"@sveltejs/vite-plugin-svelte": "^2.4.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/svelte": "^4.0.3",
"@testing-library/user-event": "^14.4.3",
"@types/testing-library__jest-dom": "^5.14.8",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@vitest/ui": "^0.33.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.30.0",
"jsdom": "^22.1.0",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.4.2",
"vitest": "^0.32.2"
},
"type": "module"
}
We need the jest-dom for more extendible expect
methods
Now install everything as dev dependency
pnpm add -D @testing-library/svelte jsdom @sveltejs/vite-plugin-svelte
# jest-dom
pnpm add -D @testing-library/jest-dom @types/testing-library__jest-dom
after the installation we need to setup our vitest config
vitest.config.ts
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
globals: true,
environment: 'jsdom'
}
});
Now we test our dependencies and build a simple component
on your src/lib
create a components directory and create a component Button.svelte
<script lang="ts">export let clicked = 0;
</script>
<button on:click={() => (clicked += 1)}>{clicked} {clicked >= 2 ? 'times' : 'time'} clicked</button>
The button is simple it will change its text when it is clicked here are the test that goes with it
on the same directory create a file named button.test.ts
import { render, fireEvent } from '@testing-library/svelte';
import { describe, it } from 'vitest';
// this is required for custom jest matchers like toBeInTheDocument
import '@testing-library/jest-dom';
// importing the component itself
import Button from './Button.svelte';
describe('Button component test', async () => {
it('should render button with text "0 time clicked" ', () => {
const { getByRole, getByText } = render(Button);
// the page should have a button
expect(getByRole('button')).toBeInTheDocument();
// the button should have text "0 time clicked"
expect(getByText('0 time clicked')).toBeInTheDocument();
});
it('should increment correctly with multiple click events', async () => {
const { getByText } = render(Button);
// first click - the button text will change the text to "1 time clicked"
await fireEvent.click(getByText('0 time clicked'));
// second click - the button text will change the text to "2 times clicked"
await fireEvent.click(getByText('1 time clicked'));
// third click - the button text will change the text to "3 times clicked"
await fireEvent.click(getByText('2 times clicked'));
// finally check the final value after clicking 3 times
expect(getByText('3 times clicked')).toBeInTheDocument();
});
});
Now time to run our tests
# This runs both playwright(e2e) and vitest(unit) in our project
pnpm test
User Events
The good thing about testing-library
it has more to offer with their user-event
pnpm add -D @testing-library/user-event
it is basically has more to offer than fireEvent
, We will now also look more into testing svelte components
with props, and will be using the getByTestId
where the elements that we will be testing must have a data-testid
property.
Here is our LoginForm.svelte
<script lang="ts">import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
let email = "";
let password = "";
let isFormSubmitted = false;
$:
isEmailValid = /^w+([.-]?w+)*@w+([.-]?w+)*(.w{2,3})+$/.test(email);
$:
isPasswordValid = password.length <= 8;
function handleSubmit() {
if (isEmailValid && isPasswordValid) {
isFormSubmitted = true;
dispatch("submit", { email, password });
}
}
</script>
<div>
<form data-testid="login-form" on:submit|preventDefault={handleSubmit}>
<label for="email">Email</label>
<input
data-testid="email-input"
type="email"
id="email"
name="email"
placeholder="Email"
required
bind:value={email}
/>
<label for="password">Password</label>
<input
data-testid="password-input"
type="password"
id="password"
name="password"
placeholder="Password"
required
bind:value={password}
/>
<button type="submit" data-testid="login-button" disabled={!isEmailValid || !isPasswordValid}>
Login
</button>
</form>
</div>
<style>
div {
width: 50vw;
/* height: 30vh; */
border: 1px solid black;
display: flex;
flex-direction: column;
padding: 1rem;
margin-top: 2rem;
}
form {
display: flex;
flex-direction: column;
justify-content: space-between;
flex-grow: 1;
}
button {
margin-top: 1rem;
font-size: 1rem;
}
</style>
And our test file loginForm.test.ts
import { render } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
// this is required for custom jest matchers like toBeInTheDocument
import '@testing-library/jest-dom';
// importing the component itself
import LoginForm from './LoginForm.svelte';
const user = userEvent.setup();
describe('LoginForm component test', async () => {
// Rendering everything by getByTestId which looks for data-testid=""
it('Should render form and children with initial conditions', () => {
const { getByTestId } = render(LoginForm);
// Check form if rendered
expect(getByTestId('login-form')).toBeInTheDocument();
// Check both inputs if rendered
expect(getByTestId('email-input')).toBeInTheDocument();
expect(getByTestId('password-input')).toBeInTheDocument();
// Check button if rendered && disabled
expect(getByTestId('login-button')).toBeInTheDocument();
expect(getByTestId('login-button')).toBeDisabled();
});
it('Should enable button after email input', async () => {
const { getByTestId } = render(LoginForm);
// User inputs element at email-input
await user.type(getByTestId('email-input'), 'test@test.com');
// Checks if input has value
expect(getByTestId('email-input')).toHaveValue('test@test.com');
// Login Button Should be enabled
expect(getByTestId('login-button')).toBeEnabled();
});
it('Should require password-input', async () => {
const { getByTestId } = render(LoginForm);
// User input email
await user.type(getByTestId('email-input'), 'test@test.com');
// User clicks Login button
await user.click(getByTestId('login-button'));
// Checks password-input invalid
expect(getByTestId('password-input')).toBeInvalid();
});
it('Should submit form', async () => {
// Initiliaze component with props
const { getByTestId, component } = render(LoginForm, {
props: {
email: 'test@test.com',
password: 'testpassword'
}
});
// Click Submit button
await user.click(getByTestId('login-button'));
// expect isFormSubmitted prop to be true
expect(component.isFormSubmitted === true);
});
});