├── .npmignore
├── .prettierignore
├── .eslintignore
├── tests
├── fixtures
│ └── Dom.svelte
└── test.js
├── prettier.config.js
├── babel.config.js
├── .github
└── workflows
│ ├── ci.yml
│ └── publish.yml
├── .dependabot
└── config.yml
├── .eslintrc.json
├── rollup.config.js
├── package.json
├── .gitignore
├── README.md
└── src
└── index.js
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist/
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules/*
2 | /dist/*
--------------------------------------------------------------------------------
/tests/fixtures/Dom.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
{answer}
6 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 80,
3 | trailingComma: 'all',
4 | tabWidth: 4,
5 | semi: false,
6 | singleQuote: true,
7 | }
8 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | node: 'current',
8 | },
9 | },
10 | ],
11 | ],
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: actions/setup-node@v1
12 | - run: yarn install
13 | - run: yarn run test
14 | env:
15 | CI: true
16 |
--------------------------------------------------------------------------------
/.dependabot/config.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 |
3 | update_configs:
4 | - package_manager: 'javascript'
5 | directory: '/'
6 | update_schedule: 'live'
7 | ignored_updates:
8 | - match:
9 | dependency_name: 'svelte'
10 | automerged_updates:
11 | - match:
12 | update_type: 'all'
13 | target_branch: master
14 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | },
5 | "parserOptions": {
6 | "ecmaVersion": 2018,
7 | "sourceType": "module"
8 | },
9 | "extends": ["airbnb-base", "prettier"],
10 | "rules": {
11 | "import/no-extraneous-dependencies": [
12 | "error",
13 | { "devDependencies": true }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import pkg from './package.json'
2 |
3 | export default {
4 | input: 'src/index.js',
5 | output: [
6 | {
7 | file: pkg.main,
8 | format: 'cjs',
9 | sourcemap: true,
10 | },
11 | {
12 | file: pkg.module,
13 | format: 'es',
14 | sourcemap: true,
15 | },
16 | ],
17 | external: [
18 | 'path',
19 | 'rollup',
20 | 'rollup-plugin-svelte',
21 | '@rollup/plugin-node-resolve',
22 | 'jsdom',
23 | 'svelte',
24 | 'svelte/compiler',
25 | 'code-red',
26 | ],
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: actions/setup-node@v1
15 | with:
16 | node-version: '12.x'
17 | registry-url: 'https://registry.npmjs.org'
18 | - run: yarn install
19 | - run: yarn run build
20 | - run: yarn run test
21 | env:
22 | CI: true
23 | - run: yarn publish
24 | env:
25 | NODE_AUTH_TOKEN: ${{ secrets.YARN_TOKEN }}
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dainte",
3 | "version": "0.1.5",
4 | "main": "dist/dainte.js",
5 | "module": "dist/dainte.es.js",
6 | "repository": "git@github.com:nathancahill/dainte.git",
7 | "author": "Nathan Cahill ",
8 | "license": "MIT",
9 | "devDependencies": {
10 | "@babel/core": "^7.14.8",
11 | "@babel/preset-env": "^7.14.9",
12 | "babel-jest": "^27.0.6",
13 | "eslint": "^7.32.0",
14 | "eslint-config-airbnb-base": "^14.2.1",
15 | "eslint-config-prettier": "^8.3.0",
16 | "eslint-plugin-import": "^2.23.4",
17 | "jest": "^27.0.6",
18 | "jest-transform-svelte": "^2.1.1",
19 | "prettier": "^2.3.2"
20 | },
21 | "scripts": {
22 | "build": "rollup -c",
23 | "format": "prettier --write \"**/*.js\" \"**/*.json\"",
24 | "lint": "eslint .",
25 | "prepublishOnly": "yarn run lint",
26 | "preversion": "yarn run test",
27 | "postversion": "git push && git push --tags",
28 | "test": "jest && yarn run lint"
29 | },
30 | "files": [
31 | "dist/**/*"
32 | ],
33 | "dependencies": {
34 | "@rollup/plugin-node-resolve": "^13.0.4",
35 | "code-red": "^0.2.1",
36 | "jsdom": "^16.7.0",
37 | "rollup": "^2.55.1",
38 | "rollup-plugin-svelte": "^7.1.0",
39 | "svelte": "^3.0.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/test.js:
--------------------------------------------------------------------------------
1 | import { tick } from 'svelte'
2 | import { mount, compile, render } from '../src'
3 |
4 | test('compile', async () => {
5 | const { Dom } = await compile('./tests/fixtures/Dom.svelte')
6 |
7 | expect(Dom).toBeInstanceOf(Function)
8 | })
9 |
10 | test('mount', async () => {
11 | const { document } = await mount('./tests/fixtures/Dom.svelte', {
12 | props: {
13 | answer: 42,
14 | },
15 | })
16 |
17 | expect(document.getElementById('answer').textContent).toBe('42')
18 | })
19 |
20 | test('inspect', async () => {
21 | const { dom } = await mount('./tests/fixtures/Dom.svelte', {
22 | props: {
23 | answer: 42,
24 | },
25 | inspect: true,
26 | })
27 |
28 | const { answer } = dom.inspect()
29 |
30 | expect(answer).toBe(42)
31 | })
32 |
33 | test('update', async () => {
34 | const { dom, document } = await mount('./tests/fixtures/Dom.svelte', {
35 | props: {
36 | answer: 42,
37 | },
38 | accessors: true,
39 | })
40 |
41 | expect(document.getElementById('answer').textContent).toBe('42')
42 |
43 | dom.$set({ answer: 40 })
44 | await tick()
45 |
46 | expect(document.getElementById('answer').textContent).toBe('40')
47 | })
48 |
49 | test('render', async () => {
50 | const { head, html, css } = await render('./tests/fixtures/Dom.svelte', {
51 | props: {
52 | answer: 42,
53 | },
54 | })
55 |
56 | expect(head).toBe('')
57 | expect(html).toBe('42
')
58 | expect(css).toMatchObject({ code: '', map: null })
59 | })
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dainte 
2 |
3 | Painless testing for Svelte components, inspired by Enzyme.
4 |
5 | 🥂 Test Svelte runtime and SSR simultanously
6 | 🎭 Zero-config compatible with Jest
7 | 🤖 Low-level compile options to ensure that tests match prod
8 | 🔎 Component "state" introspection
9 |
10 | ## Usage
11 |
12 | ### `dainte.compile`
13 |
14 | ```js
15 | result: {
16 | // Compiled Svelte component class
17 | Component,
18 |
19 | // Alias to component as specified or inferred name
20 | [name],
21 | } = await dainte.compile(source: string, options?: {...})
22 | ```
23 |
24 | Creates a compiled Svelte component class from a source file path.
25 |
26 | The following options can be passed to `compile`, including [svelte.compile options](https://svelte.dev/docs#svelte_compile).
27 | The `dev` option defaults to `true` for testing. None are required:
28 |
29 | | option | default | description |
30 | |:-------------|:------------------------|:-----------------------------------------------------------------|
31 | | `name` | `'Component'` | Name of the component class, inferred from filename |
32 | | `dev` | `true` | Perform runtime checks and provide debugging information |
33 | | `immutable` | `false` | You promise not to mutate any objects |
34 | | `hydratable` | `false` | Enables the hydrate: true runtime option |
35 | | `legacy` | `false` | Generates code that will work in IE9 and IE10 |
36 | | `accessors` | `false` | Getters and setters will be created for the component's props |
37 | | `css` | `true` | Include CSS styles in JS class |
38 | | `generate` | `'dom'` | Create JS DOM class or object with `.render()` |
39 | | `inspect` | `false` | Include `instance.inspect()` accessor |
40 | | `plugins` | `[svelte(), resolve()]` | Advanced option to manually specify Rollup plugins for bundling. |
41 |
42 | #### Example
43 |
44 | ```js
45 | import { compile } from 'dainte'
46 |
47 | const { App } = await compile('./App.svelte')
48 |
49 | const app = new App({
50 | target: document.body,
51 | })
52 | ```
53 |
54 | ### `dainte.mount`
55 |
56 | ```js
57 | result: {
58 | // Svelte component instance
59 | instance,
60 |
61 | // Compiled JS component class
62 | Component,
63 |
64 | // JSDom window and document where component is mounted.
65 | window,
66 | document,
67 |
68 | // Alias to the Component with specified or inferred name
69 | [name],
70 |
71 | // Alias to the instance with lowercase specified or inferred name
72 | [lowercase(name)],
73 | } = await mount(source: string, options?: {...})
74 | ```
75 |
76 | Creates an instance of a component from a source file path. Mounts the instance
77 | in a JSDom.
78 |
79 | All `compile` options can also be passed to `mount`. Additionally, these options, including the [component initialisation options](https://svelte.dev/docs#Creating_a_component), can be provided:
80 |
81 | | option | default | description |
82 | |:-------------|:------------------------|:-----------------------------------------------------------------|
83 | | `html` | `''` | HTML to initiate the JSDom instance with |
84 | | `target` | `'body'` | Render target (as a query selector, *not a DOM element* as in Svelte initialisation) |
85 | | `anchor` | `null` | Render anchor (as a query selector, *not a DOM element* as in Svelte initialisation) |
86 | | `props` | `{}` | An object of properties to supply to the component |
87 | | `hydrate` | `false` | Upgrade existing DOM instead of replacing it |
88 | | `intro` | `false` | Play transitions on initial render |
89 |
90 | A `svelte.tick` is awaited between mounting the instance and resolving the `mount` promise so
91 | that the DOM is full initialized. An additional `svelte.tick` should be awaited
92 | between updating the component and reading from the DOM.
93 |
94 | #### Example
95 |
96 | ```js
97 | import { mount } from 'dainte'
98 | import { tick } from 'svelte'
99 |
100 | const { app, document } = await mount('./App.svelte')
101 | app.$set({ answer: 42 })
102 | await tick()
103 |
104 | expect(document.querySelector('#answer').textContent).toBe('42')
105 | ```
106 |
107 | ### `dainte.render`
108 |
109 | ```js
110 | result: {
111 | head,
112 | html,
113 | css,
114 | } = await dainte.render(source: string, options?: {...})
115 | ```
116 |
117 | Wraps Svelte's server-side `Component.render` API for rendering a component
118 | to HTML.
119 |
120 | The following options can be passed to `render`, including [svelte.compile options](https://svelte.dev/docs#svelte_compile).
121 | The `dev` option defaults to `true` for testing. None are required:
122 |
123 | | option | default | description |
124 | |:-------------|:------------------------|:-----------------------------------------------------------------|
125 | | `dev` | `true` | Perform runtime checks and provide debugging information |
126 | | `immutable` | `false` | You promise not to mutate any objects |
127 | | `hydratable` | `false` | Enables the hydrate: true runtime option |
128 | | `css` | `true` | Include CSS styles in JS class |
129 | | `preserveComments` | `false` | HTML comments will be preserved |
130 | | `preserveWhitespace` | `false` | Keep whitespace inside and between elements as you typed it |
131 | | `plugins` | `[svelte(), resolve()]` | Advanced option to manually specify Rollup plugins for bundling. |
132 |
133 | #### Example
134 |
135 | ```js
136 | import { render } from 'dainte'
137 |
138 | const { html } = await render('./App.svelte')
139 | // '42
'
140 | ```
141 |
142 | ### `instance.inspect`
143 |
144 | ```js
145 | variables: {
146 | // Snapshot of all top-level variables and imports
147 | } = instance.inspect()
148 | ```
149 |
150 | Compiling with `inspect: true` adds a `inspect()` function to the component instance.
151 | Calling the function returns a snapshot object of all top-level variables and their
152 | current values. Snapshot values are not reactive and `inspect()` must be called
153 | again to retrieve updated values.
154 |
155 | #### Example
156 |
157 | ```js
158 | import { mount } from 'dainte'
159 |
160 | const { app } = await mount('./App.svelte', { inspect: true })
161 | const { answer } = app.inspect()
162 | // 42
163 | ```
164 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { rollup } from 'rollup'
3 | import rollupSvelte from 'rollup-plugin-svelte'
4 | import resolve from '@rollup/plugin-node-resolve'
5 | import { JSDOM } from 'jsdom'
6 | import { tick } from 'svelte'
7 | import { compile as svelteCompile } from 'svelte/compiler'
8 | import { print, b } from 'code-red'
9 |
10 | const sanitize = input =>
11 | path
12 | .basename(input)
13 | .replace(path.extname(input), '')
14 | .replace(/[^a-zA-Z_$0-9]+/g, '_')
15 | .replace(/^_/, '')
16 | .replace(/_$/, '')
17 | .replace(/^(\d)/, '_$1')
18 |
19 | const capitalize = str => str[0].toUpperCase() + str.slice(1)
20 | const lowercase = str => str[0].toLowerCase() + str.slice(1)
21 |
22 | const roll = async (
23 | source,
24 | {
25 | name: providedName = null,
26 | plugins = null,
27 | dev = true,
28 | immutable = false,
29 | hydratable = false,
30 | legacy = false,
31 | accessors = false,
32 | css = true,
33 | generate = 'dom',
34 | inspect = false,
35 | } = {},
36 | ) => {
37 | const bundle = await rollup({
38 | input: source,
39 | plugins: plugins || [
40 | rollupSvelte({
41 | emitCss: false,
42 | compilerOptions: {
43 | dev,
44 | immutable,
45 | hydratable,
46 | legacy,
47 | accessors: accessors || inspect,
48 | css,
49 | generate,
50 | },
51 | preprocess: {
52 | script: input => {
53 | if (inspect) {
54 | const ast = svelteCompile(
55 | ``,
56 | )
57 |
58 | const scope = {
59 | type: 'ObjectExpression',
60 | properties: ast.vars.map(variable => ({
61 | type: 'Property',
62 | key: {
63 | type: 'Identifier',
64 | name: variable.name,
65 | },
66 | value: {
67 | type: 'Identifier',
68 | name: variable.name,
69 | },
70 | })),
71 | }
72 |
73 | const func = print(
74 | b`export const inspect = () => (${scope})`,
75 | )
76 |
77 | return { code: `${input.content}\n${func.code}` }
78 | }
79 |
80 | return { code: input.content }
81 | },
82 | },
83 | }),
84 | resolve(),
85 | ],
86 | })
87 |
88 | const { output } = await bundle.generate({
89 | format: 'iife',
90 | name: providedName,
91 | })
92 |
93 | return output[0].code
94 | }
95 |
96 | export const compile = async (
97 | source,
98 | {
99 | name: providedName = null,
100 | plugins = null,
101 | dev = true,
102 | immutable = false,
103 | hydratable = false,
104 | legacy = false,
105 | accessors = false,
106 | css = true,
107 | generate = 'dom',
108 | inspect = false,
109 | html = '',
110 | } = {},
111 | ) => {
112 | const name = providedName || capitalize(sanitize(source))
113 |
114 | const code = await roll(source, {
115 | name,
116 | plugins,
117 | dev,
118 | immutable,
119 | hydratable,
120 | legacy,
121 | accessors,
122 | css,
123 | generate,
124 | inspect,
125 | html,
126 | })
127 |
128 | // eslint-disable-next-line no-new-func
129 | const Component = new Function(`${code}return ${name};`)()
130 |
131 | return {
132 | Component,
133 | [name]: Component,
134 | }
135 | }
136 |
137 | export const mount = async (
138 | source,
139 | {
140 | name: providedName,
141 | plugins = null,
142 | dev = true,
143 | immutable = false,
144 | hydratable = false,
145 | legacy = false,
146 | accessors = false,
147 | css = true,
148 | inspect = false,
149 |
150 | html = '',
151 | target = 'body',
152 | anchor = null,
153 | props = {},
154 | hydrate = false,
155 | intro = false,
156 | } = {},
157 | ) => {
158 | const name = providedName || capitalize(sanitize(source))
159 |
160 | const code = await roll(source, {
161 | name,
162 | plugins,
163 | dev,
164 | immutable,
165 | hydratable,
166 | legacy,
167 | accessors,
168 | css,
169 | inspect,
170 | html,
171 | })
172 |
173 | const { window } = new JSDOM(html, { runScripts: 'dangerously' })
174 | const { document } = window
175 |
176 | const script = document.createElement('script')
177 | script.type = 'text/javascript'
178 | script.text = code
179 | document.body.appendChild(script)
180 |
181 | const Component = window[name]
182 |
183 | const instance = new Component({
184 | target: document.querySelector(target),
185 | anchor: anchor ? document.querySelector(anchor) : null,
186 | props,
187 | hydrate,
188 | intro,
189 | })
190 |
191 | await tick()
192 |
193 | return {
194 | window,
195 | document,
196 | Component,
197 | instance,
198 | [name]: Component,
199 | [lowercase(name)]: instance,
200 | }
201 | }
202 |
203 | export const render = async (
204 | source,
205 | {
206 | dev = true,
207 | immutable = false,
208 | hydratable = false,
209 | css = true,
210 | preserveComments = false,
211 | preserveWhitespace = false,
212 |
213 | plugins = null,
214 | props = {},
215 | } = {},
216 | ) => {
217 | const bundle = await rollup({
218 | input: source,
219 | plugins: plugins || [
220 | rollupSvelte({
221 | emitCss: false,
222 | compilerOptions: {
223 | generate: 'ssr',
224 | dev,
225 | immutable,
226 | hydratable,
227 | css,
228 | preserveComments,
229 | preserveWhitespace,
230 | },
231 | }),
232 | resolve(),
233 | ],
234 | })
235 |
236 | const name = 'Component'
237 |
238 | const { output } = await bundle.generate({
239 | format: 'iife',
240 | name,
241 | })
242 |
243 | // eslint-disable-next-line no-new-func
244 | const Component = new Function(`${output[0].code}return ${name};`)()
245 |
246 | return Component.render(props)
247 | }
248 |
--------------------------------------------------------------------------------