routify
routify copied to clipboard
[jest] Add an example of testing using jest
Hello,
could you add some example of testing a component that uses Routify (or its part like url
)?
This would improve our experience and avoid struggling with configuration in the future.
For example, I built my project from scratch using npx @roxi/routify@next create my-r3-app
, then installed necessary packages (npm i --save-dev jest babel-jest svelte-jester @testing-library/svelte babel
).
However, still encounter an error - referring to ESM support - like:
ReferenceError: module is not defined in ES module scope
This file is being treated as an ES module because it has a '.js' file extension
and '/home/.../svelte/my-r3-app/package.json' contains "type": "module".
To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
version 3. But also related to the 2
files babel.config.js:
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
]
],
plugins: ["@babel/plugin-transform-modules-commonjs"]
};
jest.config.js:
module.exports = {
transform: {
"^.+\\.svelte$": [
"svelte-jester",
{
"preprocess": true
}
],
"^.+\\.ts$": "ts-jest",
"^.+\\.js$": "babel-jest"
},
moduleFileExtensions: [
"js",
"ts",
"svelte"
]
};
Note: see discussion at the Discord #general
channel.
+1 this would be great!
Currently, I'm just mocking routify
and overwriting the properties that I need, but that's not ideal if I want to render a component that relies on the functionality of e.g. url()
jest.mock('@roxi/routify', () => {
const originalModule = jest.requireActual('@roxi/routify');
return {
...originalModule.default,
params: writable({}),
};
});
I would also benefit greatly from seeing some examples for using (and especially mocking with) Jest. I have not been able to find any examples online of people mocking the likes of url
and isActive
.
This post on StackOverflow demonstrates a good effort to mock url
from @roxi/routify
with Jest, but it seems like he/she never resolved his/her issues.
Edit: I also cannot seem to use @tobiasbueschel's mocking approach above - I run into
ReferenceError: Cannot access 'store_1' before initialization
due to the writable({})
call. I saw this was one of the errors that the StackOverflow person encountered, too, so if there is a common fix/workaround for this, that information would be quite valuable I think.
The helpers are objects with a subscribe method, which Svelte calls on component initialization. Most of them rely on the context of the parent user component, so the logic ends up like this
import { getContext } from 'svelte'
import { derived } from 'svAelte/store'
export const someHelper = {
subscribe: () => {
// we need the context and we can only get this at component initialization
// luckily Svelte will handle this, since the presence of `$someHelper` in a component
// will cause Svelte to call subscribe at initialization
const { someStore } = getContext('routify-fragment-context')
// return the helper function, eg. url or isActive
return createSomeHelperFunctionFromContext(someStore)
}
}}
// if the helper has to be reactive, derived is used
export const someHelper2 = {
subscribe: () => {
const { someStore } = getContext('routify-fragment-context')
return derived(someStore, $someStore => {
return createSomeHelperFunctionFromContext(someStore)
})
}
}}
The actual helpers for R3 can be seen here. https://github.com/roxiness/routify/blob/next/lib/runtime/helpers/index.js
I wish I could help with Jest, but I dislike it so much I ended up writing my own test framework.
Alright, I did some tinkering with mocking in Jest and I managed to come up with a solution that allows for a little more flexibility in mocked values (thanks @jakobrosenberg for pointing me to your custom framework; it helped guide me to the answer).
Below demonstrates how to mock specific Routify helper functions using Jest such that rendering components which depend on them causes no errors. @jakobrosenberg I am not sure whether you would want to include this example somewhere in the Routify documentation?
import type { Readable, Unsubscriber } from 'svelte/store';
import type { IsActiveHelper, UrlHelper } from '@roxi/routify/typings/runtime/helpers';
import { derived, readable } from 'svelte/store';
const mockIsActive: IsActiveHelper = path => true; // Customize this based on your unit test
const mockUrl: UrlHelper = path => path; // Customize this based on your unit test
function mockAggregator<T>(value: T, setter: (value: T) => void): void | Unsubscriber {
// Do nothing; there is no reason to aggregate value changes because we hard-code the returned
// values in mockIsActive and mockUrl above
}
jest.mock('@roxi/routify', () => {
// These mock store definitions must be defined here (instead of e.g. global scope) because Jest
// will automatically hoist the jest.mock() call to the top of the file, and if isActiveStore or
// urlStore were defined in global scope, they would be undefined at the point in time this mock
// is being invoked.
// See: https://www.bam.tech/article/fix-jest-mock-cannot-access-before-initialization-error
const isActiveStore: Readable<IsActiveHelper> = {
subscribe: (run, invalidate) =>
derived(readable<IsActiveHelper>(), mockAggregator, mockIsActive).subscribe(run, invalidate)
};
const urlStore: Readable<UrlHelper> = {
subscribe: (run, invalidate) =>
derived(readable<UrlHelper>(), mockAggregator, mockUrl).subscribe(run, invalidate)
};
return {
// Add other Routify mocks here as needed
isActive: isActiveStore,
url: urlStore
};
});
test('my test', () => {
// Render your Svelte component(s) as usual
});
Additional Findings
Interestingly, when I tried doing this:
derived(readable(mockIsActive), mockAggregator).subscribe(run, invalidate)
I received internal Svelte errors in my tests. I had expected this derived
usage to work the same as
derived(readable(), mockAggregator, mockIsActive).subscribe(run, invalidate)
because the API documentation suggests that these are simply different ways to express the same thing, but that does not appear to be the case. Anyway, this is not an issue in practice because the latter approach works fine - I just wanted to call it out here for awareness.
Thanks for the solution @cooperwalbrun . I might include a link to your post. 🙂
I recently revisited this and came up with an alternative way to make the Routify context available to components during Jest unit tests (i.e. so helper functions like $url
and $isActive
work properly). This code is simpler than what I proposed above, but it still has the drawback of requiring the user to understand a bit about Routify's internal logic. Oh well. I will post it below in case it helps others.
Note: this is a minimal "get it working for my use case" implementation. The mocked store would likely have to be expanded to include other key/value pairs depending on the Routify functions that users' components depend on. My personal use case is strictly $url
and $isActive
.
Helper function:
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
import { route } from '@roxi/routify/runtime/store';
type ArbitraryMap = { [key: string]: any };
export function createRoutifyContext(
activePath: string,
activeParams: ArbitraryMap = {},
activeComponentContext: ArbitraryMap = { params: {} } // This is the default from Routify's "rootContext" constant
): Writable<any> {
const activeRoute = { path: activePath, params: activeParams };
// We have to update the global constant "route" because Routify's internal logic is not always
// strictly combinatorial; in a couple places, it references the context directly from global
// scope via an import. In other places, it will pull the context in via getRoutifyContext(),
// which will instead point at the object returned by this createRoutifyContext() function
// thanks to our usage of the "routify" context key when we render our components in unit tests.
// Because we cannot control which of these two places gets referenced by Routify internally, we
// set the active route's mocked data in both places - in the faked root context which gets
// passed around within Routify AND in the global "route" constant.
route.set(activeRoute);
// The structure of the return value below is based on the "rootContext" global constant
// that is defined here: https://github.com/roxiness/routify/blob/master/runtime/store.js.
// There are also additional fields that are expected which are not added during initialization -
// in these cases, we have added them by hand.
return writable({
component: activeComponentContext,
route: activeRoute,
routes: []
});
}
Using the helper function in a test:
test('some test', () => {
const routifyContext = createRoutifyContext('/my/path');
const dom = render(MyComponent, {
target: document.body,
// The "routify" key used below is based on what Routify itself uses, i.e. in the
// getRoutifyContext() function in @roxi/routify/runtime/helpers, the context name being used
// is "routify"
context: new Map([['routify', routifyContext]])
});
// ...perform assertions related to your component's behavior in the context of /my/path...
});