svelte-declarative-testing 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,35 +1,46 @@
1
1
  <script>
2
+ import { fireEvent } from '@testing-library/svelte';
2
3
  import { Test, Describe, Check } from '../../src/components/testing-library';
3
4
  </script>
4
5
 
5
- <Describe label="Basic test">
6
- {#snippet tests()}
7
- <Test it="finds the rendered component">
8
- <button>Click me</button>
6
+ <Test it="finds the rendered component">
7
+ {#snippet mount()}
8
+ <button>Click me</button>
9
+ {/snippet}
9
10
 
10
- {#snippet checks()}
11
- <Check
12
- fn={({ getByRole }) => {
13
- expect(getByRole('button', { name: 'Click me' })).not.toBe(null);
14
- }}
15
- />
16
- {/snippet}
17
- </Test>
11
+ <Check
12
+ fn={({ getByRole }) => {
13
+ expect(getByRole('button', { name: 'Click me' })).not.toBe(null);
14
+ }}
15
+ />
16
+ </Test>
18
17
 
19
- <Describe label="Nested describe">
20
- {#snippet tests()}
21
- <Test it="also passes">
22
- <button>Click me</button>
18
+ <Describe label="Basic test suite">
19
+ {#snippet mount()}
20
+ <button>Click me</button>
21
+ {/snippet}
23
22
 
24
- {#snippet checks()}
25
- <Check
26
- fn={({ getByRole }) => {
27
- expect(getByRole('button', { name: 'Click me' })).not.toBe(null);
28
- }}
29
- />
30
- {/snippet}
31
- </Test>
23
+ <Describe label="Nested describe">
24
+ <Test it="mounts the test's snippet instead of the describe's snippet">
25
+ {#snippet mount()}
26
+ <button>No, click me</button>
32
27
  {/snippet}
33
- </Describe>
34
- {/snippet}
28
+
29
+ <Check
30
+ fn={({ queryByRole }) => {
31
+ expect(queryByRole('button', { name: 'Click me' })).toBe(null);
32
+ expect(queryByRole('button', { name: 'No, click me' })).not.toBe(null);
33
+ }}
34
+ />
35
+ </Test>
36
+
37
+ <Test it="mounts the describe's snippet if the test doesn't have its own snippet">
38
+ <Check
39
+ fn={({ queryByRole }) => {
40
+ expect(queryByRole('button', { name: 'Click me' })).not.toBe(null);
41
+ expect(queryByRole('button', { name: 'No, click me' })).toBe(null);
42
+ }}
43
+ />
44
+ </Test>
45
+ </Describe>
35
46
  </Describe>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-declarative-testing",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A way to mount your Svelte test components declaratively",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  /**@import { CheckProps } from './' */
3
- import { getAddCheck } from './context.js';
3
+ import { getAddCheck } from './context';
4
4
 
5
5
  /**@type {CheckProps} */
6
6
  const { fn } = $props();
@@ -1,22 +1,27 @@
1
1
  <script>
2
2
  /** @import { DescribeProps } from './' */
3
3
  import { describe } from 'vitest';
4
- import { setAddTest, setSuiteRenderSnippet } from './context.js';
4
+ import { getAddDescribeChild, setAddDescribeChild, setSuiteRenderSnippet } from './context';
5
+ import tryFn from '../../utils/tryFn.js';
5
6
 
6
7
  /**@type {DescribeProps}*/
7
- const { label, todo, only, skip, skipIf, runIf, children, tests } = $props();
8
+ const { label, todo, only, skip, skipIf, runIf, children, mount } = $props();
8
9
 
9
- /**@type {((result: unknown) => void | Promise<void>)[]} */
10
- const testFns = [];
10
+ /**@type {(() => void | Promise<void>)[]} */
11
+ const childFns = [];
11
12
 
12
- setAddTest((fn) => {
13
- testFns.push(fn);
13
+ const addDescribeToParent = tryFn(getAddDescribeChild);
14
+
15
+ setAddDescribeChild((fn) => {
16
+ childFns.push(fn);
14
17
  });
15
18
 
16
19
  // svelte-ignore state_referenced_locally
17
- setSuiteRenderSnippet(children);
20
+ if (mount) {
21
+ setSuiteRenderSnippet(mount);
22
+ }
18
23
 
19
- $effect(() => {
24
+ const setupDescribe = () => {
20
25
  const describeFn = () => {
21
26
  if (skip) return describe.skip;
22
27
  if (skipIf) return describe.skipIf(skipIf());
@@ -27,11 +32,17 @@
27
32
  };
28
33
 
29
34
  describeFn()(label, async () => {
30
- for (const test of testFns) {
31
- await test();
35
+ for (const child of childFns) {
36
+ await child();
32
37
  }
33
38
  });
39
+ };
40
+
41
+ addDescribeToParent?.(setupDescribe);
42
+
43
+ $effect(() => {
44
+ if (!addDescribeToParent) setupDescribe();
34
45
  });
35
46
  </script>
36
47
 
37
- {@render tests()}
48
+ {@render children()}
@@ -1,18 +1,18 @@
1
1
  <script>
2
2
  /**@import { TestProps } from './index.js' */
3
3
  import { test } from 'vitest';
4
- import { getAddTest, setAddCheck, getSuiteRenderSnippet } from './context.js';
4
+ import { setAddCheck, getSuiteRenderSnippet, getAddDescribeChild } from './context';
5
5
  import Wrapper from './Wrapper.svelte';
6
+ import tryFn from '../../utils/tryFn.js';
6
7
 
7
8
  /**@type {TestProps} */
8
- const { it, fails, todo, only, skip, skipIf, runIf, children, checks, render } = $props();
9
+ const { it, fails, todo, only, skip, skipIf, runIf, children, mount, render } = $props();
9
10
 
10
11
  /**@type {((result: unknown) => void | Promise<void>)[]} */
11
12
  const checkFns = [];
12
13
 
13
- const addTest = getAddTest();
14
-
15
- const suiteRenderSnippet = getSuiteRenderSnippet();
14
+ const addTest = tryFn(getAddDescribeChild);
15
+ const suiteRenderSnippet = tryFn(getSuiteRenderSnippet);
16
16
 
17
17
  setAddCheck((fn) => {
18
18
  checkFns.push(fn);
@@ -30,7 +30,7 @@
30
30
  };
31
31
 
32
32
  testFn()(it, async () => {
33
- const result = await render(Wrapper, { children: children ?? suiteRenderSnippet });
33
+ const result = await render(Wrapper, { children: mount ?? suiteRenderSnippet });
34
34
 
35
35
  try {
36
36
  if (!checkFns.length) {
@@ -53,4 +53,4 @@
53
53
  });
54
54
  </script>
55
55
 
56
- {@render checks()}
56
+ {@render children()}
@@ -1,8 +1,8 @@
1
1
  <script>
2
- /**@import type { Snippet } from 'svelte';*/
2
+ /**@import { Snippet } from 'svelte';*/
3
3
 
4
4
  /**@type {{ children: Snippet }}*/
5
5
  const { children } = $props();
6
6
  </script>
7
7
 
8
- {@render children()}
8
+ {@render children?.()}
@@ -0,0 +1,14 @@
1
+ /**@import { Snippet } from 'svelte' */
2
+ /**@typedef {(fn: () => void} AddTestOrDescribeFn */
3
+ /**@typedef {(fn: (renderResult: unknown) => void | Promise<void>} AddCheckFn */
4
+
5
+ import { createContext } from 'svelte';
6
+
7
+ /**@type {[() => AddTestOrDescribeFn | undefined, (fn: AddTestOrDescribeFn) => void]} */
8
+ export const [getAddDescribeChild, setAddDescribeChild] = createContext(null);
9
+
10
+ /**@type {[() => AddCheckFn | undefined, (fn: AddCheckFn) => void]} */
11
+ export const [getAddCheck, setAddCheck] = createContext();
12
+
13
+ /**@type {[() => Snippet<[]> | undefined, (snippet: Snippet<[]> | undefined) => void]} */
14
+ export const [getSuiteRenderSnippet, setSuiteRenderSnippet] = createContext();
@@ -10,28 +10,31 @@ export type ModifierProps = XOR<
10
10
  { runIf: () => unknown }
11
11
  >;
12
12
 
13
- export type DescribeProps =
14
- | {
15
- label: string;
16
- children?: Snippet;
17
- tests: Snippet;
18
- }
19
- | ModifierProps;
13
+ export type DescribeProps = {
14
+ label: string;
15
+ children: Snippet;
16
+ mount?: Snippet;
17
+ } & ModifierProps;
20
18
 
21
- export type BaseTestProps =
22
- | {
23
- it: string;
24
- children?: Snippet;
25
- checks: Snippet;
26
- }
27
- | XOR<ModifierProps, { fails: boolean }>;
19
+ export type BaseTestProps = {
20
+ it: string;
21
+ children: Snippet;
22
+ mount?: Snippet;
23
+ } & XOR<ModifierProps, { fails: boolean }>;
24
+
25
+ type RenderResult = {
26
+ unmount: () => void | Promise<void>;
27
+ };
28
28
 
29
29
  export type TestProps = BaseTestProps & {
30
- render: (result: unknown) => void | Promise<void>;
30
+ render: (
31
+ component: Component<any, any>, //eslint-disable-line @typescript-eslint/no-explicit-any
32
+ options: unknown,
33
+ ) => RenderResult | Promise<RenderResult>;
31
34
  };
32
35
 
33
36
  export type CheckProps = {
34
- fn: (result: unknown) => void | Promise<void>;
37
+ fn: (renderResult: any) => void | Promise<void>; //eslint-disable-line @typescript-eslint/no-explicit-any
35
38
  };
36
39
 
37
40
  export declare const Describe: Component<DescribeProps>;
@@ -0,0 +1,37 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`vitest post plugin > produces code that mounts the component and returns a render result 1`] = `"export default function AComponent() {};import { mount } from "svelte"; mount((await import(import.meta.url)).default, { target: document.body });"`;
4
+
5
+ exports[`vitest pre plugin > produces code with a dummy test suite 1`] = `
6
+ "
7
+ <script>
8
+ import { Describe, Test, Check } from '../components/core';
9
+ </script>
10
+ <Describe label="A test suite">
11
+ <Test it="A test case">
12
+ <Check fn={() => {}} />
13
+ </Test>
14
+ <Describe label="A nested test suite">
15
+ <Test it="A nested test case">
16
+ <Check fn={() => {}} />
17
+ </Test>
18
+ </Describe>
19
+ </Describe>
20
+ <Describe label="Another test suite">
21
+ <Test it="Another test case">
22
+ <Check fn={() => {}} />
23
+ </Test>
24
+ </Describe>
25
+
26
+ {#if globalThis[Symbol()]}{function () {
27
+ describe("A test suite", () => {
28
+ test("A test case", () => {})
29
+ describe("A nested test suite", () => {
30
+ test("A nested test case", () => {})
31
+ })
32
+ })
33
+ describe("Another test suite", () => {
34
+ test("Another test case", () => {})
35
+ })
36
+ }}{/if}"
37
+ `;
@@ -18,13 +18,13 @@ import { SourceMapGenerator, SourceMapConsumer } from 'source-map';
18
18
  */
19
19
 
20
20
  const mountCode =
21
- 'import { mount } from "svelte"; mount((await import(import.meta.url)).default, { target: document.body });';
21
+ ';import { mount } from "svelte"; mount((await import(import.meta.url)).default, { target: document.body });';
22
22
 
23
23
  const testFileRegex = /\.(?:test|spec)\.svelte$/;
24
24
  const getNameFromAttr = (node, attr) =>
25
- node.attributes.find((a) => a.name === attr)?.value?.[0]?.data ?? '(unnamed test)';
25
+ node.attributes.find((a) => a.name === attr)?.value?.[0]?.data ?? '(unnamed)';
26
26
 
27
- const pre = /**@returns {Plugin}*/ () => ({
27
+ const pre = () => ({
28
28
  name: 'transform-svelte-declarative-test',
29
29
  filter: {
30
30
  id: testFileRegex,
@@ -110,7 +110,7 @@ const pre = /**@returns {Plugin}*/ () => ({
110
110
  },
111
111
  });
112
112
 
113
- const post = /**@returns {Plugin}*/ () => ({
113
+ const post = () => ({
114
114
  name: 'transform-svelte-declarative-test',
115
115
  filter: {
116
116
  id: testFileRegex,
@@ -131,6 +131,7 @@ const post = /**@returns {Plugin}*/ () => ({
131
131
  },
132
132
  });
133
133
 
134
+ /**@returns {import('vitest/config').Plugin[]} */
134
135
  export default function getPlugins() {
135
136
  return [pre(), post()];
136
137
  }
@@ -1,3 +1,229 @@
1
+ import { SourceMapConsumer } from 'source-map';
2
+ import getPlugins from './vitest.js';
3
+
4
+ const dummyTestFile = `
5
+ <script>
6
+ import { Describe, Test, Check } from '../components/core';
7
+ </script>
8
+ <Describe label="A test suite">
9
+ <Test it="A test case">
10
+ <Check fn={() => {}} />
11
+ </Test>
12
+ <Describe label="A nested test suite">
13
+ <Test it="A nested test case">
14
+ <Check fn={() => {}} />
15
+ </Test>
16
+ </Describe>
17
+ </Describe>
18
+ <Describe label="Another test suite">
19
+ <Test it="Another test case">
20
+ <Check fn={() => {}} />
21
+ </Test>
22
+ </Describe>
23
+ `;
24
+
25
+ const findLineAndColumn = (code: string, substring: string) => {
26
+ const index = code.indexOf(substring);
27
+ if (index === -1) {
28
+ throw new Error(`Substring "${substring}" not found in code.`);
29
+ }
30
+
31
+ const lines = code.slice(0, index).split('\n');
32
+ const line = lines.length;
33
+ const column = lines[lines.length - 1].length + 1;
34
+
35
+ return { line, column };
36
+ };
37
+
1
38
  describe('vitest pre plugin', () => {
2
- test.fails('transforms test files to mount the component', () => {});
39
+ it('does not transform non-test files', async () => {
40
+ const [pre] = getPlugins();
41
+
42
+ expect(await pre.transform(`<h1>Not a test file</h1>`, 'AComponent.svelte')).toBeUndefined();
43
+ });
44
+
45
+ it('transforms .test.svelte files', async () => {
46
+ const [pre] = getPlugins();
47
+
48
+ expect(await pre.transform(dummyTestFile, 'AComponent.test.svelte')).not.toBeUndefined();
49
+ });
50
+
51
+ it('transforms .spec.svelte files', async () => {
52
+ const [pre] = getPlugins();
53
+
54
+ expect(await pre.transform(dummyTestFile, 'AComponent.spec.svelte')).not.toBeUndefined();
55
+ });
56
+
57
+ it('produces code with a dummy test suite', async () => {
58
+ const [pre] = getPlugins();
59
+ const result = await pre.transform(dummyTestFile, 'AComponent.test.svelte');
60
+
61
+ expect(result?.code).toMatchSnapshot();
62
+ });
63
+
64
+ it('produces a source map with mappings that point to the Describe and Test components', async () => {
65
+ const [pre] = getPlugins();
66
+ const result = (await pre.transform(dummyTestFile, 'AComponent.test.svelte')) as {
67
+ code: string;
68
+ map: string;
69
+ };
70
+
71
+ const map = await new SourceMapConsumer(result?.map);
72
+ const describePosition = findLineAndColumn(result.code, 'describe("A test suite"');
73
+ const testPosition = findLineAndColumn(result.code, 'test("A test case"');
74
+ const nestedDescribePosition = findLineAndColumn(result.code, 'describe("A nested test suite"');
75
+ const nestedTestPosition = findLineAndColumn(result.code, 'test("A nested test case"');
76
+ const anotherDescribePosition = findLineAndColumn(result.code, 'describe("Another test suite"');
77
+ const anotherTestPosition = findLineAndColumn(result.code, 'test("Another test case"');
78
+
79
+ expect(map.originalPositionFor(describePosition)).toEqual(
80
+ expect.objectContaining({
81
+ line: 5,
82
+ column: 1,
83
+ }),
84
+ );
85
+ expect(map.originalPositionFor(testPosition)).toEqual(
86
+ expect.objectContaining({
87
+ line: 6,
88
+ column: 3,
89
+ }),
90
+ );
91
+ expect(map.originalPositionFor(nestedDescribePosition)).toEqual(
92
+ expect.objectContaining({
93
+ line: 9,
94
+ column: 3,
95
+ }),
96
+ );
97
+ expect(map.originalPositionFor(nestedTestPosition)).toEqual(
98
+ expect.objectContaining({
99
+ line: 10,
100
+ column: 5,
101
+ }),
102
+ );
103
+ expect(map.originalPositionFor(anotherDescribePosition)).toEqual(
104
+ expect.objectContaining({
105
+ line: 15,
106
+ column: 1,
107
+ }),
108
+ );
109
+ expect(map.originalPositionFor(anotherTestPosition)).toEqual(
110
+ expect.objectContaining({
111
+ line: 16,
112
+ column: 3,
113
+ }),
114
+ );
115
+ });
116
+
117
+ it('produces (unnamed) suite names for Describe', async () => {
118
+ const [pre] = getPlugins();
119
+ const result = (await pre.transform(
120
+ `
121
+ <Describe>
122
+ <Test it="A test case">
123
+ <Check fn={() => {}} />
124
+ </Test>
125
+ </Describe>
126
+ `,
127
+ 'AComponent.test.svelte',
128
+ )) as { code: string };
129
+
130
+ expect(result.code).toContain('describe("(unnamed)"');
131
+ });
132
+
133
+ it('produces (unnamed) test names for Test', async () => {
134
+ const [pre] = getPlugins();
135
+ const result = (await pre.transform(
136
+ `
137
+ <Describe label="A test suite">
138
+ <Test>
139
+ <Check fn={() => {}} />
140
+ </Test>
141
+ </Describe>
142
+ `,
143
+ 'AComponent.test.svelte',
144
+ )) as { code: string };
145
+
146
+ expect(result.code).toContain('test("(unnamed)"');
147
+ });
148
+
149
+ it('produces (unnamed) suite names when Describe has dynamic label', async () => {
150
+ const [pre] = getPlugins();
151
+ const result = (await pre.transform(
152
+ `
153
+ <Describe label={someVariable}>
154
+ <Test it="A test case">
155
+ <Check fn={() => {}} />
156
+ </Test>
157
+ </Describe>
158
+ `,
159
+ 'AComponent.test.svelte',
160
+ )) as { code: string };
161
+
162
+ expect(result.code).toContain('describe("(unnamed)"');
163
+ });
164
+
165
+ it('produces (unnamed) test names when Test has dynamic name', async () => {
166
+ const [pre] = getPlugins();
167
+ const result = (await pre.transform(
168
+ `
169
+ <Describe label="A test suite">
170
+ <Test it={someVariable}>
171
+ <Check fn={() => {}} />
172
+ </Test>
173
+ </Describe>
174
+ `,
175
+ 'AComponent.test.svelte',
176
+ )) as { code: string };
177
+
178
+ expect(result.code).toContain('test("(unnamed)"');
179
+ });
180
+
181
+ it('does not produce descibe or test calls when no Describe or Test components are present', async () => {
182
+ const [pre] = getPlugins();
183
+ const result = (await pre.transform(
184
+ `
185
+ <Header>Not a test file</Header>
186
+ `,
187
+ 'AComponent.test.svelte',
188
+ )) as { code: string };
189
+
190
+ expect(result.code).not.toContain('describe(');
191
+ expect(result.code).not.toContain('test(');
192
+ });
193
+ });
194
+
195
+ describe('vitest post plugin', () => {
196
+ it('does not transform non-test files', async () => {
197
+ const [, post] = getPlugins();
198
+
199
+ expect(
200
+ await post.transform(`export default function AComponent() {}`, 'AComponent.svelte'),
201
+ ).toBeUndefined();
202
+ });
203
+
204
+ it('transforms .test.svelte files to mount the component', async () => {
205
+ const [, post] = getPlugins();
206
+
207
+ expect(
208
+ await post.transform(`export default function AComponent() {}`, 'AComponent.test.svelte'),
209
+ ).not.toBeUndefined();
210
+ });
211
+
212
+ it('transforms .spec.svelte files to mount the component', async () => {
213
+ const [, post] = getPlugins();
214
+
215
+ expect(
216
+ await post.transform(`export default function AComponent() {}`, 'AComponent.spec.svelte'),
217
+ ).not.toBeUndefined();
218
+ });
219
+
220
+ it('produces code that mounts the component and returns a render result', async () => {
221
+ const [, post] = getPlugins();
222
+ const result = await post.transform(
223
+ `export default function AComponent() {}`,
224
+ 'AComponent.test.svelte',
225
+ );
226
+
227
+ expect(result?.code).toMatchSnapshot();
228
+ });
3
229
  });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tries to execute a function and returns its result or undefined if an error occurs.
3
+ * @template T
4
+ * @param {T extends (...args: any[]) => any} fn
5
+ * @returns {ReturnType<T> | undefined}
6
+ */
7
+ export default function tryFn(fn, ...args) {
8
+ try {
9
+ return fn(...args);
10
+ //eslint-disable-next-line @typescript-eslint/no-unused-vars
11
+ } catch (error) {
12
+ return undefined;
13
+ }
14
+ }
package/tsconfig.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "compilerOptions": {
3
+ "lib": ["DOM", "ESNext"],
3
4
  "types": ["vitest/globals"],
4
5
  "noEmit": true,
5
6
  "rewriteRelativeImportExtensions": true,
package/vitest.config.ts CHANGED
@@ -7,6 +7,22 @@ export default defineConfig({
7
7
  logLevel: 'warn',
8
8
  plugins: [svelte(), svelteTesting(), getPlugins()],
9
9
  test: {
10
+ projects: [
11
+ {
12
+ extends: './vitest.config.ts',
13
+ test: {
14
+ name: 'unit',
15
+ include: ['src/**/*.{test,spec}.ts'],
16
+ },
17
+ },
18
+ {
19
+ extends: './vitest.config.ts',
20
+ test: {
21
+ name: 'examples',
22
+ include: ['examples/**/*.{test,spec}.svelte'],
23
+ },
24
+ },
25
+ ],
10
26
  coverage: {
11
27
  provider: 'v8',
12
28
  reporter: ['text', 'html'],
@@ -16,6 +32,5 @@ export default defineConfig({
16
32
  requireAssertions: true,
17
33
  },
18
34
  globals: true,
19
- include: ['examples/**/*.{test,spec}.svelte', 'src/**/*.{test,spec}.ts'],
20
35
  },
21
36
  });