Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect for typescript and use that install profile for jest / ava / etc. #48

Closed
nothingismagick opened this issue Feb 21, 2019 · 5 comments · Fixed by #117
Closed

Detect for typescript and use that install profile for jest / ava / etc. #48

nothingismagick opened this issue Feb 21, 2019 · 5 comments · Fixed by #117

Comments

@nothingismagick
Copy link
Contributor

We will have to use dynamic dependencies here - which I don't like because that makes them "invisible" to real dep auditing.

Proposal: helper packages for extensions

@IlCallo
Copy link
Member

IlCallo commented Jul 2, 2019

As said on Discord channel, I'll write here updates needed to make Jest work together with Typescript.
Many updates can be already done now by default even when TS isn't set, because they doesn't change anything for JS users.


New Jest configuration

jest.setup.js extension should be changed to .ts.

jest.config.js

module.exports = {
  globals: {
    __DEV__: true,
  },
  setupFilesAfterEnv: ['<rootDir>/test/jest/jest.setup.ts'], // <== was a JS file, now is TS
  // noStackTrace: true,
  // bail: true,
  // cache: false,
  // verbose: true,
  // watch: true,
  collectCoverage: true,
  coverageDirectory: '<rootDir>/test/jest/coverage',
  collectCoverageFrom: [
    '<rootDir>/src/**/*.vue',
    '<rootDir>/src/**/*.js',
    '<rootDir>/src/**/*.ts',
    '<rootDir>/src/**/*.jsx',
  ],
  coverageThreshold: {
    global: {
      //  branches: 50,
      //  functions: 50,
      //  lines: 50,
      //  statements: 50
    },
  },
  testMatch: [
    // Matches tests in any subfolder of 'src' or into 'test/jest/__tests__'
    // Matches all files with extension 'js', 'jsx', 'ts' and 'tsx'
    '<rootDir>/test/jest/__tests__/**/*.(spec|test).+(ts|js)?(x)',
    '<rootDir>/src/**/__tests__/*_jest.(spec|test).+(ts|js)?(x)',
  ],
  moduleFileExtensions: ['vue', 'js', 'jsx', 'json', 'ts', 'tsx'],
  moduleNameMapper: {
    '^vue$': '<rootDir>/node_modules/vue/dist/vue.common.js',
    '^test-utils$':
      '<rootDir>/node_modules/@vue/test-utils/dist/vue-test-utils.js',
    '^quasar$': '<rootDir>/node_modules/quasar/dist/quasar.common.js',
    '^~/(.*)$': '<rootDir>/$1',
    '^src/(.*)$': '<rootDir>/src/$1',
    '.*css$': '<rootDir>/test/jest/utils/stub.css',
  },
  transform: {
    '^.+\\.(ts|js|html)$': 'ts-jest',
    '.*\\.vue$': 'vue-jest',
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
      'jest-transform-stub',
  },
  transformIgnorePatterns: [
    '<rootDir>/node_modules/(?!quasar/lang)',
  ],
  snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
};

New TypeScript configuration

tsconfig.json

{
  "compilerOptions": {
    "allowJs": true,
    "sourceMap": true,
    "target": "es6",
    "strict": true,
    "experimentalDecorators": true,
    "module": "esnext",
    "moduleResolution": "node",
    "baseUrl": "."
  },
  "exclude": ["node_modules"]
}

allowJs: true must be added or components mount won't work, even if I didn't understand exactly why. It is something related to ts-jest.

There are two options to extend TS capabilities to test folder (and everywhere else there could be a TS file):

  • remove "include": [ "./src/**/*" ] and use "exclude": ["node_modules"] (the one I show in the code above). This is needed to make Typescript recognize test files with .ts and .tsx extensions everywhere, excluding node_modules. This is the default configuration chosen by Angular for its projects.
  • if the first options slows down the system too much, include should be used, but adding every folder where TS files can be used. Initially changing it like "include": [ "./src/**/*", "./test/**/*" ]. This is less maintenable.

Demo component

Currently the only way to get typings into tests is to move from a SFC to a DFC (Double File Component 😁), which means that TS script content should be extracted in a TS file in the same folder of the SFC one and called the same, then referenced by the script tag into the SFC.

QBtn-demo.ts

import Vue from 'vue';

export default Vue.extend({ // <= MUST extend Vue instance
  name: 'QBUTTON',
  data: function(): { counter: number; input: string } { // <= data MUST have a return type or TS won't be able to correctly infer its content on `this` context later on
    return {
      counter: 0,
      input: 'rocket muffin',
    };
  },
  methods: {
    increment(): void { // <= methods return type MUST be annotated too
      this.counter++;
    },
  },
});

