svelte-declarative-testing 0.1.0 → 0.2.0

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.
package/id-test.code ADDED
@@ -0,0 +1,44 @@
1
+ <script>
2
+ import { Test, Describe, Check } from '../../src/components/testing-library';
3
+ </script>
4
+
5
+ <Describe label="Basic test">
6
+ {#snippet tests()}
7
+ <Test it="passes">
8
+ <button>Click me</button>
9
+
10
+ {#snippet checks()}
11
+ <Check
12
+ fn={({ getByRole }) => {
13
+ expect(getByRole('button', { name: 'Click me' })).not.toBe(null);
14
+ }}
15
+ />
16
+ {/snippet}
17
+ </Test>
18
+
19
+ <Describe label="Nested describe">
20
+ {#snippet tests()}
21
+ <Test it="also passes">
22
+ <button>Click me</button>
23
+
24
+ {#snippet checks()}
25
+ <Check
26
+ fn={({ getByRole }) => {
27
+ expect(getByRole('button', { name: 'Click me' })).not.toBe(null);
28
+ }}
29
+ />
30
+ {/snippet}
31
+ </Test>
32
+ {/snippet}
33
+ </Describe>
34
+ {/snippet}
35
+ </Describe>
36
+ {globalThis[Symbol()] ? function () {
37
+ describe("Basic test", () => {
38
+ test("passes", () => {})
39
+ describe("Nested describe", () => {
40
+ test("also passes", () => {})
41
+
42
+ })
43
+ })
44
+ } : ""}
package/id-test.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[""],"sourcesContent":["<script>\n import { Test, Describe, Check } from '../../src/components/testing-library';\n</script>\n\n<Describe label=\"Basic test\">\n {#snippet tests()}\n <Test it=\"passes\">\n <button>Click me</button>\n\n {#snippet checks()}\n <Check\n fn={({ getByRole }) => {\n expect(getByRole('button', { name: 'Click me' })).not.toBe(null);\n }}\n />\n {/snippet}\n </Test>\n\n <Describe label=\"Nested describe\">\n {#snippet tests()}\n <Test it=\"also passes\">\n <button>Click me</button>\n\n {#snippet checks()}\n <Check\n fn={({ getByRole }) => {\n expect(getByRole('button', { name: 'Click me' })).not.toBe(null);\n }}\n />\n {/snippet}\n </Test>\n {/snippet}\n </Describe>\n {/snippet}\n</Describe>\n"],"names":[],"mappings":"AAAA;AACA;AACA;;AAEA;AACA;AACA,IAAI;AACJ;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,IAAI;AACJ;AACA,MAAM;AACN;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-declarative-testing",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A way to mount your Svelte test components declaratively",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "scripts": {
27
- "test": "echo \"Error: no test specified\" && exit 1"
27
+ "test": "DEBUG= vitest"
28
28
  },
29
29
  "keywords": [
30
30
  "svelte",
@@ -36,13 +36,23 @@
36
36
  "license": "ISC",
37
37
  "dependencies": {
38
38
  "magic-string": "^0.30.21",
39
+ "source-map": "^0.7.6",
39
40
  "ts-essentials": "^10.1.1",
40
41
  "zimmerframe": "^1.1.4"
41
42
  },
42
43
  "devDependencies": {
44
+ "@eslint/compat": "^2.0.2",
45
+ "@eslint/js": "^9.39.2",
46
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
47
+ "@vitest/coverage-v8": "^4.0.18",
43
48
  "eslint": "^9.39.2",
49
+ "eslint-config-prettier": "^10.1.8",
50
+ "eslint-plugin-svelte": "^3.14.0",
51
+ "globals": "^17.3.0",
52
+ "happy-dom": "^20.5.0",
44
53
  "prettier": "^3.8.1",
45
- "prettier-plugin-svelte": "^3.4.1"
54
+ "prettier-plugin-svelte": "^3.4.1",
55
+ "typescript-eslint": "^8.54.0"
46
56
  },
47
57
  "peerDependencies": {
48
58
  "@testing-library/svelte": "^5.3.1",
@@ -1,4 +1,4 @@
1
- <script lang="ts">
1
+ <script>
2
2
  /**@import { CheckProps } from './' */
3
3
  import { getAddCheck } from './context.js';
4
4
 
@@ -2,6 +2,7 @@ import { RenderResult } from '@testing-library/svelte';
2
2
  import { Component } from 'svelte';
3
3
  import type { BaseTestProps, DescribeProps } from '../core';
4
4
 
5
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
5
6
  export type CheckFn = (result: RenderResult<Component<any, any>>) => void | Promise<void>;
6
7
 
7
8
  export type CheckProps = {
@@ -2,6 +2,7 @@ import { RenderResult } from 'vitest-browser-svelte';
2
2
  import { Component } from 'svelte';
3
3
  import type { BaseTestProps, DescribeProps } from '../core';
4
4
 
5
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
5
6
  export type CheckFn = (result: RenderResult<Component<any, any>>) => void | Promise<void>;
6
7
 
7
8
  export type CheckProps = {
@@ -2,91 +2,134 @@
2
2
  import { parse } from 'svelte/compiler';
3
3
  import { walk } from 'zimmerframe';
4
4
  import MagicString from 'magic-string';
5
+ import { SourceMapGenerator, SourceMapConsumer } from 'source-map';
5
6
 
6
- const pre = () =>
7
- /**@type {Plugin}*/ ({
8
- name: 'transform-svelte-declarative-test',
9
- enforce: 'pre',
10
- transform(code, id) {
11
- if (!/\.(?:test|spec)\.svelte$/.test(id)) {
12
- return;
7
+ /**
8
+ * This plugin has two parts:
9
+ *
10
+ * 1. A pre-transform that looks for <Test> and <Describe> components in
11
+ * .test.svelte files and generates corresponding test/describe blocks, while
12
+ * tracking source locations for accurate source maps. The generated code
13
+ * here has no purpose and does not run, it just allows Vitest to find the
14
+ * tests when it goes looking, and is an import part of VSCode Vitest compatibility.
15
+ *
16
+ * 2. A post-transform that mounts the Svelte component to the DOM when it is
17
+ * imported so that the tests run.
18
+ */
19
+
20
+ const mountCode =
21
+ 'import { mount } from "svelte"; mount((await import(import.meta.url)).default, { target: document.body });';
22
+
23
+ const testFileRegex = /\.(?:test|spec)\.svelte$/;
24
+ const getNameFromAttr = (node, attr) =>
25
+ node.attributes.find((a) => a.name === attr)?.value?.[0]?.data ?? '(unnamed test)';
26
+
27
+ const pre = /**@returns {Plugin}*/ () => ({
28
+ name: 'transform-svelte-declarative-test',
29
+ filter: {
30
+ id: testFileRegex,
31
+ },
32
+ enforce: 'pre',
33
+ async transform(code, id) {
34
+ if (!testFileRegex.test(id)) {
35
+ return;
36
+ }
37
+
38
+ const mappings = [];
39
+
40
+ let appendedCode = [''];
41
+ let currentLine = code.split('\n').length;
42
+
43
+ const addLine = (lineCode, location) => {
44
+ if (location) {
45
+ mappings.push({
46
+ generatedLine: currentLine,
47
+ generatedColumn: 0,
48
+ sourceLine: location.line - 1,
49
+ sourceColumn: location.column,
50
+ });
13
51
  }
52
+ appendedCode.push(lineCode);
53
+ currentLine++;
54
+ };
55
+
56
+ // We wrap generated test code in a Svelte if block that checks the global
57
+ // object for something we know doesn't exist. This ensures the generated
58
+ // code is never executed, but still allows Vitest to find the
59
+ // test/describe blocks when it parses the file.
60
+ //
61
+ // Note: we could just write `{#if false}` but if the Svelte parser is
62
+ // updated to optimize away dead code, it might remove our generated blocks entirely,
63
+ addLine('{#if globalThis[Symbol()]}{function () {');
64
+
65
+ const s = new MagicString(code);
66
+ const ast = parse(code, { modern: true });
67
+
68
+ // We walk the AST looking for our custom <Test> and <Describe>
69
+ // components, and generate corresponding test/describe blocks. We also
70
+ // track the original location of each node so we can create accurate
71
+ // source map mappings later.
72
+ walk(
73
+ ast.fragment,
74
+ {},
75
+ {
76
+ Component(node, { visit }) {
77
+ if (node.name === 'Test') {
78
+ const name = getNameFromAttr(node, 'it');
79
+ addLine(`test("${name}", () => {})`, node.name_loc.start);
80
+ } else if (node.name === 'Describe') {
81
+ const name = getNameFromAttr(node, 'label');
82
+ addLine(`describe("${name}", () => {`, node.name_loc.start);
14
83
 
15
- const s = new MagicString(code);
16
- const ast = parse(code);
17
- walk(
18
- ast.html,
19
- {},
20
- {
21
- InlineComponent(node, { visit }) {
22
- if (node.name === 'Test') {
23
- for (const attr of node.attributes) {
24
- if (attr.name === 'it') {
25
- const name =
26
- attr.value?.[0]?.data?.replace(/"/g, '\\"') ?? '(dynamically named test)';
27
-
28
- s.appendLeft(
29
- attr.start,
30
- `data-test-code={function () { test("${name}", () => {}) }} `,
31
- );
32
- }
33
- }
34
-
35
- return;
36
- } else if (node.name === 'Describe') {
37
- for (const attr of node.attributes) {
38
- if (attr.name === 'label') {
39
- const name =
40
- attr.value?.[0]?.data?.replace(/"/g, '\\"') ?? '(dynamically named test suite)';
41
-
42
- s.appendLeft(
43
- attr.start,
44
- `data-describe-code={function () { describe("${name}", () => {}) }} `,
45
- );
46
- }
47
- }
48
-
49
- for (const child of node.children) {
50
- if (child.type === 'SnippetBlock') {
51
- visit(child);
52
- }
53
- }
54
- }
55
- },
56
- SnippetBlock(node, { visit }) {
57
- for (const child of node.children) {
58
- if (child.type === 'InlineComponent') {
59
- visit(child);
60
- }
61
- }
62
- },
84
+ // Look through children for nested tests/describes
85
+ visit(node.fragment);
86
+
87
+ addLine(`})`, null);
88
+ }
63
89
  },
64
- );
65
-
66
- console.log(s.toString());
67
- return { code: s.toString(), map: s.generateMap({ hires: true }) };
68
- },
69
- });
70
-
71
- const post = () =>
72
- /**@type {Plugin}*/ ({
73
- name: 'transform-svelte-declarative-test',
74
- enforce: 'post',
75
- transform(code, id) {
76
- if (!/\.(?:test|spec)\.svelte$/.test(id)) {
77
- return;
78
- }
90
+ },
91
+ );
92
+
93
+ addLine(`}}{/if}`, null);
94
+
95
+ s.append(appendedCode.join('\n'));
96
+
97
+ const originalMap = s.generateMap({ source: id, includeContent: true, hires: true });
98
+ const consumer = await new SourceMapConsumer(originalMap);
99
+ const generator = SourceMapGenerator.fromSourceMap(consumer);
100
+
101
+ for (const mapping of mappings) {
102
+ generator.addMapping({
103
+ generated: { line: mapping.generatedLine + 1, column: mapping.generatedColumn },
104
+ source: id,
105
+ original: { line: mapping.sourceLine + 1, column: mapping.sourceColumn },
106
+ });
107
+ }
108
+
109
+ return { code: s.toString(), map: generator.toString() };
110
+ },
111
+ });
112
+
113
+ const post = /**@returns {Plugin}*/ () => ({
114
+ name: 'transform-svelte-declarative-test',
115
+ filter: {
116
+ id: testFileRegex,
117
+ },
118
+ enforce: 'post',
119
+ transform(code, id) {
120
+ if (!testFileRegex.test(id)) {
121
+ return;
122
+ }
123
+
124
+ const s = new MagicString(code);
79
125
 
80
- const s = new MagicString(code);
81
- const componentName = id.split('/').pop()?.split('.').slice(0, -1).join('_');
82
- s.append(`
83
- import { mount } from 'svelte';
84
- mount(${componentName}, { target: document.body });
85
- `);
126
+ // We mount by reimporting the default export of the Svelte file (the
127
+ // component). This is the magic that runs the tests.
128
+ s.append(mountCode);
86
129
 
87
- return { code: s.toString(), map: s.generateMap({ hires: true }) };
88
- },
89
- });
130
+ return { code: s.toString() };
131
+ },
132
+ });
90
133
 
91
134
  export default function getPlugins() {
92
135
  return [pre(), post()];
@@ -0,0 +1,3 @@
1
+ describe('vitest pre plugin', () => {
2
+ test.fails('transforms test files to mount the component', () => {});
3
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["vitest/globals"],
4
+ "noEmit": true,
5
+ "rewriteRelativeImportExtensions": true,
6
+ "allowJs": true,
7
+ "checkJs": true,
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "skipLibCheck": true,
12
+ "sourceMap": true,
13
+ "strict": true,
14
+ "module": "preserve",
15
+ "moduleResolution": "bundler"
16
+ },
17
+ "include": [
18
+ "./types/**/$types.d.ts",
19
+ "./vite.config.js",
20
+ "./vite.config.ts",
21
+ "./src/**/*.js",
22
+ "./src/**/*.ts",
23
+ "./src/**/*.svelte",
24
+ "./tests/**/*.js",
25
+ "./tests/**/*.ts",
26
+ "./tests/**/*.svelte"
27
+ ],
28
+ "exclude": ["./node_modules/**"]
29
+ // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
30
+ // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
31
+ //
32
+ // To make changes to top-level options such as include and exclude, we recommend extending
33
+ // the generated config; see https://svelte.dev/docs/kit/configuration#typescript
34
+ }
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import { svelteTesting } from '@testing-library/svelte/vite';
3
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
4
+ import getPlugins from './src/plugins/vitest';
5
+
6
+ export default defineConfig({
7
+ logLevel: 'warn',
8
+ plugins: [svelte(), svelteTesting(), getPlugins()],
9
+ test: {
10
+ coverage: {
11
+ provider: 'v8',
12
+ reporter: ['text', 'html'],
13
+ },
14
+ environment: 'happy-dom',
15
+ expect: {
16
+ requireAssertions: true,
17
+ },
18
+ globals: true,
19
+ include: ['examples/**/*.{test,spec}.svelte', 'src/**/*.{test,spec}.ts'],
20
+ },
21
+ });