svelteness / svelte-jester

A Jest transformer for Svelte - compile your components before importing them into tests.
MIT License
128 stars 18 forks source link

Run the svelte code through babel #3

Closed spirift closed 4 years ago

spirift commented 4 years ago

I'm using svelte-jester successfully to test my svelte app with testing-library. Love it!

However mocking svelte components is so far something that I have not figured out. I am trying to mock svelte-select in this instance but it is something that will come up time and again while testing.

I am currently trying to use babel-plugin-rewire-exports which I can get working for any js module imports but not the svelte ones. I figure it's only a case of running the compiled code produced from svelte-jester through babel but I'm not sure how to do that. Any ideas?

mihar-22 commented 4 years ago

Hey @spirift sure you can use jest.mock to mock the module.

  1. You'll need to create the mock first like so
<script>
  // tests/fixtures/MockSvelteSelect.svelte

 export let items = [];
 export let selectedValue = null;
</script>

<div>Mocked Svelte Select</div>

Then in your test tell Jest to swap out svelte-select for the mocked version

import MockSvelteSelect from './fixtures/MockSvelteSelect.svelte';

// If the name doesn't start with "Mock" you'll get an error.
jest.mock('svelte-select', () => ({ default: MockSvelteSelect }));

// your usual testing stuff ...

That's it :)

spirift commented 4 years ago

Hey @mihar-22 thank you so much for the swift response. Unfortunately that is not working for me. It's pretty similar to a pattern I've used hundreds of times with React. I've created a reproduction on sandbox here or github here.

Any light you can shed on where I'm going wrong would be much appreciated.

mihar-22 commented 4 years ago

This is really weird because I can't get it to work in the repo you provided but I have pretty much an exact clone I made separately via the svelte-template and it works just fine ... Both repositories are pretty much identical.

I'm getting TypeError: Select is not a constructor

mihar-22 commented 4 years ago

Have you tried cloning a fresh svelte-template and trying again?

mihar-22 commented 4 years ago

So even stranger is the fact that if I delete the src folder and recreate the exact same code from scratch it works, if I use the original it doesn't...

spirift commented 4 years ago

That was a fresh repo I made this morning using npx degit sveltejs/template svelte-jest-mocking-example. I get the same error in an app I made using the sapper template which originally lead me down this rabbit hole.

mihar-22 commented 4 years ago

Hey @spirift I actually solved this before but my brain went completely blank. The explanation is here. In short Jest respects only the main key, so it's pulling in the UMD file and failing, you can test this by simply changing your import statement to import * as Select from 'svelte-select';.

To fix this you can install this custom resolver so that Jest can resolve the file using the svelte key, but you still have to tell Jest to transform it because by default it ignores node_modules, so add this to your jest.config.js:

transformIgnorePatterns: [
  "node_modules/(?!svelte-select)"
]

Sorry about that, completely slipped my mind. Enjoy!

spirift commented 4 years ago

Thanks again for that @mihar-22 the custom resolver helps a lot. Mocking is still painful though. I went through a bunch of different patterns before finding something that works. I've updated the repo if you want to take a look. Anyway I got something working which is enough, cheers!

mihar-22 commented 4 years ago

Hey @spirift I'm glad you made some progress. Out of curiosity I tried to see if I can recreate any of your issues but I couldn't. Jest caches transformations and that can sometimes cause issues to not go away.

Your configuration is correct and the following is the definitely the right way to do it.

import MockSvelteSelect from './fixtures/MockSvelteSelect.svelte';

jest.mock('svelte-select', () => ({ default: MockSvelteSelect }));

Try running your command like this yarn test --no-cache. Hopefully that fixes it. Make sure your Node and Yarn is updated as well.

nunesmatheus commented 4 years ago

Hey @mihar-22, first of all, thanks for providing the library! Like @spirift, I'm using it successfully to test my Svelte components. However after adding a Babel plugin I believe I am facing the exact same issue that @spirift faced, specifically:

