Monday, September 22, 2014

TDD a Fix for Broken i18n in Polymer.dart


I think my Polymer.dart element is broken. Somewhere in the various upgrades to Polymer.dart, the internationalization project code for Patterns in Polymer stopped working. It sure would have been nice had my continuous integration server been working when that happened, but I can only change the future.

The problem is not that localization does not work at all. When I specify the locale with an attribute (<hello-you locale="fr"><hello-you>), it localizes just fine:



The problem occurs when changing locales with the drop-down menu. Specifically, the labels for the element remain in the original locale when the drop-down changes:



Since I can only change the future, I refuse to allow this to happen to me again. I will have tests that catch this problem. The needs for such tests is that much more important by virtue of the code already failing once.

Before jumping into writing the test case for my failure, I start with a simpler case. I know that specifying the locale via published Polymer attribute works, so I start with a test for that:
    test('starts in French', (){
      schedule((){
        expect(_el.shadowRoot.text, contains('Bonjour'));
      });
    });
The schedule() comes from the scheduled_test, which is a testing package for readable asynchronous tests built on top of unittest. This schedule is guaranteed to come after any schedules in the test setup, which can be quite helpful.

Helpful perhaps, but it does not solve all problems. This test fails:
FAIL: <hello-you locale="fr"/> (French) starts in French
  Caught ScheduleError:
  | Expected: contains 'Bonjour'
  |   Actual: '\n'
  |   '    \n'
  |   '      Hello \n'
  |   '    \n'
The test itself is sound. The problem winds up being in the setup of my test:
    setUp((){
      schedule(()=> Polymer.onReady);

      schedule((){
        var _completer = new Completer();
        _el = createElement('<hello-you locale="fr"></hello-you>');
        document.body.append(_el);

        _el.async((_){ _completer.complete(); });

        return _completer.future;
      });
    });
    // ...
  });
There are two schedules in my setup. One waits until the Polymer library itself is ready. The second one waits for this particular element is ready. A Polymer element is really ready when it updates its bound variables and renders. Polymer normally does this when appropriate, but it is possible to encourage this to happen immediately with async(). So the second schedule tells my Polymer element to update its bound variables, draw itself, then invoke the supplied callback which completes the schedule's completer.

Normally, this is enough when testing Polymer elements. But in this case, completing the completer when the element is ready is not sufficient. I also have to wait a few milliseconds for the JSON localization files to load and apply to the Polymer element:
      schedule((){
        var _completer = new Completer();
        _el = createElement('');
        document.body.append(_el);

        _el.async((_){
          // wait for l10n to be loaded
          new Timer(
            new Duration(milliseconds: 50),
            ()=> _completer.complete()
          );
        });

        return _completer.future;
      });
In all likelihood this a Polymer code smell. The complexity of this test setup is probably trying to tell me that my custom element should itself expose a future that completes when all localization data is loaded. I table that for the moment as I would like to focus on the task at hand.

With the additional wait time, my element now renders both French and Spanish versions:
CONSOLE MESSAGE: PASS: <hello-you locale="fr"/> (French) starts in French
CONSOLE MESSAGE: PASS: <hello-you locale="es"/> (Spanish) starts in Spanish
Perhaps this is all I need for my switching code?

Well, no. Even with the delay, a test for changing the locale still fails:
    test('defaults to English', (){
      schedule((){
        expect(_el.shadowRoot.text, contains('Hello'));
      });
    });

    test('can localize to French', (){
      schedule(()=> _el.locale = 'fr');
      schedule((){
        expect(_el.shadowRoot.text, contains('Bonjour'));
      });
    });
The text inside the Polymer element remains English, which is not too surprising. I have only written new test code so far, not made code changes.

So what is the actual problem? Eventually, I realize that the <hello-you> element and its translation strategy element, <x-translate> do not seem to be communicating via the bound variables that they share:
<polymer-element name="hello-you">
  <template>
    <!-- ... -->
    <x-translate id="l10n"
                 locale={{locale}}
                 labels={{labels}}></x-translate>
  </template>
  <script type="application/dart" src="hello_you.dart"></script>
</polymer-element>
I know that the properties are set correctly—otherwise my working tests would not be working—it is possible set the locale once via attribute, just not update them. Wait…

I know this. In fact I just ran into this in the JavaScript version of Polymer. The solution there was to mark a Polymer element's property as published and reflectable. I know how to do that in JavaScript, but what about Dart?

The answer is @PublishedProperty. Instead of declaring locale as just published:
@CustomTag('x-translate')
class XTranslate extends PolymerElement {
  @published String locale = 'en';
  // ...
}
I need to instead declare it as a published property whose value is reflected in the attribute whenever a change is made:
@CustomTag('x-translate')
class XTranslate extends PolymerElement {
  @PublishedProperty(reflect: true)
  String locale = 'en';
  // ...
}
With that, I have my element changing locales in response to drop-down menu changes:



More importantly, I now have 5 passing tests to ensure that this never again breaks without my knowledge:
CONSOLE MESSAGE: PASS: <hello-you/> (i18n) defaults to English
CONSOLE MESSAGE: PASS: <hello-you/> (i18n) can localize to French
CONSOLE MESSAGE: PASS: <hello-you/> (i18n) can localize to Spanish
CONSOLE MESSAGE: PASS: <hello-you locale="fr"/> (French) starts in French
CONSOLE MESSAGE: PASS: <hello-you locale="es"/> (Spanish) starts in Spanish
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 5 tests passed.
This “reflectable” property stuff is starting to get on my nerves. This is at least the third time that its behavior has surprised me. Maybe someday I will expect the actual behavior. Until then, I have tests verifying that I have correctly written my code for my desired behavior.


Day #191

No comments:

Post a Comment