Showing posts with label drop. Show all posts
Showing posts with label drop. Show all posts

Friday, September 13, 2013

I Don't Understand Drop Events (in any language)


Regardless of whether or not I can test it in Dart, I still need to be able to drop project files into ICE Code Editor. So as much as it pains me to do this… tonight I will add that feature to ICE without a test to guide me.

After last night's vain attempt to TDD the behavior into existence, I have the outline of how this will look:
    Element.
      dropEvent.
      forTarget(document).
      listen((event) {
        event
          ..preventDefault()
          ..stopPropagation();

        var file = event.dataTransfer.files.first;
      });
Nothing too fancy there—in Dart or JavaScript. It establishes an event stream for drop events. When such an event occurs, the listener callback is invoked with the drop event. I prevent the default action (which would be to open the file in the browser) and then get started with reading the contents of the dropped file.

As of last night, I had only gotten as far as getting a reference to the uploaded file. I was unable to populate the dataTransfer property in a test, but it ought to work just fine in real life. The API for file readers in Dart seems to follow the JavaScript exactly. So my next step is to create a FileReader object that reads the dropped file as text:

      // ...
      listen((event) {
        event
          ..preventDefault()
          ..stopPropagation();

        var file = event.dataTransfer.files.first;
        new FileReader()
          ..onLoad((read_event) {
              print(read_event.target.result);
            })
          ..readAsText(file);
      });
If everything is working correctly, when I drop a file into ICE:



I should now see the contents of the file printed to the console.

Instead, I see the contents of the file that I just uploaded:



Arrgh! Is drop just completely broken in Dart?!

Er, no. It's just me.

In the end, I have to boil this down into the simplest JavaScript version that is possible before I realize that it still does not work. It seems that it is not sufficient to prevent the drop event—I also have to prevent the “dragover” event:
<body>
<h1>Hello</h1>
</body>
<script>
document.addEventListener('dragover', function (e) {
  e.preventDefault();
});

document.addEventListener('drop', function(e) {
  e.preventDefault();
  var file = e.dataTransfer.files[0];

  console.log('file.name: ' + file.name);
  console.log('file.type: ' + file.type);

  var reader = new FileReader();
  reader.onload = function (e) {
    console.log(e.target.result);
  };
  reader.readAsText(file);
});
</script>
And, in fact, I need to prevent both the dragover and drop default event behavior so that the dropped file does not open in the browser:



DOM coding: catch the fever!

Comforted in the knowledge that my trouble is with DOM coding, not Dart, I switch back to Dart. There, I also prevent the default dragover event behavior:
    document
      ..onDragOver.
          listen((event) {
            event.preventDefault();
          })
      ..onDrop.
          listen((event) {
            event.preventDefault();

            var file = event.dataTransfer.files.first;
            print('file.name: ${file.name}');
            print('file.type: ${file.type}');

            new FileReader()
              ..onLoad.
                  listen((read_event) {
                    print(read_event.target.result);
                  })
              ..readAsText(file);
          });
With that, I can successfully read the pertinent information from the dropped file:



No doubt there is a very good reason for needing to prevent two different events. I do not really want to know what it is. Really. Mostly I would like Dart or one of its libraries to make this go away. Regardless, I believe that I have a sufficient understanding of how this works to enable me to hook it into ICE—even if I cannot test it. That seems a fine stopping point for tonight.


Day #873

Thursday, September 12, 2013

Trying to Test Drive Drop Events in Dart


OK, I really am going to give up now. I have been banging my head against the custom keyboard event wall in Dart of late. I feel that I have made some real progress on that front, but think it probably best to wait for the new KeyEvent changes to land in Dart proper.

Instead, I switch gears tonight to something different: events in Dart. Well, maybe not that different, but Definitely not keyboard events. Tonight, I will try to add the ability to drop a code file onto the ICE Code Editor, triggering a file reader.

I am going to drive this with tests. So I start with a fixture file in my tests directory:
➜  ice-code-editor git:(master) ✗ cat test/fixtures/file_upload.html 
<body></body>
<script>
console.log("yo");
</script>
Actually…

