Search code examples
angularunit-testingkarma-runnerkarma-webpackangular-hybrid

Hybrid Angular App Testing with Karma Cannot Load HTML


We have a hybrid Angular app that uses karma for unit testing. I'm trying to add our first suite of tests but I'm getting some errors that indicate karma cannot find the dashboard.component.html.

View:

import { Component, OnInit } from '@angular/core';

@Component({
    templateUrl: './views/components/dashboard/dashboard.component.html'
})
export class DashboardComponent implements OnInit {
    constructor() {}

    ngOnInit() {
        console.log('works!');
    }
}

Here is my karma.config.js

module.exports = function(config) {
    config.set({
        basePath: '',
        frameworks: ['angular', 'jasmine'],
        files: [
            { pattern: 'src/test.ts', watched: false },
            { pattern: 'dist/views/components/dashboard/dashboard.component.html', included: false, watched: true }
        ],
        exclude: [],
        preprocessors: {
            'src/test.ts': ['webpack', 'sourcemap']
        },
        webpack: require('./webpack-base.config'),
        reporters: ['progress'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        singleRun: true,
        concurrency: Infinity,
        browsers: ['Chrome_Headless'],
        customLaunchers: {
            Chrome_Headless: {
                base: 'Chrome',
                flags: [
                    '--headless',
                    '--disable-gpu',
                    '--remote-debugging-port=9222'
                ]
            },
            Chrome_without_security: {
                base: 'Chrome',
                flags: [
                    '--headless',
                    '--disable-gpu',
                    '--remote-debugging-port=9222',
                    '--disable-web-security'
                ]
            }
        },
        // workaround for typescript and chrome/headless
        mime: {
            'text/x-typescript': ['ts', 'tsx']
        }
    });
};

Our hybrid app gets compiled using Webpack. All view files are copied over to /view. Here is our webpack file:

/* eslint-env node */
const webpack = require('webpack');
const helpers = require('./helpers');
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractSass = new ExtractTextPlugin({
    filename: 'css/[name].[hash].css',
    disable: process.env.NODE_ENV === 'development'
});

module.exports = {
    mode: 'development',
    entry: {
        app: './src/js/index.ts'
    },
    resolve: {
        extensions: ['.ts', '.js', '.html'],
        alias: {
            '@angular/upgrade/static':
                '@angular/upgrade/bundles/upgrade-static.umd.js'
        }
    },
    plugins: [
        new CleanWebpackPlugin(['dist']),
        // Workaround for angular/angular#11580
        new webpack.ContextReplacementPlugin(
            // The (\\|\/) piece accounts for path separators in *nix and Windows
            /angular(\\|\/)core(\\|\/)@angular/,
            helpers.root('./src'), // location of your src
            {} // a map of your routes
        ),
        new HtmlWebpackPlugin({
            template: './src/index.html',
            inject: 'body'
        }),
        new CopyWebpackPlugin([
            { from: './src/views', to: 'views' },
            { from: './src/js/components', to: 'views/components', ignore: ['*.ts', '*.scss']},
            { from: './src/img', to: 'img' },
            { from: './src/config.js', to: '' }
        ]),
        extractSass
    ],
    devtool: 'inline-source-map',
    devServer: {
        contentBase: './dist',
        historyApiFallback: {
            disableDotRule: true
        }
    },
    output: {
        filename: 'js/[name].[hash].js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                loaders: ['awesome-typescript-loader', 'angular-router-loader']
            },
            {
                test: /\.scss$/,
                use: extractSass.extract({
                    use: [
                        {
                            loader: 'css-loader',
                            options: {
                                url: false,
                                import: true,
                                minimize: true,
                                sourceMap: true,
                                importLoaders: 1
                            }
                        },
                        {
                            loader: 'sass-loader',
                            options: {
                                sourceMap: true
                            }
                        }
                    ],
                    fallback: 'style-loader'
                })
            }
        ]
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all'
                }
            }
        }
    }
};

Finally here is my very simple test:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { DashboardComponent } from './dashboard.component';

describe('The Dashboard', () => {
    let component: DashboardComponent;
    let fixture: ComponentFixture<DashboardComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [DashboardComponent]
        }).compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(DashboardComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    it('should be created', () => {
        expect(component).toBeTruthy();
    });
});

This app works correctly as it should when npm start. Again the problem that I'm getting is a 404 of the HTML file.

ERROR: 'Unhandled Promise rejection:', 'Failed to load views/components/dashboard/dashboard.component.html', '; Zone:', 'ProxyZone', '; Task:', 'Promise.then', '; Value:', 'Failed to load views/components/dashboard/dashboard.component.html', undefined

I've tried overriding the test spec in the TestBed.configureTestingModule() to look for the HTML file in different places. I've tried adding a new files pattern in the karma.config.js. I've also tried a combination of the two to no success.


Solution

  • I fixed it by doing the following:

    In karma.config.js I added this line:

    proxies: { "/dist/": 'http://localhost:8080' }
    

    In the spec file I added this override:

    beforeEach(async(() => {
            TestBed.configureTestingModule({
                declarations: [DashboardComponent]
            }).overrideComponent(DashboardComponent, {
                set: {
                    templateUrl: '/dist/views/components/dashboard/dashboard.component.html'
                }
            })
            .compileComponents();
        }));
    

    I did remove the { pattern: 'dist/views/components/dashboard/dashboard.component.html', included: false, watched: true } pattern as it wasn't doing anything someone pointed out in the comments.