angularjswebpackjestjswebpack-html-loader

Using html-loader with angularJS (v1) templates and Jest is failing


I've got an AngularJS (v.1.7) app served by Rails. I just moved from Sprockets to Webpack. While trying to migrate my Jasmine tests to Jest. I ran into an issue with html-loader which I'm using to import my directive templates into the directive.

For one simple directive where I import the template, Jest fails to load the test because the html-loader fails with the error

TypeError: Cannot read property 'query' of undefined

      1 | const htmlLoader = require("html-loader");
      2 | module.exports = {
    > 3 |   process: (src) => htmlLoader(src)
        |                     ^
      4 | };
      5 |
    at getOptions (node_modules/html-loader/node_modules/loader-utils/lib/getOptions.js:6:31)

I was following recommendations in this SO post and from this npm package html-loader-jest. In my package.json, I've added the following jest config

  "jest": {
    "moduleFileExtensions": [
      "js",
      "html"
    ],
    "transform": {
      "^.+\\.js$": "babel-jest",
      "^.+\\.html$": "<rootDir>/jest/support/html_loader.js"
    },
    ...
  }

And the support file

// jest/support/html_loader.js
const htmlLoader = require("html-loader");
module.exports = {
  process: (src) => htmlLoader(src) 
};

The stacktrace points me to html-loader (from node_modules)

// node_modules/html-loader/node_modules/loader-utils/lib/getOptions.js
function getOptions(loaderContext) {
  const query = loaderContext.query;
...

If I trace into here during the Jest run, I find this loaderContext undefined (as the error reports).

My question is... is this the correct way to use this htmlLoader? If so, what should I expect loaderContext to be? Is there a way to get jest to provide that value? Or is this not the way the htmlLoader is supposed to be called outside of the actual Webpack pipeline.

This problem only happens when I'm running via jest. webpack properly compiles the all assets for the app.

library versions

html-loader: 1.0.0
webpack: 4.42.1
jest: 25.2.4

code (for clarity)

// mailer.js
import angular from "angular";
import ngInject from "@js/ng-inject";
import template from "./index.html";

const mailer = ngInject(function () {
  return {
    restrict: "E",
    scope: {
      text: "@",
    },
    template: template,
  };
});
angular.module("app-directives").directive("mailer", mailer);
<!-- index.html -->
<a>{{text}}</a>
// mailer.test.js
import expect from "expect";
import "./mailer";

describe("app-directives.mailer", function () {
  it("works", () => {
    expect(true).toBeTruthy();
  })
});

Solution

  • Well, I figured it out. It seems that when jest runs, it's not passing the correct context (as I suspected above) causing the getOptions call to fail.

    The solution was to write a very simple loader just for jest which does not bother with getOptions. I was able to replace my jest/support/html_loader.js code with the following (basically cribbed from webpack-contrib/raw-loader.

    // jest/support/html-loader.js
    module.exports = {
      process: (content, _path) => {
        // escape newlines
        const json = JSON.stringify(content)
              .replace(/\u2028/g, '\\u2028')
              .replace(/\u2029/g, '\\u2029');
        return `module.exports = ${json};`
      }
    };
    

    This basically returns the template as js module that exports the template. The replace appears to be dealing with newlines.

    I hope this saves someone else some digging.