8

I'm trying to test components built with Angular Material, however I'm encountering a problem initializing Material elements using Harness Loader as per documentation (section 'Getting started'.). I'd like to extract the logic of initializing them outside of the test methods to make them more concise, but it doesn't seem to work.

Within describe():

let usernameFormField: MatFormFieldHarness;
let registrationButton: MatButtonHarness;

beforeEach(async(() => {
    TestBed.configureTestingModule({
        imports: [MaterialModule, BrowserAnimationsModule, ReactiveFormsModule],
        declarations: [RegistrationComponent],
        providers: [ /*provide spies */ ]
    }).compileComponents().then(async () => {
        fixture = TestBed.createComponent(RegistrationComponent);
        loader = TestbedHarnessEnvironment.loader(fixture);

        // what works, but I don't like
        /*loader.getHarness(
            MatFormFieldHarness.with({selector: '#username-form-field'})
        ).then(harness => {
            usernameFormField = harness;
        });*/

        // what doesn't work
        usernameFormField = await loader
            .getHarness(MatFormFieldHarness.with({selector: '#username-form-field'}))

        // other form elements

        // to my confusion, this works without any problem
        registrationButton = await loader.getHarness(MatButtonHarness);
    });
}));

The await on loader.getHarness() causes lots of errors, seemingly about code not running in 'ProxyZone'.