I figure it's only a case of running the compiled code produced from svelte-jester through babel but I'm not sure how to do that

I configured rollup to use babel, so the plugin works well when I access the application on the browser, but when I try to run a test for the component that uses the plugin, I get the error Cannot find module [...], which makes sense if the Babel plugin is not being used.

It seems you guys solved @spirift specific problem another way, so I wonder: what should be the general approach for this?

I don't have much experience with JS and I am still trying to wrap my head around this stuff, so I am sorry if this issue is not related to this repository! Anyway, any guidance is appreciated...

mihar-22 commented 4 years ago

Hey @nunesmatheus basically Jest runs in a Node environment so it resolves only CommonJS modules and finds the files to pull in by looking at the main key in each package's package.json. This is why we have the concept of resolvers and transformers. Resolvers help us find the files we are trying to import, and transformers "transform" those files into CommonJS to be executed in Node.

svelte-jester is a transformer that makes sure Svelte code is transformed into JS that can run in Node so Jest can run your tests. If you're importing files from an external package then by default Jest doesn't transform those files at all. That's why babel-jest and svelte-jester are doing nothing and you're getting an error. In addition, if you're importing an external Svelte package and expecting Jest to resolve the files via the svelte key then that's not going to happen. As mentioned, it only looks at the main key.

So the general solution is if you're importing a package that contains un-transpiled code, then you have to add the following to your Jest configuration to get babel-jest to transform it:

transformIgnorePatterns: [
  "node_modules/(?!some-library)"
]

If you're importing a Svelte package and you're importing files relying on resolving them via the svelte key. You'll need a resolver such as jest-svelte-resolver.

nunesmatheus commented 4 years ago

@mihar-22 thanks for the explanation! That's the kind of stuff I am trying to understand. I believe, however, that I failed to explain my problem. I was actually trying to import a simple .js file located in the src folder of my svelte project. As I am going down folders, the imports were becoming verbose, like:

import { myFunction } from '../../../utils';

So I found babel-plugin-module-resolver, which allows me to set an alias on babel.config.js and import like this:

import { myFunction } from 'utils';

I configured rollup to run babel after compiling svelte files, so the babel plugin worked when running the application on the web browser, but not on tests, because jest was using svelte-jester to transform svelte files, not babel, so I figured I needed to chain svelte-jester with babel-jest. Here's the transformer I came up with:

const babelJest = require('babel-jest')
const svelteJester = require('svelte-jester')

const transformer = (src, filename, ...rest) => {
  const transformedContent = svelteJester.createTransformer().process(src, filename);
  return babelJest.process(transformedContent.code, filename, ...rest)
}

exports.process = transformer

I saved it to a file ./svelte-jester-babel.js and changed my jest.config.js from:

module.exports = {
  transform: {
    '^.+\\.svelte$': 'svelte-jester',
    '^.+\\.js$': 'babel-jest',
  }
}

to:

module.exports = {
  transform: {
    '^.+\\.svelte$': './svelte-jester-babel',
    '^.+\\.js$': 'babel-jest',
  }
}

After hours and hours of research, I feel like this should be straightforward for experienced people, but I am posting here because it might be useful for someone that comes across something like this and struggles with the core concepts like me...

Unfortunately my transformer does not handle sourcemaps well, so jest --coverage is broken, but I believe this is a start.

mihar-22 commented 4 years ago

Ahhh I see, I haven't been in that situation exactly but I would try to use moduleNameMapper.

Maybe something like this:

module.exports = {
  // ...
  'moduleNameMapper': {
    'utils': '<rootDir>/src/utils/$1'
  }
}

Try it out and let me know if it works :)

nunesmatheus commented 4 years ago

Cool! It does work :). It's a tiny bit of a bummer that the same aliases have to be declared on two files, but it is totally worth it because now I can use jest --coverage! Thanks!!