Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Material/Jasmine testing - errors while loading Material Harnesses

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": []
            }
        }
    }
}
like image 711
Kamil Plich Avatar asked Oct 26 '22 19:10

Kamil Plich


1 Answers

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"
}
like image 58
Trevor Karjanis Avatar answered Oct 30 '22 03:10

Trevor Karjanis