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": []
}
}
}
}
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.
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);
});
}));
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'}));
});
Change the target to ES2016 in tsconfig.json (demo).
"compilerOptions": {
"target": "es2016"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With