Sunday, June 2, 2013

Dart Tests with Window Boundaries

‹prev | My Chain | next›

OK, today I am going to implement the Update button feature. No distractions, dammit. You know, we introduced a really interesting new test helper in Firday night's #pairwithme session… No! Update button, dammit.

The Update button in the ICE Code Editor forces an immediate update of the preview layer. It seems that the easiest test would be to add some content in the editor, click the update button and check the preview layer's DOM:
    test("updates the preview layer", (){
      helpers.createProject("My Project");

      editor.content = "<h1>Hello</h1>";'button', text: " Update");

      var preview = query('iframe').contentWindow.document.body;
That would be a quick, easy, and reliable way to test that the preview iframe is updated correctly… if it were possible to directly communicate between frames in Dart. Unfortunately for me, Dart is extremely restrictive about inter-frame communication.

The results is that my simple test fails because a Dart iframe's contentWindow does not support the document property:
FAIL: Update Button updates the preview layer
  Caught Class '_DOMWindowCrossFrame' has no instance getter 'document'.
  NoSuchMethodError : method not found: 'document'
  Receiver: Instance of '_DOMWindowCrossFrame@0x3918afae'
  Arguments: []
I understand the need for better security and applaud the restriction. But man, does that suck for me.

If I have no way to directly query the content of an iframe, I am left with two choices for my test. One option would be to have the iframe respond to a postMessage() with its contents. The other option is to introduce an event stream that gets updated whenever the iframe content is updated.

I opt for the latter option as it seems more likely to be of some potential benefit in the application code. Also, it seems a worthwhile boundary.

So I rewrite my test in the anticipation of a soon-to-be-created onPreviewChange stream:
    test("updates the preview layer", (){
      helpers.createProject("My Project");
      editor.content = "<h1>Hello</h1>";

      editor.onPreviewChange.listen(expectAsync1((_)=> true));'button', text: " Update");
At first that fails due to a lack of the onPreviewChange getter. I fix it with:
class Full {
  // ...
  Stream get onPreviewChange =>;

  StreamController __previewChangeController;
  StreamController get _previewChangeController {
    if (__previewChangeController != null) return  __previewChangeController;

    return __previewChangeController = new StreamController();
With that, I eliminate the missing property error, but now I am faced with the following console message:
Unable to post message to file://. Recipient has origin null.
Ugh. Testing web applications locally can be a pain sometimes. Sending messages between frames is one of them.

The only thing that I can do it account for file: URLs in the application code when sending the postMessage():
  updatePreview() {
      // ...
      var url = new RegExp(r'^file://').hasMatch(window.location.href)
        ? '*': window.location.href;
      iframe.contentWindow.postMessage(_ace.value, url);
The '*' value for file: URLs allow postMessage() messages between any windows, regardless of origin. Unfortunately, this is the only way to allow communication between two file: windows.

Once the update is complete, I trigger an event on the stream by adding to the stream:
In this case, it does not matter what I add to the stream since it is the event itself that is important. In the future, I might add the content as part of the event payload. Since it ought to be the same as the editor's content, there does not seem much point to this.

In the end, I am a bit frustrated not being able to access the content of the iframe. Until now, my tests had been very much of the functional variety—clicking buttons and checking resultant page content. I like tests with stricter boundaries—more along the lines of true unit tests. They have their purpose as they had tonight. Still, I appreciate having the choice.

Day #770

No comments:

Post a Comment