context.js:265 Unhandled Promise rejection: Expected to be running in 'ProxyZone', but it was not found. ; Zone: <root> ; Task: Promise.then ; Value: Error: Expected to be running in 'ProxyZone', but it was not found.
    at Function.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.assertPresent (zone-testing.js:210) [<root>]
    at Function.setup (testbed.js:61) [<root>]
    at new TestbedHarnessEnvironment (testbed.js:572) [<root>]
    at TestbedHarnessEnvironment.createEnvironment (testbed.js:633) [<root>]
    at TestbedHarnessEnvironment.createComponentHarness (testing.js:341) [<root>]
    at TestbedHarnessEnvironment.<anonymous> (testing.js:384) [<root>]
    at Generator.next (<anonymous>) [<root>]
    at :9876/_karma_webpack_/node_modules/tslib/tslib.es6.js:74:1 [<root>]
    at new ZoneAwarePromise (zone-evergreen.js:960) [<root>]
    at __awaiter (tslib.es6.js:70) [<root>]
    at TestbedHarnessEnvironment._getQueryResultForElement (testing.js:379) [<root>]
    at :9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing.js:366:1 [<root>]
    at Array.map (<anonymous>) [<root>]
    at TestbedHarnessEnvironment.<anonymous> (testing.js:366) [<root>] Error: Expected to be running in 'ProxyZone', but it was not found.
    at Function.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.assertPresent (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-testing.js:210:1) [<root>]
    at Function.setup (http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing/testbed.js:61:1) [<root>]
    at new TestbedHarnessEnvironment (http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing/testbed.js:572:1) [<root>]
    at TestbedHarnessEnvironment.createEnvironment (http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing/testbed.js:633:1) [<root>]
    at TestbedHarnessEnvironment.createComponentHarness (http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing.js:341:1) [<root>]
    at TestbedHarnessEnvironment.<anonymous> (http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing.js:384:1) [<root>]
    at Generator.next (<anonymous>) [<root>]
    at http://localhost:9876/_karma_webpack_/node_modules/tslib/tslib.es6.js:74:1 [<root>]
    at new ZoneAwarePromise (http://localhost:9876/_karma_webpack_/node_modules/zone.js/dist/zone-evergreen.js:960:1) [<root>]
    at __awaiter (http://localhost:9876/_karma_webpack_/node_modules/tslib/tslib.es6.js:70:1) [<root>]
    at TestbedHarnessEnvironment._getQueryResultForElement (http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing.js:379:25) [<root>]
    at http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing.js:366:1 [<root>]
    at Array.map (<anonymous>) [<root>]
    at TestbedHarnessEnvironment.<anonymous> (http://localhost:9876/_karma_webpack_/node_modules/@angular/cdk/fesm2015/testing.js:366:1) [<root>]

I'd also tried running this with a global async function (with the following syntax:)

beforeEach(async( async () => {
    // magic happening here
}));

I even tried extracting these harnesses into separate functions, to call them as late as possible, but it also didn't work well:

const usernameFormField = () => {
    loader.getHarnes(...);
}

// later in code; not the most elegant, but good enough
const usernameField = await usernameFormField();
expect(await usernameField().hasErrors()).toBeFalsy();

As this post discusses, the 'double-async' construct is valid, if a little clumsy. However, it didn't work for me; the only variant that did was beforeEach(async( () => { ... } ));. Is it possible to use async-await inside beforeEach in async zone, or am I stuck with handling everything manually using Promises?

EDIT: a similar problem shows up not only in beforeEach(), but also in test methods themselves, even when I don't preinitialize the harnesses:

it('should display \'log out\' and \'my account\' buttons when user is authenticated',
    async () => {
        const EXAMPLE_USERNAME = 'username';

        spyOnProperty(authenticationService, 'authenticatedUser')
            .and.returnValue(EXAMPLE_USERNAME);

       expect(fixture.componentInstance.authenticatedUser)
.toEqual(EXAMPLE_USERNAME);

        const logOutButton = await loader
            .getHarness(MatButtonHarness.with({text: BUTTON_LOG_OUT_TEXT}));
        expect(await logOutButton.isDisabled()).toBeFalsy();

        // the following line causes a problem
        /*const myAccountButton = await loader
            .getHarness(MatButtonHarness.with({text: BUTTON_MY_ACCOUNT_TEXT}));
        expect(await myAccountButton.isDisabled()).toBeFalsy();
        await myAccountButton.click();
        expect(routerSpy.navigateByUrl).toHaveBeenCalled();*/
    });

When I uncomment only the first commented line, the code breaks and the test doesn't pass. When I include the async zone, the test passes, but the errors persist. I initially thought this is a problem with initializing the component, but now it seems it's more related to HarnessLoader.

EDIT 2: Coderer's answer links to some problems with karma.conf.js, so here are some of my configuration files:

karma.conf.js:

// custom headless chrome from
// https://coryrylan.com/blog/building-angular-cli-projects-with-github-actions

module.exports = function (config) {
  config.set({
    // adding any files here didn't seem to work
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular-devkit/build-angular/plugins/karma')
    ],
    client: {
      clearContext: false, // leave Jasmine Spec Runner output visible in browser
      jasmine: {
        random: false
      }
    },
    coverageIstanbulReporter: {
      dir: require('path').join(__dirname, './coverage/elx-front-end'),
      reports: ['html', 'lcovonly', 'text-summary'],
      fixWebpackSourcePaths: true
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    restartOnFileChange: true,
    customLaunchers: {
      ChromeHeadlessCI: {
        base: 'ChromeHeadless',
        flags: ['--no-sandbox', '--disable-gpu']
      }
    }
  });
};

src/test.ts: (importing as described here also didn't work)

// This file is required by karma.conf.js and loads recursively all the .spec and framework files

import "zone.js/dist/zone-testing";
import {getTestBed} from "@angular/core/testing";
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from "@angular/platform-browser-dynamic/testing";

declare const require: any;

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context("./", true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

tsconfig.base.json:

{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "module": "es2020",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "allowSyntheticDefaultImports": true,
        "importHelpers": true,
        "target": "es2018",
        "strict": true,
        "typeRoots": [
            "node_modules/@types"
        ],
        "lib": [
            "es2018",
            "dom"
        ],
        "paths": { /* custom paths */}
    },
    "angularCompilerOptions": {
        "fullTemplateTypeCheck": true,
        "strictInjectionParameters": true
    }
}

tsconfig.spec.json:

{
    "extends": "./tsconfig.base.json",
    "compilerOptions": {
        "outDir": "./out-tsc/spec",
        "types": [
            "jasmine",
            "node"
        ]
    },
    "files": [
        "src/test.ts",
        "src/polyfills.ts"
    ],
    "include": [
        "src/**/*.spec.ts",
        "src/**/*.d.ts"
    ]
}

fragment of angular.json:

"architect": {
    ...
    "test": {
        "builder": "@angular-devkit/build-angular:karma",
        "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.spec.json",
            "karmaConfig": "karma.conf.js",
            "assets": [
                "src/favicon.ico",
                "src/assets"
            ],
            "styles": [
                "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
                "src/styles.scss",
                "src/dimens.scss"
                ],
                "scripts": []
            }
        }
    }
}
Kamil Plich
  • 85
  • 2
  • 8
  • Did you find an answer for this? I noticed that the "expected to be in ProxyZone" errors go away if I don't include `zone-patch-rxjs-fake-async`, but calls to `loader.getAllHarnesses` never resolve. – Coderer Oct 13 '20 at 14:12
  • See my answer below about including `zone-patch-etc` in the right order, but also, my calls to `loader.getAllHarnesses` *actually* fail to resolve because they call `fixture.whenStable()` under the hood, and `whenStable` never resolves if the component under test uses `setInterval` at all. I'm still poking at that one. – Coderer Oct 14 '20 at 11:52
  • Hey, I've noticed your answer only now. I'll take a look in a moment and let you know. – Kamil Plich Oct 14 '20 at 15:40

2 Answers2

4

I filed issue 21632 about this for Angular 11, because the setup is as it is described in the documentation's Getting started. The primary culprit is the beforeEach function, and there are three potential solutions.

Option 1

The async function has been deprecated and was replaced with waitForAsync. Using waitForAsync in the beforeEach instead of an asynchronous function is one way to resolve the issue as of Angular 11 (demo).

beforeEach(waitForAsync(() => {
  TestBed.configureTestingModule({
    imports: [MaterialModule, BrowserAnimationsModule, ReactiveFormsModule],
    declarations: [RegistrationComponent]
  }).compileComponents().then(async () => {
    fixture = TestBed.createComponent(RegistrationComponent);
    loader = TestbedHarnessEnvironment.loader(fixture);
    usernameFormField = await loader
      .getHarness(MatFormFieldHarness.with({selector: '#username-form-field'}))
    registrationButton = await loader.getHarness(MatButtonHarness);
  });
}));

Option 2

Passing an async function to the beforeEach without waitForAsync, also works for your specific case (demo). However, when not handling the compileComponents promise, as most documentation does, move the call to TestbedHarnessEnvironment.loader to the it function (demo).

beforeEach(async () => {
  await TestBed.configureTestingModule({
    imports: [MatButtonModule],
    declarations: [ButtonHarnessExample]
  }).compileComponents();
  fixture = TestBed.createComponent(ButtonHarnessExample);
});

it('should click a button', async () => {
  const loader = TestbedHarnessEnvironment.loader(fixture);
  const button = await loader.getHarness(MatButtonHarness.with({text: 'Basic button'}));
});

Option 3

Change the target to ES2016 in tsconfig.json (demo).

"compilerOptions": {
  "target": "es2016"
}
Trevor Karjanis
  • 1,485
  • 14
  • 25
  • Thanks so much for the answer! It was enough to downgrade the target; apparently, I didn't come to think that it might be the cause, even though the CLI warns about potential issues with `async/await` in newer targets. – Kamil Plich Jan 24 '21 at 20:20
  • changing the target from es2017 to es2016 worked for me as well – John May 25 '21 at 17:06
-1

If you're seeing errors about ProxyZone, you probably have your Zone dependencies loading in the wrong order. There is some detail in this GitHub issue but honestly they just did a pretty bad job of documenting how to set up your own tests with Zone. (The current testing intro page says "The CLI takes care of Jasmine and Karma configuration for you.", which must be nice... if you created your project with the CLI.)

I had the same error you're getting, and got past it by putting this in my karma.conf.ts:

        // list of files / patterns to load in the browser
        files: [
            // Everybody needs Zone, probably
            "node_modules/zone.js/bundles/zone.umd.js",
            "node_modules/zone.js/bundles/zone-patch-rxjs-fake-async.umd.js",
            "node_modules/zone.js/bundles/zone-testing.umd.js",
            { pattern: "./src/app/**/*.spec.ts", type: "js" }
        ],

I think your error comes if you either don't include patch-rxjs-fake-async, or if you import it after zone-testing, but I couldn't say for sure.

Coderer
  • 25,844
  • 28
  • 99
  • 154
  • Thank you for your answer! Unfortunately, importing these files either in `karma.conf.js` or `test.ts` didn't help (then, actually none of the tests passed). I updated the question with some (possibly) relevant configuration files; I'd appreciate it if you took a look at them. – Kamil Plich Oct 14 '20 at 16:52
  • Do I understand correctly that including `zone-patch-rxjs-fake-async` causes all your tests to fail? What error message do you get? I'm not sure how much I can help, other than to say that I got the "expected to be in ProxyZone" error without it and now I don't. – Coderer Oct 15 '20 at 15:26