QBtn-demo.vue

<script lang="ts" src="./QBtn-demo.ts"></script>
<template>
  <div>
    <p class="textContent">{{ input }}</p>
    <span>{{ counter }}</span>
    <q-btn id="mybutton" @click="increment()"></q-btn>
  </div>
</template>

Demo test

app.spec.js should be renamed to app.spec.ts.

I removed initial JSDoc comments:

  • eslint-disable won't be needed anymore once ESLint setup for Jest #99 is fixed because jest-specific linting will work out-of-the-box
  • @jest-environment jsdom is redundant: jsdom is the default option and I don't see default overrides anywhere

app.spec.ts

import { createLocalVue, mount } from '@vue/test-utils';  // <== shallowMount removed because not used
import * as All from 'quasar';
import { VueConstructor } from 'vue';

import QBtnDemo from './demo/QBtn-demo';
// import langIt from 'quasar/lang/it' // change to any language you wish! => this breaks wallaby :(

const { Quasar, date } = All;

function isComponent(value: any): value is VueConstructor {
  return value && value.component && value.component.name != null;
}

const components = Object.keys(All).reduce<{[index: string]: VueConstructor}>((object, key) => {
  const val = (All as any)[key];
  if (isComponent(val)) {
    object[key] = val;
  }
  return object;
}, {});

describe('Mount Quasar', () => {
  const localVue = createLocalVue();
  localVue.use(Quasar, { components });
  // localVue.use(Quasar, { components, lang: langEn }); <= specify a language different from the default

  const wrapper = mount(QBtnDemo, { localVue });
  const vm = wrapper.vm;

  it('stuff', () => {
    // ... stuff
  });
});

It think it's appropriated to also add a new file with more down to earth tests, because you probably want to show the component into more isolated tests (aka, where you load only the bare minimum Quasar components to make it work) and show the usage of shallowMount.

QBtn-demo.spec.ts

import { createLocalVue, shallowMount } from '@vue/test-utils';
import { Quasar, QBtn } from 'quasar'; // <= cherry pick only the components you actually use
import { VueConstructor } from 'vue';

import QBtnDemo from './demo/QBtn-demo';

const localVue = createLocalVue();
localVue.use(Quasar, { components: { QBtn } }); // <= you should register every component you use. If not declared here, `shallowMount` won't be able to stub them

const factory = (propsData: any = {}) => {
  return shallowMount(QBtnDemo, { // <= used `shallowMount` instead of `mount`, will stub all **registered** components into the template
    localVue,
    propsData,
  });
};

describe('QBtnDemo', () => {
  test('is a Vue instance', () => {
    const wrapper = factory(); // <= when no props are not needed
    // const wrapper = factory({ propName: propValue }); <= when props are needed
    expect(wrapper.isVueInstance()).toBeTruthy();
  });
});

If you are like me, you're probably asking yourself "is this black magic? How can that TS work for mounting? It doesn't know anything about the template and style data!"
And you're right. See vuejs/vue-jest#188 for a possible explanation.


TODO

Transpile libraries exported with ES6 syntax using babel-jest (es. lodash-es)

I gave up after some hours of trial and error (mostly error and error actually).
I found a fixer which whitelists some particular modules by name back when I worked with Angular, but everything I tried now on Vue/Quasar world doesn't seem to work.
That workaround required to use babel.config.js file, otherwise node_modules could not be transformed.
It's not entirely clear to me which one is using now Quasar, because the project contains both a babel.config.js and a .babelrc (which is then imported into the first one) without comments about why is it so. An explanation on this point would be interesting.

Fixer

jest.config.js

// Array containing modules to transpile with Babel
// See https://github.com/nrwl/nx/issues/812#issuecomment-429488470
const esModules = ['lodash-es'].join('|');
module.exports = {
  // ...
  transform: {
    // See https://jestjs.io/docs/en/configuration.html#transformignorepatterns-array-string
    [`^(${esModules}).+\\.js$`]: 'babel-jest',
    // ...
  },
  transformIgnorePatterns: [
    // ...
    `<rootDir>/node_modules/(?!(${esModules}))`,
  ],
  // ...
};

For lodash-es it's possible to stub it using moduleNameMapper and map it to its non-ES6 counterpart, but it won't work with other libraries.
Reference

jest.config.js