That probably will not work. The browser and dart:html tends not to have access to the file system. I am starting to get a scary feeling about this. But I plug ahead anyway. I start with the usual setUp() that creates an instance of the full screen editor with single projects whose contents are Test. Then, in my test, I create a drop event, dispatch it to the document, and set my expectation:
    test("can create projects", (){
      var file_upload_event = new MouseEvent('drop');
      document.dispatchEvent(file_upload_event);

      expect(
        editor.content,
        'File Upload Content'
      );
    });
I have no reason whatsoever to expect that dispatching the drop event will change the editor content. I am not setting the “File Upload Content” content anywhere in the event. Even if that content were in the event, there is no code in the editor to process the event. Since I am not sure where to put the content in the event, I start with the code to start a file reader.

I already have a private method to attach mouse handlers in ICE, so this seems a reasonable place to put the drop handler:
  _attachMouseHandlers() {
    // ...
    Element.
      dropEvent.
      forTarget(document).
      listen((event) {
        print('event: ${event}');
        event
          ..preventDefault()
          ..stopPropagation();
        // File reader will go here...
      });
  }
My test still fails, but I see the mouse event in the handler's print statement:
unittest-suite-wait-for-done
event: Instance of 'MouseEvent'
FAIL: File Upload can create projects
  Expected: 'File Upload Content'
    Actual: 'Test'
     Which: is different.
  Expected: File Uploa ...
    Actual: Test
            ^
   Differ at offset 0
  
  package:unittest/src/expect.dart 78:29                                                                                                 expect
  ../full/file_upload_test.dart 27:13 
Dart seems to support the same dataTransfer property from JavaScript, so I add that into my handler:
    Element.
      dropEvent.
      forTarget(document).
      listen((event) {
        print('event: ${event}');
        event
          ..preventDefault()
          ..stopPropagation();

        var file = event.dataTransfer.files.first;
        print('file: $file');
      });
That results in an error because the dataTransfer property on my event is null:
Exception: The null object does not have a getter 'files'.

NoSuchMethodError : method not found: 'files'
Receiver: null
Arguments: [] dart:core-patch/object_patch.dart:20
Object.noSuchMethod dart:core-patch/object_patch.dart:20
Full._attachMouseHandlers.<anonymous closure> package:ice_code_editor/full.dart:229
Node.dispatchEvent /mnt/data/b/build/slave/dartium-lucid64-full-trunk/build/src/out/Release/gen/blink/bindings/dart/dart/html/Node.dart:215
file_upload_tests.<anonymous closure>.<anonymous closure> file_upload_test.dart:25
_run.<anonymous closure>
Unfortunately, I find myself in familiar territory here. The dataTransfer property on Dart's MouseEvent is final—there is no way to update it. Even if there was, I have no means of creating an instance of DataTransfer—it has no constructor (it is only instantiated internally).

Perhaps this is a job for mocks?

I import the mock library:
import 'package:unittest/unittest.dart';
import 'package:unittest/mock.dart';
Then create a mock version of the MouseEvent class:
class MockMouseEvent extends Mock implements MouseEvent {
  MockMouseEvent(String type);
}
If this works, I will add a mock DataTransfer. First, I update my test to dispatch my mock mouse event:
    test("can create projects", (){
      var file_upload_event = new MockMouseEvent('drop');
      document.dispatchEvent(file_upload_event);

      expect(
        editor.content,
        'File Upload Content'
      );
    });
And, when I run my test now, I get:
unittest-suite-wait-for-done undefined:1
ERROR: File Upload can create projects
  Test failed: Caught InvalidStateError: Internal Dartium Exception
  ../../../../../../mnt/data/b/build/slave/dartium-lucid64-full-trunk/build/src/out/Release/gen/blink/bindings/dart/dart/html/Node.dart 215:120  Node.dispatchEvent
  ../full/file_upload_test.dart 29:29                                                                                                            file_upload_tests.<fn>.<fn>
That is disappointing.

I am starting to get the feeling that testing anything involving events in Dart will not work. It is certainly frustrating because I would like to do the right thing for my codebase. One of the reasons that I switched from JavaScript to Dart for ICE was to gain the assurance that comes with testing. And yet I have hit my second testing dead-end.

Bother.


Day #872