quasarframework / quasar-testing

Testing Harness App Extensions for the Quasar Framework 2.0+
https://testing.quasar.dev
MIT License
179 stars 66 forks source link

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

Closed nothingismagick closed 4 years ago

nothingismagick commented 5 years ago

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 commented 5 years ago

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` ```js module.exports = { globals: { __DEV__: true, }, setupFilesAfterEnv: ['/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: '/test/jest/coverage', collectCoverageFrom: [ '/src/**/*.vue', '/src/**/*.js', '/src/**/*.ts', '/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' '/test/jest/__tests__/**/*.(spec|test).+(ts|js)?(x)', '/src/**/__tests__/*_jest.(spec|test).+(ts|js)?(x)', ], moduleFileExtensions: ['vue', 'js', 'jsx', 'json', 'ts', 'tsx'], moduleNameMapper: { '^vue$': '/node_modules/vue/dist/vue.common.js', '^test-utils$': '/node_modules/@vue/test-utils/dist/vue-test-utils.js', '^quasar$': '/node_modules/quasar/dist/quasar.common.js', '^~/(.*)$': '/$1', '^src/(.*)$': '/src/$1', '.*css$': '/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: [ '/node_modules/(?!quasar/lang)', ], snapshotSerializers: ['/node_modules/jest-serializer-vue'], }; ```

New TypeScript configuration `tsconfig.json` ```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` ```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` ```vue ```

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 #99 is fixed because jest-specific linting will work out-of-the-box - `@jest-environment jsdom` is redundant: `jsdom` is the [default option](https://jestjs.io/docs/en/configuration#testenvironment-string) and I don't see default overrides anywhere `app.spec.ts` ```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` ```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 https://github.com/vuejs/vue-jest/issues/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` ```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: [ // ... `/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 commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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 :laughing:

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 :+1: My vue smoking-code tests are still working as well so that's a good sign for me.