moduleNameMapper: {
  // ...
  "^lodash-es$": "lodash",
}

Related 1 2 3 4 5

Broken automatic import for components into Vue.use(Quasar, { components: { QBtn, ... } })

Into VSCode, using intellisense autocomplete for components doesn't automatically import those same components, but just autocompletes the corresponding object key. No idea how to solve this tho, using an array with a union type composed of all components instead of the current typed object would allow to insert multiple times the same component and would require a change current implementation.

Props in factory function not automatically inferred

It would be super-good to make factory function to automatically infer props from the component (and I guess it's possible to do so via typeof stuff or similar) but I could not find a way to do so.

const factory = (propsData: any /* should be automatically typed! */ = {}) => {
  return shallowMount(QBtnDemo, {
    localVue,
    propsData,
  });
};

Then it would be possible to create a reusable util function like the ones into Angular Spectator, taking as input localVue and the component, and returning the factory.


Any help in proceeding further is appreciated.

@outofmemoryagain
Copy link

outofmemoryagain commented Jul 12, 2019

@IlCallo one thing that we did when adding support for typescript in the main cli was to not include the extension when reference the files for import. If you use node module loading strategy it should import the file without a need for the extension to be specified, so setupFilesAfterEnv: ['<rootDir>/test/jest/jest.setup']. I'm not 100% sure of the context because I haven't used the extension, but it may help make the code extension code more generic. Just a though as I read through your issue comments. Ignore this comment if it is completely out of context and not relevant at all....

@IlCallo
Copy link
Member

IlCallo commented Jul 12, 2019

Seems like removing the extension will work for JS-version, but will break when using a TS file.
That's probably because Jest actually runs on JS and therefore follows all JS conventions (so it searches a .js file when the extension is not specified).

I'll look better into it after I fix linting stuff

@IlCallo
Copy link
Member

IlCallo commented Aug 19, 2019

Additional updates after typescript-eslint v2 has been released

unbound-method false positive
Into app.spec.ts

  it('has a created hook', () => {
    // False positive
    // See https://github.com/typescript-eslint/typescript-eslint/issues/692
    // eslint-disable-next-line @typescript-eslint/unbound-method
    expect(typeof vm.increment).toBe('function');
  });

Use native startWith instead of RegExp
Into test > cypress > support > index.js use this version

const resizeObserverLoopError = 'ResizeObserver loop limit exceeded';

Cypress.on('uncaught:exception', err => {
  if (err.message.startsWith(resizeObserverLoopError)) {
    // returning false here prevents Cypress from
    // failing the test
    return false;
  }
});

Use const instead of let
Into lighthouse-runner.js, Performance is defined as let but never re-assigned. Can be changed to const

Avoid async for sync methods
jest-loader.js returns an async function, even if all operations are synchronous.
It should be removed.

@rulrok
Copy link

rulrok commented Apr 16, 2020

I'm using quasar/typescript and followed this guide to make the project work.

Just a note here about importing ES6 modules in case it helps someone, and if someone can also help me understand what I did 😆

On my code I needed to import this quasar utils code:

import { testPattern } from 'quasar/src/utils/patterns';

and jest as failing with

export const testPattern = {
    ^^^^^^

    SyntaxError: Unexpected token 'export'

Strictly using the examples provided here wasn't helping me, so I updated jest.config.js more or less alike the examples here, but changing the transform rules.

I'm using babel-jest for js and html. I guess it is correct since the project doesn't have any other html files besides the generated template. As .vue files are transformed with vue-jest and it already takes care of typescript afaik, I guess my configuration is correct on using ts-jest only for .tsx? files.

const esModules = ['quasar/lang', 'quasar/src/utils'].join('|');
module.exports = {
  globals: {
    __DEV__: true
  },
...
  transform: {
    '^.+\\.(js|html)$': 'babel-jest',
    '^.+\\.tsx?$': 'ts-jest',
    '.*\\.vue$': 'vue-jest',
    '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
      'jest-transform-stub'
    // use these if NPM is being flaky
    // '.*\\.vue$': '<rootDir>/node_modules/@quasar/quasar-app-extension-testing-unit-jest/node_modules/vue-jest',
    // '.*\\.js$': '<rootDir>/node_modules/@quasar/quasar-app-extension-testing-unit-jest/node_modules/babel-jest'
  },
  transformIgnorePatterns: [`<rootDir>/node_modules/(?!(${esModules}))`],
...
};

Now everything is working 👍
My vue smoking-code tests are still working as well so that's a good sign for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants