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

Unable to use services exported under a namespace w/ Jest 27 + Ng12 #963

Open
AgentEnder opened this issue Jun 24, 2021 · 39 comments
Open

Comments

@AgentEnder
Copy link

🐛 Bug Report

Attempting to test components that inject a service imported from a namespace fails in Jest 27 / Angular 12.

To Reproduce

  1. ng new my-app
  2. Install jest / jest-preset-angular
  3. Create a new folder, services.
  4. Create a file in that folder (my-service.ts) containing the following:
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class MyService {
    constructor() {
        console.log('HELLO')
    }
}
  1. Create a new file (index.ts) inside the services folder containing the following:
import * as Services from './my-service';

export { Services }
  1. Add private myService: Services.MyService to the constructor of app-component
  2. Import Services in app-component.
  3. Try to run tests with npx jest

Expected behavior

Tests run successfully

Link to repo (highly encouraged)

https://github.com/AgentEnder/ng-jest-issue-6097

Error log:

 Can't resolve all parameters for AppComponent: (?).

      at syntaxError (../packages/compiler/src/util.ts:108:17)
      at CompileMetadataResolver.Object.<anonymous>.CompileMetadataResolver._getDependenciesMetadata (../packages/compiler/src/metadata_resolver.ts:1010:27)
      at CompileMetadataResolver.Object.<anonymous>.CompileMetadataResolver._getTypeMetadata (../packages/compiler/src/metadata_resolver.ts:889:20)
      at CompileMetadataResolver.Object.<anonymous>.CompileMetadataResolver.getNonNormalizedDirectiveMetadata (../packages/compiler/src/metadata_resolver.ts:387:18)
      at CompileMetadataResolver.Object.<anonymous>.CompileMetadataResolver.loadDirectiveMetadata (../packages/compiler/src/metadata_resolver.ts:238:41)
      at ../packages/compiler/src/jit/compiler.ts:137:36
          at Array.forEach (<anonymous>)
      at ../packages/compiler/src/jit/compiler.ts:135:65
          at Array.forEach (<anonymous>)
      at JitCompiler.Object.<anonymous>.JitCompiler._loadModules (../packages/compiler/src/jit/compiler.ts:132:71)
      at JitCompiler.Object.<anonymous>.JitCompiler._compileModuleAndAllComponents (../packages/compiler/src/jit/compiler.ts:117:32)
      at JitCompiler.Object.<anonymous>.JitCompiler.compileModuleAndAllComponentsAsync (../packages/compiler/src/jit/compiler.ts:69:33)
      at CompilerImpl.Object.<anonymous>.CompilerImpl.compileModuleAndAllComponentsAsync (../packages/platform-browser-dynamic/src/compiler_factory.ts:69:27)
      at TestingCompilerImpl.Object.<anonymous>.TestingCompilerImpl.compileModuleAndAllComponentsAsync (../packages/platform-browser-dynamic/testing/src/compiler_factory.ts:59:27)
      at TestBedViewEngine.Object.<anonymous>.TestBedViewEngine.compileComponents (../packages/core/testing/src/test_bed.ts:366:27)
      at Function.Object.<anonymous>.TestBedViewEngine.compileComponents (../packages/core/testing/src/test_bed.ts:155:25)
      at src/app/app.component.spec.ts:10:8
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:407:30)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:3765:43)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:406:56)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:167:47)
      at Object.wrappedFunc (node_modules/zone.js/bundles/zone-testing-bundle.umd.js:4250:34)

envinfo

System:
    OS: Ubuntu (tested under wsl2)

Npm packages:
    jest: 27.0.5
    jest-preset-angular: 9.0.4
    typescript: 4.2.3
@wtho
Copy link
Collaborator

wtho commented Jun 25, 2021

Does the same setup work in Angular v11 with same jest/ts-jest/jest-preset-angular versions?

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jun 25, 2021

I debugged and saw that the compiled output of AppComponent contains undefined as ctor parameter instead of referencing the DemoService, which causes the issue. The problem could be that TypeScript LanguageService couldn't resolve the import from export namespace.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Jun 25, 2021

The bug should occur to Angular 11 too as we use the same transformer.

One unknown thing is why Karma + Jasmine works. The most suspicious point would be module resolution doesn’t work correctly which makes LanguageService not able to find the information of the file.

Workaround

For now pls avoid using export/import namespace but following what Angular library does, e.g.

export { something } from ‘a-path’

@ahnpnl ahnpnl added 🐛 Bug Confirmed Bug is confirmed and removed Bug Report Needs Repo Need a minimium repository to reproduce the problem Needs Triage labels Jun 25, 2021
@Maximaximum
Copy link

I'm facing exactly the same issue! (And it took me a few days to trace it down!).

The issue only happens with Jest (not with Jasmine/Karma) and only if using namespace imports, ie import * as Exported from './exported';.

In my case the issue is that I'm using some code generated by a 3rd party code generator that contains namespace imports, and there's no way to tweak the code generator's behavior

@Maximaximum
Copy link

And btw, if you run npx ngcc, the error message (Can't resolve all parameters for AppComponent: (?).) gets replaced with

This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid.
    This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.

    Please check that 1) the type for the parameter at index 0 is correct and 2) the correct Angular decorators are defined for this class and its ancestors.

And I'm using Angular 12, not sure if same happens with Angular 11

@Maximaximum
Copy link

Here's a simplified reproduction repo: https://github.com/Maximaximum/jest-angular-namespace-import-bug

@Maximaximum
Copy link

I've just checked and the issue is reproducible with Angular v11 as well.

@thymikee @ahnpnl @wtho Is there anything I could do to help resolve this issue?

The suggested workaround is not usable for me, so this bug is blocking me from adding unit tests to my app. If this can't be fixed soon, I'll probably have to switch back to Jasmine+Karma.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Aug 26, 2021

the only workaround is don't use import namespace for now but you should import directly from the file as well as avoiding barrel file because that won't work unfortunately.

We don't reuse the way how Angular CLI compiles codes therefore some efforts need to check.

One thing I haven't tested is: import namespace into a dummy file and rexport whatever comes from that namespace to import into component. The error occurs because import namespace is used directly in a file which contains Angular decorators.

@wtho
Copy link
Collaborator

wtho commented Aug 26, 2021

@ahnpnl do you know why this importing is a problem? Is it related to ts-jest, jest or node?

@ahnpnl
Copy link
Collaborator

ahnpnl commented Aug 26, 2021

It is ts-jest problem as well as architecture problem. The error is caused by downlevel ctor transformer that it can’t resolve the injected dependencies which it modifies wrongly the AST.

I think this might be fixed if the LanguageService has the dependencies information to provide to the downlevel ctor transformer. However, this is still a problem with isolatedModules: true though because that mode does simple transpilation from ts to js.

The ideal way is: we follow completely the way like Angular CLI does. We would need to have a single place where the compilation is done, not at Jest transformer level.

@Maximaximum
Copy link

@ahnpnl As I have mentioned above, in some cases the workaround is not an option at all

@ahnpnl
Copy link
Collaborator

ahnpnl commented Aug 27, 2021

There is still one more workaround is: use ngc to compile everything in your project and point Jest to run on the output folder, similar to the approach of using tsc to compile everything and run Jest on output folder. For watch mode, that might not work.

Unfortunately we don't have a quick fix now so those 2 workarounds are the ones I can think of.

@pheinicke
Copy link

Hi,
I have the exact same issue as @Maximaximum. That is Angular 12, Jest and code generated by a third party tool, containing namespace imports.
@ahnpnl How would you tell Jest to run on the compiled files?

Thanks for the help!

@ahnpnl
Copy link
Collaborator

ahnpnl commented Aug 31, 2021

You can configure where Jest should look for the files by using testMatch, testRegex, rootDir. I think mainly rootDir, see https://jestjs.io/docs/configuration#rootdir-string

You have to use ngc to compile, don’t use tsc

@Maximaximum
Copy link

Fwiw, in the title of this issue "exported" should be replaced with "imported", because it's not the export syntax, but the import syntax that causes the issue

@Maximaximum
Copy link

FWIW I'm now trying to use the 2nd workaround suggested by @ahnpnl. But what drastically complicates things even more is that I'm having an nx workspace with about 20 different nx projects, instead of just a single Angular project. There doesn't seem to be a way to run ngc for every nx project automatically. And even if there was, I'm not sure where does the ngc output go, and how to make sure that jest is run against the built files, not against the sources.

Looks like I'm stuck with adding any unit tests to my project right now. Neither applying a workaround for jest, nor reverting back to using karma seems to be an easy thing to do.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Sep 10, 2021

ngc can be configured but you would need to find documentation online.

@Maximaximum
Copy link

I can't find documentation about configuring the output folder here https://angular.io/guide/angular-compiler-options. Does ng build do the same thing as ngc? Should we run jest against the bundled files located in the dist folder, produced by ng build?

@ahnpnl
Copy link
Collaborator

ahnpnl commented Sep 10, 2021

IIRC ng build does similar thing like ngc. ngc is a replacement of tsc which does some Angular things extra.

After producing build outputs, you would need to configure Jest to run on output folder yes.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 17, 2021

Hi all, I found an easy workaround for this issue. You can configure Jest moduleNameMapper to instruct Jest to load the correct module. With the example repo from @AgentEnder, the configuration will be

// jest.config.js
module.exports = {
   moduleNameMapper: {
       './services$': '<rootDir>/src/app/services/demo-service.ts'
   }
}

There are 2 more possible workarounds:

// jest.config.js
module.exports = {
   globals: {
    'ts-jest': {
      astTransformers: {
        before: ['ts-jest/dist/transformers/path-mapping']
      }
    }
  },
}

@Maximaximum
Copy link

@ahnpnl I don't know about the exact use cases of the other folks here, but in my specific case I have dozens of auto-generated files containing namespaced imports. Adding an entry to jest.config.js for each of these imports is definitely not an option, because I'm having an nx workspace with about 20 different angular projects, and each of them having its own jest.config.js. Managing all these moduleNameMapper settings in all of the jest.config.js would be a nightmare!

Creating a custom resolver might be an option, but I'm totally new to Jest, so it might be quite an overwhelming task for me.

As per using path-mapping transformer, it looks like it should be relatively easy to do though. Will give it a try, thank you.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 20, 2021

ye the downside of moduleNameMapper is developers need to create an "ultimate" RegEx pattern to capture all scenarios which are not too ideal.

About custom Jest resolver, you can check https://github.com/nrwl/nx/blob/master/packages/jest/plugins/resolver.ts

In general, it's about module resolution in Jest is different from the way how webpack and Angular internal do.

@Maximaximum
Copy link

@ahnpnl I'm currently trying to implement a custom Jest resolver, but it seems like a customer resolver won't be able to fix the issue.

Let's take a import * as Apollo from 'apollo-angular'; line as an example.

As far as I can see, a Jest resolver only deals with resolving import paths like apollo-angular to actual absolute file paths in the filesystem (like /workspaces/my-project/frontend/node_modules/apollo-angular/bundles/ngApollo.umd.js). But it has nothing to do with handling the * as Apollo part. The resolver doesn't even get the * as Apollo (or anything like { gql } or someDefaultExport) part as an argument, it has no idea about what values are actually being imported from an es6 module.

@Maximaximum
Copy link

And I'm not entirely sure, but it looks like the path-mapping AST transformer has nothing to do with imported values neither, it's just dealing with paths.

Looks like there has been a misunderstanding here? The issue is caused whenever a namespaced import is used, like import * as Apollo from 'apollo-angular';. The import { gql } from 'apollo-angular'; syntax does not cause any issues. So it's not about path resolution, it's about resolving the values imported by one file to the values exported by another file.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 21, 2021

It’s about module resolution happens in Jest and partially related to how ts is compiled to js with ts-jest.

When compiling, the import namespace is converted into js which Jest will read and perform module resolution to load the necessary files.

With Angular compiler, they alter AST which will modify the import namespace to the precise import file. That is not the case here when we use ts-jest which uses simple TypeScript compiler, no magic like Angular.

So, the 2 suggestions:

  • custom Jest resolver: this will make Jest load the correct file for import namespace. Idk how the logic should be. Basically custom resolver will tell Jest “hey this namespace import should be resolved at precise import”. Custom resolver is a generic way to handle module resolution when moduleNameMapper becomes too complex to configure.

  • Custom AST transformer and put to ts-jest config to do magic like Angular compiler does. This is similar to path-mapping transformer.

Ideal solution: use Angular compiler to compile all codes before running Jest. We want to go for this ofc, but will need some time to investigate how it would play well with Jest architecture.

@Maximaximum
Copy link

I'm sorry @ahnpnl but I still genuinely don't get it regarding a custom Jest resolver.

Considering import * as Apollo from 'apollo-angular';, the default resolver already properly resolves the apollo-angular path to /workspaces/my-project/frontend/node_modules/apollo-angular/bundles/ngApollo.umd.js path. The resolved path is correct, so there's nothing we can do in the resolver to fix the issue. Am I wrong here?

@ahnpnl
Copy link
Collaborator

ahnpnl commented Oct 21, 2021

If that is the case, only modify AST is the only choice left, or using the ideal solution. The resolver solution won't work all the time, especially in the case you import a compiled js like apollo-angular.

2 suggestions are just workarounds, won't fit for all scenarios.

@Maximaximum
Copy link

I'm trying to write an ast trasnformer that would convert namespace imports to named imports. Actually, there's a refactoring for Typescript that does exactly that: https://github.com/microsoft/TypeScript/pull/24469/files But I can't find any documentation on how to run the Typescript refactors programmatically. Any ideas?

@Maximaximum
Copy link

Maximaximum commented Nov 1, 2021

I was finally able to create a workaround that seems to solve the issue (at least for me) and lets angular+jest unit tests with namespace imports actually run: https://www.npmjs.com/package/jest-namespace-imports-transformer

I still hope that the jest-preset-angular team is going to address this issue within jest-preset-angular itself so that my workaround (which might be quite buggy) won't be needed anymore.

@ahnpnl
Copy link
Collaborator

ahnpnl commented Nov 1, 2021

I guess all the logic to desugar namespace syntax lies here https://github.com/Maximaximum/jest-namespace-imports-transformer/blob/main/src/transform-script.ts#L88 ? We are happy to add it as a custom AST transformer to internal codes

@Maximaximum
Copy link

@ahnpnl Yes, that would be great! I can't guarantee my workaround works nicely for all cases and scenarios (I'm a total newbie with regards to Typescript compiler API, jest and jest-preset-angular, so I have easily messed up something), but so far so good: it works for me.

@Nielsb85
Copy link

Nielsb85 commented Nov 17, 2021

@ahnpnl Yes, that would be great! I can't guarantee my workaround works nicely for all cases and scenarios (I'm a total newbie with regards to Typescript compiler API, jest and jest-preset-angular, so I have easily messed up something), but so far so good: it works for me.

