3

We're trying to figure out how to use Jest.js with Knockout.js. Essentially we want to create a small fragment of DOM such as:

<div class="card" data-bind="component: myCustomComponent"/>

Then be able to call ko.applyBindings() with a custom view model, and that DOM fragment and for Knockout to load the component up against the view model, so we can then do a Jest snapshot test.

The bit we're struggling to work out, is how can I effectively create a DOM fragment that is suited to ko.applyBindings() as it requires a DOM node which we're not sure how to create/select.

EDIT

I've played around with the answer from @user3297291 and put together the following example, which unfortunately I can't get to work. Here's my example that I've put together:

const jsdom = require("jsdom");
const ko = require("knockout");

describe("Knockout Test", () => {

    beforeAll(() => {
        ko.components.register("greeting", {
            viewModel: function(params) {
                //params.name = "test";
                //this.message = ko.observable(`Hello ${params.name}`);
                this.message = ko.observable("Hello World!");
            },
            template: '<span data-bind="text: message"/>'
        });
    });

    it("test pass", (done) => {

        jsdom.env(`<div class="wrapper"></div>`, [], (err, window) => {
            var wrapper = window.document.querySelector(".wrapper");
            var element = window.document.createElement("div");

            element.setAttribute("data-bind", 'component: "greeting"');
            wrapper.appendChild(element);

            ko.applyBindings({ name: 'Ian' }, wrapper);

            setTimeout(() => {
                console.log(element.innerHTML);
                expect(element.innerHTML).toMatchSnapshot();
                done();
            }, 10);

          }
        );
    });
});

If I modify element.setAttribute("data-bind", 'component: "greeting"'); to be element.setAttribute("data-bind", 'text: name'); then it works correctly, but loading a component (which is what I actually want to do) always seems to result in an empty element.innerHTML.

Here's a package.json if you want to reproduce, and I'm running npm run jest:

