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/.vscode/extensions.json +8 -0
- package/ast.json +1587 -0
- package/eslint.config.js +40 -0
- package/examples/basic/Basic.test.svelte +35 -0
- package/examples/basic/Basic.test.svelte.ast.json +803 -0
- package/id-test.code +44 -0
- package/id-test.map +1 -0
- package/package.json +13 -3
- package/src/components/core/Check.svelte +1 -1
- package/src/components/testing-library/index.d.ts +1 -0
- package/src/components/vitest-browser-svelte/index.d.ts +1 -0
- package/src/plugins/vitest.js +122 -79
- package/src/plugins/vitest.test.ts +3 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +21 -0
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.
|
|
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": "
|
|
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",
|
|
@@ -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 = {
|
package/src/plugins/vitest.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
130
|
+
return { code: s.toString() };
|
|
131
|
+
},
|
|
132
|
+
});
|
|
90
133
|
|
|
91
134
|
export default function getPlugins() {
|
|
92
135
|
return [pre(), post()];
|
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
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|