For me , when adding the transformer:


 ● Test suite failed to run

    Cannot find module './jest-transformer'
    Require stack:
    - /client/node_modules/jest-namespace-imports-transformer/dist/index.js
    - /client/node_modules/@jest/core/node_modules/jest-util/build/requireOrImportModule.js
    - /client/node_modules/@jest/core/node_modules/jest-util/build/index.js
    - /client/node_modules/@jest/core/build/FailedTestsInteractiveMode.js
    - /client/node_modules/@jest/core/build/plugins/FailedTestsInteractive.js
    - /client/node_modules/@jest/core/build/watch.js
    - /client/node_modules/@jest/core/build/cli/index.js
    - /client/node_modules/@jest/core/build/jest.js
    - /client/node_modules/jest/node_modules/jest-cli/build/cli/index.js
    - /client/node_modules/jest/node_modules/jest-cli/bin/jest.js
    - /client/node_modules/jest/bin/jest.js

      at Object.<anonymous> (node_modules/jest-namespace-imports-transformer/dist/index.js:16:44)

I did however, migrate to angular 13

@ahnpnl
Copy link
Collaborator

ahnpnl commented Nov 25, 2021

@Maximaximum I just found a new workaround that you can adjust your tsconfig.spec.json to have

{
   //...
  "include": ["src/**/*.ts"]
}

at least it fixed the issue with the sample repo.

The problem I think is similar to #1199 is that: Angular doesn't support transpile ts to js in "isolated way". Angular always requires one single TypeScript Program (see angular/angular#43165) to process all the files together while here with Jest, we split them up into multiple workers. Compilation per worker is not the same as using one single Program.

@Maximaximum
Copy link

@ahnpnl Any news regarding properly fixing this issue within jest-preset-angular?

@DaSchTour
Copy link

@Maximaximum I just found a new workaround that you can adjust your tsconfig.spec.json to have

{
   //...
  "include": ["src/**/*.ts"]
}

at least it fixed the issue with the sample repo.

The problem I think is similar to #1199 is that: Angular doesn't support transpile ts to js in "isolated way". Angular always requires one single TypeScript Program (see angular/angular#43165) to process all the files together while here with Jest, we split them up into multiple workers. Compilation per worker is not the same as using one single Program.

This doesn't seam to work with nx repos and graphql codegen 😞

@bvklingeren
Copy link

I am using generated code from apollo-angular like @Maximaximum and was able to fix the issue with an ngcc run and the replacement of

{
   //...
  "include": ["**/*.spec.ts", "**/*.d.ts"]
}

with

{
   //...
  "include": ["**/*.ts"]
}

in tsconfig.spec.json

Currently using [email protected]

@ibanjo
Copy link

ibanjo commented Aug 5, 2022

I am using generated code from apollo-angular like @Maximaximum and was able to fix the issue with an ngcc run and the replacement of

{
   //...
  "include": ["**/*.spec.ts", "**/*.d.ts"]
}

with

{
   //...
  "include": ["**/*.ts"]
}

in tsconfig.spec.json

Currently using [email protected]

This workaround works for me, with the (possibly trivial) caveat that, if you're importing namespaced symbols from another library in your same workspace (e.g. when using Nx Workspaces), the tsconfig.spec.json to be patched is that in the consumer library (no need to touch the exporting lib as well).

@devmanbr
Copy link

any solution for this? the solutions found here did not work for me.

@jorgevds
Copy link

jorgevds commented Sep 7, 2023

Hey friends, chiming in real quick to say that the change to tsconfig.spec.json:

"include": ["src/**/*.ts"]

solved 1/3 of my issue. Changing my service and spec file name (yes, really) from:

hyphenated-name.sandbox.spec.ts

to

hyphenated-name-sandbox.service.spec.ts

fixed another third of my issue.

After that, I started getting more specific Apollo errors, about "invariant" something this and "no provider for _Apollo" that. I solved those by including a defaultOptions object into my Apollo client setup that I use exclusively for unit tests, where before I only used the link and cache object keys. This is essentially a module (read: via static method) that sets the APOLLO_OPTIONS token with some fake values to be used in test.

Finally, optionally, for anyone using Nx and MSW who may be overlooking this, make sure your library's project.json test configuration has your mockServiceWorker.js file in its assets array, and that you setup your MSW server inside of your test-setup.ts file within your library's src folder.

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

Successfully merging a pull request may close this issue.