{
  "name": "kotest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "jest": "jest"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "jasmine": "^2.5.3",
    "jest": "^18.1.0",
    "jsdom": "^9.9.1",
    "knockout": "^3.4.1"
  }
}
Ian
  • 33,605
  • 26
  • 118
  • 198
  • You can just create a DOM element with jQuery or vanilla js and [pass it as the second argument to `applyBindings`](http://stackoverflow.com/a/18990371/419956) AFAIK. Have you tried it yet? – Jeroen Jan 17 '17 at 10:20
  • @Jeroen you sure you can do that within a nodejs/jest environment? I've tried and it seems that jQuery can't really create the DOM Element, it gives you back an array with an empty object in it. This in turn prevents knockout binding – Ian Jan 17 '17 at 12:00
  • No, wasn't entirely sure. I don't know too much about jestjs, and hadn't grasped from your question that you're using a nodejs environment. In the past I've usually run QUnit tests on KnockoutJS code using Chutzpah which AFAIK uses PhantomJS under the hood providing you with DOM stuff. Hopefully someone else can help. Good luck! – Jeroen Jan 17 '17 at 13:15
  • @Jeroen ah fair enough. Yeah unfortunately I believe Jest only runs in a node environment. Maybe I'll add an extra tag to my question. – Ian Jan 17 '17 at 15:31
  • I've tried out some of the code in my answer using `jsdom` to mimic the `window`/DOM and didn't run into any issues. Curious to know if it's of any help... – user3297291 Jan 17 '17 at 17:09
  • @user3297291: yeah a colleague was looking at this, but I just decided to give it a try. It's close, and I think solves one of the hurdles - I'm struggling getting components to load which is probably the main thing you'd want to test using Knockout and snapshotting. Wonder if you have any ideas? – Ian Jan 18 '17 at 15:11

1 Answers1

1

In a browser's javascript environment, you can test knockout virtually by creating DOM elements through code. For example, you can do this:

ko.applyBindings({}, document.createElement("div"));

This means you can write any UI test by applying a component binding to a virtual <div> and checking its contents. The example below shows some of the basics.

function testComponent(options, test) {
  var wrapper = document.createElement("div");

  // If you have a HTML string, you can do this by setting
  // wrapper.innerHTML = "<div ... ></div>";
  var binding = "component: { name: name, params: params }"
  wrapper.setAttribute("data-bind", binding);
  
  // Apply bindings to virtual element
  ko.applyBindings(options, wrapper);
  
  // Once done, call test with wrapper
  setTimeout(test.bind(wrapper));
};

var messageEditorTest = function() {
  var opts = { name: "message-editor", params: {} };
  
  testComponent(opts, function() {
    var input = this.querySelector("input"),
        display = this.querySelector("span"),
        vm = ko.dataFor(this.firstElementChild);
    
    console.log("MESSAGE EDITOR TESTS:");
    
    console.log("Has an input field:", 
                input !== null);
    
    console.log("Has a display field:",
                display !== null);
    
    console.log("Length is initially zero:",
                display.innerText === "0");
    
    vm.text("Four");
    console.log("Length reflects value input:",
                display.innerText === "4");
                
  });
};

// Component
ko.components.register('message-editor', {
    viewModel: function(params) {
        this.text = ko.observable(params && params.initialText || '');
    },
    template: 'Message: <input data-bind="value: text" /> '
            + '(length: <span data-bind="text: text().length"></span>)'
});

messageEditorTest();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>

Now, I'm not sure how it works if you want to test in Node.js, without the help of a browser... I think you can use a javascript DOM implementation like jsdom, or you could try firing up a "test-browser" like PhantomJS.

Edit: I ran a quick test using jsdom and everything seems to work just fine

  • run npm install jsdom
  • run npm install knockout
  • create a file named test.js:

    var jsdom = require("jsdom");
    var ko = require("knockout");
    
    jsdom.env(
      '<div class="wrapper"></div>',
      [],
      function (err, window) {
        var wrapper = window.document.querySelector(".wrapper");
    
        var element = window.document.createElement("div");
        element.setAttribute("data-bind", "text: test");
        wrapper.appendChild(element);
    
        ko.applyBindings({ test: 'My Test' }, wrapper);
    
        console.log(element.innerHTML); // Logs "My Test"
      }
    );
    
  • run node test.js
  • logs My Test
user3297291
  • 22,592
  • 4
  • 29
  • 45
  • Looking into this a bit more (trying to load a component) I get the following issue: `[Error]: Message: First argument to Node.prototype.insertBefore must be a Node​​ ​​​​​[Info]: Thu, 19 Jan 2017 09:45:58 GMT wallaby:workers Test executed: component registered​​​​​ ​​[Error]: at HTMLDivElementImpl.insertBefore (C:\source\mood\kotest\node_modules\jsdom\lib\jsdom\living\nodes\Node-impl.js:171:13)​​` – Ian Jan 19 '17 at 09:49
  • I don't get the same error code, but I'm also not sure if the `innerHTML` gets set.. I'll have another look later today, if I can find the time. – user3297291 Jan 19 '17 at 10:22
  • I was able to reproduce your error by setting `synchronous: true` for the component. The problem *seems* to be related to the default template engine not working together very well with our virtual environment... I was able to solve this by overriding the default `loadTemplate` method. See test here: https://jsfiddle.net/sygjajcj/ I'd advice you to have another look and create some more tests before the actual test that calls `applyBindings`. For example "registers the component" with `expect(ko.components.isRegistered("greeting")).toBeTruthy();` – user3297291 Jan 19 '17 at 10:53
  • I might be missing something, if you get a chance could you have a look at my attempt to reproduce your fix? https://github.com/MooDEdge/ko-jest – Ian Jan 19 '17 at 11:56
  • I think the problem might be that the `beforeAll` didn't run using the same `window` property we had used to run our tests with... But still not entirely sure. I managed to pass your tests with these modifications: https://jsfiddle.net/f6y848bd/ (see comments) – user3297291 Jan 19 '17 at 12:26
  • Thanks very much, be interested to know how you identified the different `window`. I'd spent a bit of time debugging, but not managed to spot that one. – Ian Jan 19 '17 at 13:22