Vue test utils: extend VueWrapper to add plugin method - vue.js

I am using Vue test utils and Typescript. I have added Data Test ID Plugin.
How can I extend VueWrapper interface to avoid this error:
Property 'findByTestId' does not exist on type 'VueWrapper<{ $: ComponentInternalInstance; $data: { showUserMenu: boolean ...

One solution is to export a type that adds findByTestId:
// my-vue-test-utils-plugin.ts
import { config, DOMWrapper, createWrapperError, type VueWrapper } from '#vue/test-utils'
👇
export type TestWrapper = VueWrapper<any> & {
findByTestId: (selector: string) => DOMWrapper<HTMLElement>
}
const DataTestIdPlugin = (wrapper: VueWrapper<any>) => {
function findByTestId(selector: string) {
const dataSelector = `[data-testid='${selector}']`
const element = wrapper.element.querySelector(dataSelector)
if (element) {
return new DOMWrapper(element)
}
return createWrapperError('DOMWrapper')
}
return {
findByTestId
}
}
config.plugins.VueWrapper.install(DataTestIdPlugin as any)
Then, use type assertion (as keyword followed by the exported type above) on the mount() result:
// MyComponent.spec.ts
import type { TestWrapper } from './my-vue-test-utils-plugin.ts'
describe('MyComponent', () => {
it('renders properly', () => { 👇
const wrapper = mount(MyComponent) as TestWrapper
expect(wrapper.findByTestId('my-component').text()).toBe('Hello World')
})
})

Another option is to create a .d.ts file e.g. vue-test-utils.d.ts with the following content:
import { DOMWrapper } from '#vue/test-utils';
declare module '#vue/test-utils' {
export class VueWrapper {
findByTestId(selector: string): DOMWrapper[];
}
}
So you're able to extend the existing definition of the VueWrapper class.

Related

Vue.js 2.7, "script setup"-approach, defineEmits with composable which needs a emit-function

I am using Vue.js 2.7, I have a (probably TS-only) issue with defineEmits and a self-written composable which needs an emit-function to pass in. The self-written composable should replace a Mixin and is actually too complex, but that's not the problem a like to ask about.
The following does not work.
vue
<script setup lang="ts">
import { defineEmits } from 'vue'; // <- removing this import has no effect
import { useIcomActCompatibility } from '#/composables/compatibility/useIcomActCompatibility';
import { ApiCallParameterType } from '#/services/ApiService';
import { ApiContacts_ListType } from '#/store/types/api/events';
const emit = defineEmits({
'i-do-nothing'(): boolean {
return true;
},
});
const { apiId /* TODO add other necessary stuff */ } = useIcomActCompatibility<
'contacts',
ApiContacts_ListType,
'List'
>({
usedApiId: 'contacts',
usedApiUrl: '/configuration/events/contacts',
usedDataByIdStorePath: 'apiEvents/getApiDataById',
emit, // <- TS2322
sendApiRequest: (_p: ApiCallParameterType): Promise<boolean> => (
console.log('calling dummy sendApiRequest'), Promise.resolve(true)
),
});
</script>
TS2322 is:
Type '(event: 'i-do-nothing') => void' is not assignable to type '(event: string, ...args: any[]) => void'.
Types of parameters 'event' and 'event' are incompatible.
Type 'string' is not assignable to type ''i-do-nothing''.
The code (Vue component) above, beside the TS-Error, could not be created.
Well so far. If I change the code like this?
Vue
<script lang="ts">
import { defineComponent, Ref } from 'vue';
import { useIcomActCompatibility } from '#/composables/compatibility/useIcomActCompatibility';
import { ApiCallParameterType, ApiDataType } from '#/services/ApiService';
import { ApiContacts_ListType } from '#/store/types/api/events';
export default defineComponent({
name: 'HttpServerAct',
setup(props, { emit }): { apiId: Ref<keyof ApiDataType> } {
const { apiId /* TODO add other necessary stuff */ } = useIcomActCompatibility<
'contacts',
ApiContacts_ListType,
'List'
>({
usedApiId: 'contacts',
usedApiUrl: '/configuration/events/contacts',
usedDataByIdStorePath: 'apiEvents/getApiDataById',
emit,
sendApiRequest: (_p: ApiCallParameterType): Promise<boolean> => (
console.log('calling dummy sendApiRequest'), Promise.resolve(true)
),
});
return { apiId };
},
});
</script>
Everything works. My expectation was/is that the 1. version should work also.
What am I doing wrong? I can't imagine that the TS error is solely responsible for this.
Your defineEmits should look like this:
const emit = defineEmits<{
(e: 'i-do-nothing'): boolean;
}>();
https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits

Vue test utils: mock getter return value

I'm writing a test that mocks the return value of a namespaced getter, which accepts a single argument. The test parameter will be the mocked return value for the getter, which controls the visibility of an element on the page.
Mocking the getter with a generic return value allows it to be defined, but so far mocking the return value with the test parameter is not working.
Getter (in the "app" module):
const getters = {
myGetter:
(state) =>
(getterArg) => {
return state.someStateVariable === true
? valueForTrue
: valueForFalse;
}
};
Test:
import { cloneDeep } from "lodash";
import { createLocalVue, shallowMount } from "#vue/test-utils";
import MyComponent from "#/views/MyComponent.vue";
import { storeConfig } from "#/store";
import Vuex from "vuex";
describe("My test", () => {
let localVue;
let store;
let wrapper;
beforeEach(() => {
localVue = createLocalVue();
localVue.use(Vuex);
});
test.each([[false], [true]])(
"Test param: '%s'",
(testParam) => {
store = new Vuex.Store({
modules: {
app: {
namespaced: true,
...cloneDeep(storeConfig.modules.app),
state: {
...cloneDeep(storeConfig.modules.app.state),
getters: {
...cloneDeep(storeConfig.modules.app.getters),
// getter defined, but doesn't include test param
myGetter: jest.fn().mockReturnValue(jest.fn())
// getter variation with a mocked return value (test fails due to testParam not being returned)
myGetter: jest.fn().mockReturnValue(jest.fn().mockReturnValue(testParam))
// getter variation with the argument passed by the component, and a mocked return value (test fails due to testParam not being returned)
myGetter: jest.fn("getterArg").mockReturnValue(jest.fn().mockReturnValue(testParam))
}
}
}
});
wrapper = shallowMount(MyComponent, {
localVue,
store
});
expect(wrapper.find("#my-element").exists()).toBe(
testParam
);
I also tried updating the wrapper after creation:
// Test fails due to getter being undefined
Object.defineProperty(wrapper.vm, "app/myGetter", {
value: () => {
return testParam;
}
});
// Test fails due to getter being undefined
Object.defineProperty(wrapper.vm, "app/myGetter", {
value: ("getterArg") => {
return testParam;
}
});
How can this namespaced getter be configured to dynamically return the test parameter?

How to access shadowDom when testing Lit element with open-wc

Lit docs refer to Web Test Runner as testing. It navigates to this example page.
I tried testing MyElement, which has only one <p>.
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
#customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`<p>Hello, World.</p>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"my-element": MyElement;
}
}
When testing by open-wc, the element's shadowDom did not have <p> in descendant.
import { expect, fixture, html } from "#open-wc/testing";
import { MyElement } from "../src/MyElement";
it("get shadowDom", async () => {
const el: MyElement = await fixture(html`<my-element></my-element>`);
expect(el).shadowDom.to.be.not.null; // passed
expect(el).shadowDom.to.have.descendant("p"); // failed
});
Does it need more setup to test Lit elements with open-wc?
web-test-runner.config.js is:
import { esbuildPlugin } from '#web/dev-server-esbuild';
export default {
files: ['test/*.test.ts'],
plugins: [esbuildPlugin({ ts: true })],
};
Try shadowRoot instead of shadowDom:
it("get shadowDom", async () => {
const el = await fixture(
html` <my-element></my-element>`
);
const descendant = el.shadowRoot!.querySelector("p")!;
expect(descendant).to.be.not.null;
});
I had similar issue. In my case shadowRoot was "null". To have shadowRoot content I had to import my web component like that:
import './myWebcomponent';

Async function support in Mithril.js for webpack dynamic imports?

I'm trying to figure out how to use dynamic import in webpack with mithril. To do that elegantly, I think I'll need to use an async function somewhere along the line. Right now this is how I have used the async function:
import m from 'mithril'
let App = async () => {
let { Component } = await import('./components.js')
return {
view () {
return m(Component)
}
}
}
App().then(app => m.mount(document.body, app))
Ideally, I want to use it like this:
import m from 'mithril'
let App = {
async view () {
let { Component } = await import('./components.js')
return m(Component)
}
}
}
m.mount(document.body, App)
Is there something I've been missing from the documentation to acheive what I'd like to do? I've tried to look at every mention of promise, but it's possible that I've missed this.
Any help would be appreciated.
One way that should work is this:
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
(async () => {
const myModule = await import('./myModule.js');
})();
For further information follow the link below.
ES proposal: import() – dynamically importing ES modules
Try the following, which provides a simple component named DynamicComponent which can be used anywhere and with children:
App.js
import m from 'mithril'
import { DynamicComponent } from './DynamicComponent'
const App = {
view() {
return m( DynamicComponent, {
component: 'OtherComponent'
}, 'Hello world' ),
}
}
}
m.mount(document.body, App)
OtherComponent.js
import m from 'mithril'
export function OtherComponent() { return {
view({ children }) { return m( 'div', children )}
}}
DynamicComponent.js
import { hooks } from '/hooks'
export function DynamicComponent() { return {
...hooks,
attrs: null,
component: null,
view({ children }) { return (
// Await module resolution and process attributes.
// Use '&&' as a shortcut to only continue
// once 'this.component' isn't null.
// Pass a clone of attrs to the loaded component.
this.component && m( this.component.default, this.attrs, children )
)}
}}
hooks.js
async function oninit({ attrs }) {
// Preload -> Load immediately, in parallel
// Prefetch -> Load when browser is idle (Can be less responsive)
// See more: https://webpack.js.org/guides/code-splitting/#prefetching-preloading-modules
// Dynamically import component and append '.js' (Don't include '.js' in your imports).
if ( attrs.loadType = 'prefetch' ) {
// Lazy load
this.component = await import( /* webpackPrefetch: true */ `
${ attrs.component }.js`
)
} else {
// Immediate load
this.component = await import( /* webpackPreload: true */ `
${ attrs.component }.js`
)
}
/*
Process and pass along attributes
This clones the attributes to prevent any changes from affecting
the original attributes.
You can save memory if it becomes a problem by directly
assigning `v.attrs` to `newAttrs`, but you lose this immutability.
*/
const newAttrs = { ...v.attrs }
// Remove attributes used in `DynamicComponent`
delete newAttrs.component
delete newAttrs.loadType
// Assign to component
this.attrs = newAttrs
m.redraw()
}
export const hooks = {
oninit,
}

How to correctly test effects in ngrx 4?

There are plenty of tutorials how to test effects in ngrx 3.
However, I've found only 1 or 2 for ngrx4 (where they removed the classical approach via EffectsTestingModule ), e.g. the official tutorial
However, in my case their approach doesn't work.
effects.spec.ts (under src/modules/list/store/list in the link below)
describe('addItem$', () => {
it('should return LoadItemsSuccess action for each item', async() => {
const item = makeItem(Faker.random.word);
actions = hot('--a-', { a: new AddItem({ item })});
const expected = cold('--b', { b: new AddUpdateItemSuccess({ item }) });
// comparing marbles
expect(effects.addItem$).toBeObservable(expected);
});
})
effects.ts (under src/modules/list/store/list in the link below)
...
#Effect() addItem$ = this._actions$
.ofType(ADD_ITEM)
.map<AddItem, {item: Item}>(action => {
return action.payload
})
.mergeMap<{item: Item}, Observable<Item>>(payload => {
return Observable.fromPromise(this._listService.add(payload.item))
})
.map<any, AddUpdateItemSuccess>(item => {
return new AddUpdateItemSuccess({
item,
})
});
...
Error
should return LoadItemsSuccess action for each item
Expected $.length = 0 to equal 1.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: AddUpdateItemSuccess({ payload: Object({ item: Object({ title: Function }) }), type: 'ADD_UPDATE_ITEM_SUCCESS' }), error: undefined, hasValue: true }) }).
at compare (webpack:///node_modules/jasmine-marbles/index.js:82:0 <- karma-test-shim.js:159059:33)
at Object.<anonymous> (webpack:///src/modules/list/store/list/effects.spec.ts:58:31 <- karma-test-shim.js:131230:42)
at step (karma-test-shim.js:131170:23)
NOTE: the effects use a service which involves writing to PouchDB. However, the issue doesn't seem related to that
and also the effects work in the running app.
The full code is a Ionic 3 app and be found here (just clone, npm i and npm run test)
UPDATE:
With ReplaySubject it works, but not with hot/cold marbles
const item = makeItem(Faker.random.word);
actions = new ReplaySubject(1) // = Observable + Observer, 1 = buffer size
actions.next(new AddItem({ item }));
effects.addItem$.subscribe(result => {
expect(result).toEqual(new AddUpdateItemSuccess({ item }));
});
My question was answered by #phillipzada at the Github issue I posted.
For anyone checking this out later, I report here the answer:
Looks like this is a RxJS issue when using promises using marbles. https://stackoverflow.com/a/46313743/4148561
I did manage to do a bit of a hack which should work, however, you will need to put a separate test the service is being called unless you can update the service to return an observable instead of a promise.
Essentially what I did was extract the Observable.fromPromise call into its own "internal function" which we can mock to simulate a call to the service, then it looks from there.
This way you can test the internal function _addItem without using marbles.
Effect
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import { Injectable } from '#angular/core';
import { Actions, Effect } from '#ngrx/effects';
import { Action } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
export const ADD_ITEM = 'Add Item';
export const ADD_UPDATE_ITEM_SUCCESS = 'Add Item Success';
export class AddItem implements Action {
type: string = ADD_ITEM;
constructor(public payload: { item: any }) { }
}
export class AddUpdateItemSuccess implements Action {
type: string = ADD_UPDATE_ITEM_SUCCESS;
constructor(public payload: { item: any }) { }
}
export class Item {
}
export class ListingService {
add(item: Item) {
return new Promise((resolve, reject) => { resolve(item); });
}
}
#Injectable()
export class SutEffect {
_addItem(payload: { item: Item }) {
return Observable.fromPromise(this._listService.add(payload.item));
}
#Effect() addItem$ = this._actions$
.ofType<AddItem>(ADD_ITEM)
.map(action => action.payload)
.mergeMap<{ item: Item }, Observable<Item>>(payload => {
return this._addItem(payload).map(item => new AddUpdateItemSuccess({
item,
}));
});
constructor(
private _actions$: Actions,
private _listService: ListingService) {
}
}
Spec
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
import { async, TestBed } from '#angular/core/testing';
import { Actions } from '#ngrx/effects';
import { Store, StoreModule } from '#ngrx/store';
import { getTestActions, TestActions } from 'app/tests/sut.helpers';
import { AddItem, AddUpdateItemSuccess, ListingService, SutEffect } from './sut.effect';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
describe('Effect Tests', () => {
let store: Store<any>;
let storeSpy: jasmine.Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({})
],
providers: [
SutEffect,
{
provide: ListingService,
useValue: jasmine.createSpyObj('ListingService', ['add'])
},
{
provide: Actions,
useFactory: getTestActions
}
]
});
store = TestBed.get(Store);
storeSpy = spyOn(store, 'dispatch').and.callThrough();
storeSpy = spyOn(store, 'select').and.callThrough();
}));
function setup() {
return {
effects: TestBed.get(SutEffect) as SutEffect,
listingService: TestBed.get(ListingService) as jasmine.SpyObj<ListingService>,
actions$: TestBed.get(Actions) as TestActions
};
}
fdescribe('addItem$', () => {
it('should return LoadItemsSuccess action for each item', async () => {
const { effects, listingService, actions$ } = setup();
const action = new AddItem({ item: 'test' });
const completion = new AddUpdateItemSuccess({ item: 'test' });
// mock this function which we can test later on, due to the promise issue
spyOn(effects, '_addItem').and.returnValue(Observable.of('test'));
actions$.stream = hot('-a|', { a: action });
const expected = cold('-b|', { b: completion });
expect(effects.addItem$).toBeObservable(expected);
expect(effects._addItem).toHaveBeenCalled();
});
})
})
Helpers
import { Actions } from '#ngrx/effects';
import { Observable } from 'rxjs/Observable';
import { empty } from 'rxjs/observable/empty';
export class TestActions extends Actions {
constructor() {
super(empty());
}
set stream(source: Observable<any>) {
this.source = source;
}
}
export function getTestActions() {
return new TestActions();
}