diff --git a/package.json b/package.json index ab084e1..eabdd17 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,15 @@ "version": "0.0.0", "scripts": { "dev": "vite dev", - "build": "vite build && npm run package", + "build": "vite build && pnpm run package", "preview": "vite preview", "package": "svelte-kit sync && svelte-package && publint", - "prepublishOnly": "npm run package", - "test": "npm run test:integration && npm run test:unit", + "prepublishOnly": "pnpm run package", + "test": "vitest", "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", "release": "changeset publish", "changeset": "changeset" }, @@ -61,6 +59,7 @@ "types": "./dist/index.d.ts", "type": "module", "dependencies": { + "matchers": "link:@testing-library/jest-dom/matchers", "svelte-persisted-store": "^0.7.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d73cd35..f318f70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + matchers: + specifier: link:@testing-library/jest-dom/matchers + version: link:@testing-library/jest-dom/matchers svelte-persisted-store: specifier: ^0.7.0 version: 0.7.0(svelte@4.0.5) diff --git a/scripts/setupTest.ts b/scripts/setupTest.ts new file mode 100644 index 0000000..3e00e0f --- /dev/null +++ b/scripts/setupTest.ts @@ -0,0 +1,92 @@ +// setupTest.ts +/* eslint-disable @typescript-eslint/no-empty-function */ +import { vi } from 'vitest'; +import type { Navigation, Page } from '@sveltejs/kit'; +import { readable } from 'svelte/store'; +import * as environment from '$app/environment'; +import * as navigation from '$app/navigation'; +import * as stores from '$app/stores'; +import { configure } from '@testing-library/dom'; + +configure({ + asyncUtilTimeout: 1500 +}); + +// Mock SvelteKit runtime module $app/environment +vi.mock('$app/environment', (): typeof environment => ({ + browser: false, + dev: true, + building: false, + version: 'any' +})); + +// Mock SvelteKit runtime module $app/navigation +vi.mock('$app/navigation', (): typeof navigation => ({ + afterNavigate: () => {}, + beforeNavigate: () => {}, + disableScrollHandling: () => {}, + goto: () => Promise.resolve(), + invalidate: () => Promise.resolve(), + invalidateAll: () => Promise.resolve(), + preloadData: () => Promise.resolve(), + preloadCode: () => Promise.resolve() +})); + +// Mock SvelteKit runtime module $app/stores +vi.mock('$app/stores', (): typeof stores => { + const getStores: typeof stores.getStores = () => { + const navigating = readable(null); + const page = readable({ + url: new URL('http://localhost'), + params: {}, + route: { + id: null + }, + status: 200, + error: null, + data: {}, + form: undefined + }); + const updated = { subscribe: readable(false).subscribe, check: async () => false }; + + return { navigating, page, updated }; + }; + + const page: typeof stores.page = { + subscribe(fn) { + return getStores().page.subscribe(fn); + } + }; + const navigating: typeof stores.navigating = { + subscribe(fn) { + return getStores().navigating.subscribe(fn); + } + }; + const updated: typeof stores.updated = { + subscribe(fn) { + return getStores().updated.subscribe(fn); + }, + check: async () => false + }; + + return { + getStores, + navigating, + page, + updated + }; +}); + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn() + })) +}); diff --git a/src/tests/Mode.svelte b/src/tests/Mode.svelte new file mode 100644 index 0000000..c508db0 --- /dev/null +++ b/src/tests/Mode.svelte @@ -0,0 +1,9 @@ + + + +{$mode} + + + diff --git a/src/tests/mode.spec.ts b/src/tests/mode.spec.ts new file mode 100644 index 0000000..264025e --- /dev/null +++ b/src/tests/mode.spec.ts @@ -0,0 +1,70 @@ +import { render } from '@testing-library/svelte'; +import { expect, it } from 'vitest'; +import Mode from './Mode.svelte'; +import userEvent from '@testing-library/user-event'; + +it('renders mode', async () => { + const { container } = render(Mode); + const rootEl = container.parentElement; + const classes = getClasses(rootEl); + expect(classes).toContain('dark'); +}); + +it('toggles the mode', async () => { + const { container, getByTestId } = render(Mode); + const rootEl = container.parentElement; + const classes = getClasses(rootEl); + expect(classes).toContain('dark'); + const toggle = getByTestId('toggle'); + await userEvent.click(toggle); + const classes2 = getClasses(rootEl); + expect(classes2).not.toContain('dark'); + await userEvent.click(toggle); + const classes3 = getClasses(rootEl); + expect(classes3).toContain('dark'); +}); + +it('allows the user to set the mode', async () => { + const { container, getByTestId } = render(Mode); + const rootEl = container.parentElement; + const classes = getClasses(rootEl); + expect(classes).toContain('dark'); + const light = getByTestId('light'); + await userEvent.click(light); + const classes2 = getClasses(rootEl); + expect(classes2).not.toContain('dark'); + + const dark = getByTestId('dark'); + await userEvent.click(dark); + const classes3 = getClasses(rootEl); + expect(classes3).toContain('dark'); +}); + +it('keeps the mode store in sync with current mode', async () => { + const { container, getByTestId } = render(Mode); + const rootEl = container.parentElement; + const light = getByTestId('light'); + const dark = getByTestId('dark'); + const mode = getByTestId('mode'); + const classes = getClasses(rootEl); + expect(classes).toContain('dark'); + expect(mode.textContent).toBe('dark'); + + await userEvent.click(light); + const classes2 = getClasses(rootEl); + expect(classes2).not.toContain('dark'); + expect(mode.textContent).toBe('light'); + + await userEvent.click(dark); + const classes3 = getClasses(rootEl); + expect(classes3).toContain('dark'); + expect(mode.textContent).toBe('dark'); +}); + +function getClasses(element: HTMLElement | null): string[] { + if (element === null) { + return []; + } + const classes = element.className.split(' ').filter((c) => c.length > 0); + return classes; +} diff --git a/tests/test.ts b/tests/test.ts deleted file mode 100644 index 5816be4..0000000 --- a/tests/test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('index page has expected h1', async ({ page }) => { - await page.goto('/'); - await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible(); -}); diff --git a/vite.config.ts b/vite.config.ts index 37b6a84..d44282c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,18 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ plugins: [sveltekit()], test: { - include: ['src/**/*.{test,spec}.{js,ts}'] + include: ['src/**/*.{test,spec}.{js,ts}'], + // jest like globals + globals: true, + environment: 'jsdom', + // in-source testing + includeSource: ['src/**/*.{js,ts,svelte}'], + // Add @testing-library/jest-dom matchers & mocks of SvelteKit modules + setupFiles: ['./scripts/setupTest.ts'], + // Exclude files in v8 + coverage: { + exclude: ['setupTest.ts'] + }, + alias: [{ find: /^svelte$/, replacement: 'svelte/internal' }] } });