Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(95)

Side by Side Diff: packages/template_binding/test/node_bind_test.dart

Issue 2989763002: Update charted to 0.4.8 and roll (Closed)
Patch Set: Removed Cutch from list of reviewers Created 3 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 library template_binding.test.node_bind_test;
6
7 import 'dart:async';
8 import 'dart:html';
9
10 import 'package:observe/observe.dart'
11 show toObservable, PathObserver, PropertyPath;
12 import 'package:template_binding/template_binding.dart'
13 show nodeBind, enableBindingsReflection;
14 import 'package:observe/mirrors_used.dart'; // make test smaller
15 import 'package:smoke/mirrors.dart' as smoke;
16
17 import 'package:unittest/html_config.dart';
18 import 'package:unittest/unittest.dart';
19 import 'utils.dart';
20
21 // Ported from: https://github.com/Polymer/NodeBind/blob/master/tests/tests.js
22
23 var bindings;
24
25 main() => dirtyCheckZone().run(() {
26 smoke.useMirrors();
27 useHtmlConfiguration();
28
29 setUp(() {
30 document.body.append(testDiv = new DivElement());
31 bindings = [];
32 });
33
34 tearDown(() {
35 testDiv.remove();
36 testDiv = null;
37 for (var b in bindings) if (b != null) b.close();
38 bindings = null;
39 });
40
41 group('Text bindings', testBindings);
42 group('Element attribute bindings', elementBindings);
43 group('Form Element bindings', formBindings);
44 });
45
46 testBindings() {
47 test('Basic', () {
48 var text = new Text('hi');
49 var model = toObservable({'a': 1});
50 bindings.add(nodeBind(text).bind('text', new PathObserver(model, 'a')));
51 expect(text.text, '1');
52
53 model['a'] = 2;
54 return new Future(() {
55 expect(text.text, '2');
56 });
57 });
58
59 test('oneTime', () {
60 var text = new Text('hi');
61 bindings.add(nodeBind(text).bind('text', 1, oneTime: true));
62 expect(text.text, '1');
63 });
64
65 test('No Path', () {
66 var text = new Text('hi');
67 var model = 1;
68 bindings.add(nodeBind(text).bind('text', new PathObserver(model)));
69 expect(text.text, '1');
70 });
71
72 test('Path unreachable', () {
73 var text = testDiv.append(new Text('hi'));
74 var model = 1;
75 var pathObserver = new PathObserver(model, 'a');
76 expect(() => nodeBind(text).bind('text', pathObserver), throws);
77 expect(text.text, 'hi');
78 });
79
80 test('Observer is Model', () {
81 // Dart note: we don't have _allObserversCount so we use binding reflection
82 // instead.
83 enableBindingsReflection = true;
84
85 // This future is here so we can turn off bindings reflection reliably.
86 Text text;
87 return new Future(() {
88 text = new Text('');
89 var model = toObservable({'a': {'b': {'c': 1}}});
90 var observer = new PathObserver(model, 'a.b.c');
91 bindings.add(nodeBind(text).bind('text', observer));
92 expect(text.text, '1');
93
94 var binding = nodeBind(text).bindings['text'];
95 expect(binding, observer, reason: 'should reuse observer');
96
97 model['a']['b']['c'] = 2;
98 }).then(endOfMicrotask).then((_) {
99 expect(text.text, '2');
100 }).whenComplete(() {
101 enableBindingsReflection = false;
102 });
103 });
104 }
105
106 elementBindings() {
107
108 test('Basic', () {
109 var el = new DivElement();
110 var model = toObservable({'a': '1'});
111 bindings.add(nodeBind(el).bind('foo', new PathObserver(model, 'a')));
112
113 return new Future(() {
114 expect(el.attributes['foo'], '1');
115 model['a'] = '2';
116 }).then(endOfMicrotask).then((_) {
117 expect(el.attributes['foo'], '2');
118 model['a'] = 232.2;
119 }).then(endOfMicrotask).then((_) {
120 expect(el.attributes['foo'], '232.2');
121 model['a'] = 232;
122 }).then(endOfMicrotask).then((_) {
123 expect(el.attributes['foo'], '232');
124 model['a'] = null;
125 }).then(endOfMicrotask).then((_) {
126 expect(el.attributes['foo'], '');
127 });
128 });
129
130 // Dart specific test
131 test('enableBindingsReflection defaults to off', () {
132 expect(enableBindingsReflection, false);
133
134 var el = new DivElement();
135 var model = toObservable({'a': '1'});
136 bindings.add(nodeBind(el).bind('foo', new PathObserver(model, 'a')));
137
138 expect(nodeBind(el).bindings, null);
139 });
140
141 test('enableBindingsReflection', () {
142 enableBindingsReflection = true;
143 try {
144 var el = testDiv.append(new DivElement());
145 var model = toObservable({'a': '1'});
146 bindings.add(nodeBind(el).bind('foo', new PathObserver(model, 'a')));
147 bindings.add(nodeBind(el).bind('bar', new PathObserver(model, 'a')));
148 bindings.add(nodeBind(el).bind('baz', new PathObserver(model, 'a')));
149
150 expect(nodeBind(el).bindings.keys,
151 unorderedEquals(['foo', 'bar', 'baz']));
152
153 } finally {
154 enableBindingsReflection = false;
155 }
156 });
157
158 test('oneTime', () {
159 var el = testDiv.append(new DivElement());
160 var model = toObservable({'a': '1'});
161 bindings.add(nodeBind(el).bind('foo', 1, oneTime: true));
162 expect('1', el.attributes['foo']);
163 });
164
165 test('No Path', () {
166 var el = testDiv.append(new DivElement());
167 var model = 1;
168 bindings.add(nodeBind(el).bind('foo', new PathObserver(model)));
169 return new Future(() {
170 expect(el.attributes['foo'], '1');
171 });
172 });
173
174 test('Path unreachable', () {
175 var el = testDiv.append(new DivElement());
176 var model = toObservable({});
177 bindings.add(nodeBind(el).bind('foo', new PathObserver(model, 'bar')));
178 return new Future(() {
179 expect(el.attributes['foo'], '');
180 });
181 });
182
183 test('Dashes', () {
184 var el = testDiv.append(new DivElement());
185 var model = toObservable({'a': '1'});
186 bindings.add(nodeBind(el).bind('foo-bar', new PathObserver(model, 'a')));
187 return new Future(() {
188 expect(el.attributes['foo-bar'], '1');
189 model['a'] = '2';
190
191 }).then(endOfMicrotask).then((_) {
192 expect(el.attributes['foo-bar'], '2');
193 });
194 });
195
196 test('Element.id, Element.hidden?', () {
197 var element = new DivElement();
198 var model = toObservable({'a': 1, 'b': 2});
199 bindings.add(
200 nodeBind(element).bind('hidden?', new PathObserver(model, 'a')));
201 bindings.add(nodeBind(element).bind('id', new PathObserver(model, 'b')));
202
203 expect(element.attributes, contains('hidden'));
204 expect(element.attributes['hidden'], '');
205 expect(element.id, '2');
206
207 model['a'] = null;
208 return new Future(() {
209 expect(element.attributes, isNot(contains('hidden')),
210 reason: 'null is false-y');
211
212 model['a'] = false;
213 }).then(endOfMicrotask).then((_) {
214 expect(element.attributes, isNot(contains('hidden')));
215
216 model['a'] = 'foo';
217 model['b'] = 'x';
218 }).then(endOfMicrotask).then((_) {
219 expect(element.attributes, contains('hidden'));
220 expect(element.attributes['hidden'], '');
221 expect(element.id, 'x');
222 });
223 });
224
225 test('Element.id - path unreachable', () {
226 var element = testDiv.append(new DivElement());
227 var model = toObservable({});
228 bindings.add(nodeBind(element).bind('id', new PathObserver(model, 'a')));
229 return new Future(() => expect(element.id, ''));
230 });
231 }
232
233 formBindings() {
234 inputTextAreaValueTest(String tagName) {
235 var el = new Element.tag(tagName);
236 testDiv.nodes.add(el);
237 var model = toObservable({'x': 42});
238 bindings.add(nodeBind(el).bind('value', new PathObserver(model, 'x')));
239 expect(el.value, '42');
240
241 model['x'] = 'Hi';
242 expect(el.value, '42', reason: 'changes delivered async');
243 return new Future(() {
244 expect(el.value, 'Hi');
245
246 el.value = 'changed';
247 dispatchEvent('input', el);
248 expect(model['x'], 'changed');
249 });
250 }
251
252 inputTextAreaValueOnetime(String tagName) {
253 var el = testDiv.append(new Element.tag(tagName));
254 bindings.add(nodeBind(el).bind('value', 42, oneTime: true));
255 expect(el.value, '42');
256 }
257
258 inputTextAreaNoPath(String tagName) {
259 var el = testDiv.append(new Element.tag(tagName));
260 var model = 42;
261 bindings.add(nodeBind(el).bind('value', new PathObserver(model)));
262 expect(el.value, '42');
263 }
264
265 inputTextAreaPathUnreachable(String tagName) {
266 var el = testDiv.append(new Element.tag(tagName));
267 var model = toObservable({});
268 bindings.add(nodeBind(el).bind('value', new PathObserver(model, 'a')));
269 expect(el.value, '');
270 }
271
272 test('Input.value',
273 () => inputTextAreaValueTest('input'));
274
275 test('Input.value - oneTime',
276 () => inputTextAreaValueOnetime('input'));
277
278 test('Input.value - no path',
279 () => inputTextAreaNoPath('input'));
280
281 test('Input.value - path unreachable',
282 () => inputTextAreaPathUnreachable('input'));
283
284 test('TextArea.value',
285 () => inputTextAreaValueTest('textarea'));
286
287 test('TextArea.value - oneTime',
288 () => inputTextAreaValueOnetime('textarea'));
289
290 test('TextArea.value - no path',
291 () => inputTextAreaNoPath('textarea'));
292
293 test('TextArea.value - path unreachable',
294 () => inputTextAreaPathUnreachable('textarea'));
295
296 test('Radio Input', () {
297 var input = new InputElement();
298 input.type = 'radio';
299 var model = toObservable({'x': true});
300 bindings.add(nodeBind(input).bind('checked', new PathObserver(model, 'x')));
301 expect(input.checked, true);
302
303 model['x'] = false;
304 expect(input.checked, true);
305 return new Future(() {
306 expect(input.checked, false,reason: 'model change should update checked');
307
308 input.checked = true;
309 dispatchEvent('change', input);
310 expect(model['x'], true, reason: 'input.checked should set model');
311
312 bindings[0].close();
313
314 input.checked = false;
315 dispatchEvent('change', input);
316 expect(model['x'], true,
317 reason: 'disconnected binding should not fire');
318 });
319 });
320
321 test('Input.value - user value rejected', () {
322 var model = toObservable({'val': 'ping'});
323
324 var rejector = new PathObserver(model, 'val');
325 rejector.open(() {
326 model['val'] = 'ping';
327 });
328
329 var el = new InputElement();
330 bindings.add(nodeBind(el).bind('value', new PathObserver(model, 'val')));
331
332 return new Future(() {
333 expect(el.value, 'ping');
334
335 el.value = 'pong';
336 dispatchEvent('input', el);
337
338 }).then(endOfMicrotask).then((_) {
339 // rejector will have set the bound value back to 'ping'.
340 expect(el.value, 'ping');
341
342 rejector.close();
343 });
344 });
345
346 test('Checkbox Input.checked', () {
347 var el = testDiv.append(new InputElement());
348 el.type = 'checkbox';
349
350 var model = toObservable({'x': true});
351 bindings.add(nodeBind(el).bind('checked', new PathObserver(model, 'x')));
352 expect(el.checked, true);
353
354 model['x'] = false;
355 expect(el.checked, true, reason: 'changes delivered async');
356 return new Future(() {
357 expect(el.checked, false);
358
359 el.click();
360 expect(model['x'], true);
361 }).then(endOfMicrotask).then((_) {
362
363 el.click();
364 expect(model['x'], false);
365 });
366 });
367
368 test('Checkbox Input.checked - oneTime', () {
369 var input = testDiv.append(new InputElement());
370 input.type = 'checkbox';
371 bindings.add(nodeBind(input).bind('checked', true, oneTime: true));
372 expect(input.checked, true, reason: 'checked was set');
373 });
374
375 test('Checkbox Input.checked - path unreachable', () {
376 var input = testDiv.append(new InputElement());
377 input.type = 'checkbox';
378 var model = toObservable({});
379 bindings.add(nodeBind(input).bind('checked', new PathObserver(model, 'x')));
380 expect(input.checked, false);
381 });
382
383 test('Checkbox Input.checked 2', () {
384 var model = toObservable({'val': true});
385
386 var el = testDiv.append(new InputElement());
387 el.type = 'checkbox';
388 bindings.add(nodeBind(el).bind('checked', new PathObserver(model, 'val')));
389 return new Future(() {
390 expect(el.checked, true);
391
392 model['val'] = false;
393 }).then(endOfMicrotask).then((_) {
394 expect(el.checked, false);
395
396 el.click();
397 expect(model['val'], true);
398
399 el.click();
400 expect(model['val'], false);
401
402 el.onClick.listen((_) {
403 expect(model['val'], true);
404 });
405 el.onChange.listen((_) {
406 expect(model['val'], true);
407 });
408
409 el.dispatchEvent(new MouseEvent('click', view: window));
410 });
411 });
412
413 test('Checkbox Input.checked - binding updated on click', () {
414 var model = toObservable({'val': true});
415
416 var el = new InputElement();
417 testDiv.append(el);
418 el.type = 'checkbox';
419 bindings.add(nodeBind(el).bind('checked', new PathObserver(model, 'val')));
420 return new Future(() {
421 expect(el.checked, true);
422
423 int fired = 0;
424 el.onClick.listen((_) {
425 fired++;
426 expect(model['val'], false);
427 });
428
429 el.dispatchEvent(new MouseEvent('click', view: window));
430
431 expect(fired, 1, reason: 'events dispatched synchronously');
432 });
433 });
434
435 test('Checkbox Input.checked - binding updated on change', () {
436 var model = toObservable({'val': true});
437
438 var el = new InputElement();
439 testDiv.append(el);
440 el.type = 'checkbox';
441 bindings.add(nodeBind(el).bind('checked', new PathObserver(model, 'val')));
442 return new Future(() {
443 expect(el.checked, true);
444
445 int fired = 0;
446 el.onChange.listen((_) {
447 fired++;
448 expect(model['val'], false);
449 });
450
451 el.dispatchEvent(new MouseEvent('click', view: window));
452
453 expect(fired, 1, reason: 'events dispatched synchronously');
454 });
455 });
456
457 test('Radio Input.checked', () {
458 var input = testDiv.append(new InputElement());
459 input.type = 'radio';
460 var model = toObservable({'x': true});
461 bindings.add(nodeBind(input).bind('checked', new PathObserver(model, 'x')));
462 expect(input.checked, true);
463
464 model['x'] = false;
465 expect(input.checked, true);
466 return new Future(() {
467 expect(input.checked, false);
468
469 input.checked = true;
470 dispatchEvent('change', input);
471 expect(model['x'], true);
472 });
473 });
474
475 test('Radio Input.checked - oneTime', () {
476 var input = testDiv.append(new InputElement());
477 input.type = 'radio';
478 bindings.add(nodeBind(input).bind('checked', true, oneTime: true));
479 expect(input.checked, true, reason: 'checked was set');
480 });
481
482 radioInputChecked2(host) {
483 var model = toObservable({'val1': true, 'val2': false, 'val3': false,
484 'val4': true});
485 var RADIO_GROUP_NAME = 'test';
486
487 var container = host.append(new DivElement());
488
489 var el1 = container.append(new InputElement());
490 el1.type = 'radio';
491 el1.name = RADIO_GROUP_NAME;
492 bindings.add(
493 nodeBind(el1).bind('checked', new PathObserver(model, 'val1')));
494
495 var el2 = container.append(new InputElement());
496 el2.type = 'radio';
497 el2.name = RADIO_GROUP_NAME;
498 bindings.add(
499 nodeBind(el2).bind('checked', new PathObserver(model, 'val2')));
500
501 var el3 = container.append(new InputElement());
502 el3.type = 'radio';
503 el3.name = RADIO_GROUP_NAME;
504 bindings.add(
505 nodeBind(el3).bind('checked', new PathObserver(model, 'val3')));
506
507 var el4 = container.append(new InputElement());
508 el4.type = 'radio';
509 el4.name = 'othergroup';
510 bindings.add(
511 nodeBind(el4).bind('checked', new PathObserver(model, 'val4')));
512
513 return new Future(() {
514 expect(el1.checked, true);
515 expect(el2.checked, false);
516 expect(el3.checked, false);
517 expect(el4.checked, true);
518
519 model['val1'] = false;
520 model['val2'] = true;
521 }).then(endOfMicrotask).then((_) {
522 expect(el1.checked, false);
523 expect(el2.checked, true);
524 expect(el3.checked, false);
525 expect(el4.checked, true);
526
527 el1.checked = true;
528 dispatchEvent('change', el1);
529 expect(model['val1'], true);
530 expect(model['val2'], false);
531 expect(model['val3'], false);
532 expect(model['val4'], true);
533
534 el3.checked = true;
535 dispatchEvent('change', el3);
536 expect(model['val1'], false);
537 expect(model['val2'], false);
538 expect(model['val3'], true);
539 expect(model['val4'], true);
540 });
541 }
542
543 test('Radio Input.checked 2', () => radioInputChecked2(testDiv));
544
545 test('Radio Input.checked 2 - ShadowRoot', () {
546 if (!ShadowRoot.supported) return null;
547
548 var shadowRoot = new DivElement().createShadowRoot();
549 return radioInputChecked2(shadowRoot);
550 });
551
552 radioInputCheckedMultipleForms(host) {
553 var model = toObservable({'val1': true, 'val2': false, 'val3': false,
554 'val4': true});
555 var RADIO_GROUP_NAME = 'test';
556
557 var container = testDiv.append(new DivElement());
558 var form1 = new FormElement();
559 container.append(form1);
560 var form2 = new FormElement();
561 container.append(form2);
562
563 var el1 = new InputElement();
564 form1.append(el1);
565 el1.type = 'radio';
566 el1.name = RADIO_GROUP_NAME;
567 bindings.add(
568 nodeBind(el1).bind('checked', new PathObserver(model, 'val1')));
569
570 var el2 = new InputElement();
571 form1.append(el2);
572 el2.type = 'radio';
573 el2.name = RADIO_GROUP_NAME;
574 bindings.add(
575 nodeBind(el2).bind('checked', new PathObserver(model, 'val2')));
576
577 var el3 = new InputElement();
578 form2.append(el3);
579 el3.type = 'radio';
580 el3.name = RADIO_GROUP_NAME;
581 bindings.add(
582 nodeBind(el3).bind('checked', new PathObserver(model, 'val3')));
583
584 var el4 = new InputElement();
585 form2.append(el4);
586 el4.type = 'radio';
587 el4.name = RADIO_GROUP_NAME;
588 bindings.add(
589 nodeBind(el4).bind('checked', new PathObserver(model, 'val4')));
590
591 return new Future(() {
592 expect(el1.checked, true);
593 expect(el2.checked, false);
594 expect(el3.checked, false);
595 expect(el4.checked, true);
596
597 el2.checked = true;
598 dispatchEvent('change', el2);
599 expect(model['val1'], false);
600 expect(model['val2'], true);
601
602 // Radio buttons in form2 should be unaffected
603 expect(model['val3'], false);
604 expect(model['val4'], true);
605
606 el3.checked = true;
607 dispatchEvent('change', el3);
608 expect(model['val3'], true);
609 expect(model['val4'], false);
610
611 // Radio buttons in form1 should be unaffected
612 expect(model['val1'], false);
613 expect(model['val2'], true);
614 });
615 }
616
617 test('Radio Input.checked - multiple forms', () {
618 return radioInputCheckedMultipleForms(testDiv);
619 });
620
621 test('Radio Input.checked - multiple forms - ShadowRoot', () {
622 if (!ShadowRoot.supported) return null;
623
624 var shadowRoot = new DivElement().createShadowRoot();
625 return radioInputCheckedMultipleForms(shadowRoot);
626 });
627
628 test('Select.selectedIndex', () {
629 var select = new SelectElement();
630 testDiv.append(select);
631 var option0 = select.append(new OptionElement());
632 var option1 = select.append(new OptionElement());
633 var option2 = select.append(new OptionElement());
634
635 var model = toObservable({'val': 2});
636
637 bindings.add(
638 nodeBind(select).bind('selectedIndex', new PathObserver(model, 'val')));
639 return new Future(() {
640 expect(select.selectedIndex, 2);
641
642 select.selectedIndex = 1;
643 dispatchEvent('change', select);
644 expect(model['val'], 1);
645 });
646 });
647
648 test('Select.selectedIndex - oneTime', () {
649 var select = new SelectElement();
650 testDiv.append(select);
651 var option0 = select.append(new OptionElement());
652 var option1 = select.append(new OptionElement());
653 var option2 = select.append(new OptionElement());
654
655 bindings.add(nodeBind(select).bind('selectedIndex', 2, oneTime: true));
656 return new Future(() => expect(select.selectedIndex, 2));
657 });
658
659 test('Select.selectedIndex - invalid path', () {
660 var select = new SelectElement();
661 testDiv.append(select);
662 var option0 = select.append(new OptionElement());
663 var option1 = select.append(new OptionElement());
664 option1.selected = true;
665 var option2 = select.append(new OptionElement());
666
667 var model = toObservable({'val': 'foo'});
668
669 bindings.add(
670 nodeBind(select).bind('selectedIndex', new PathObserver(model, 'val')));
671 return new Future(() => expect(select.selectedIndex, 0));
672 });
673
674 test('Select.selectedIndex - path unreachable', () {
675 var select = new SelectElement();
676 testDiv.append(select);
677 var option0 = select.append(new OptionElement());
678 var option1 = select.append(new OptionElement());
679 option1.selected = true;
680 var option2 = select.append(new OptionElement());
681
682 var model = toObservable({});
683
684 bindings.add(
685 nodeBind(select).bind('selectedIndex', new PathObserver(model, 'val')));
686 return new Future(() => expect(select.selectedIndex, 0));
687 });
688
689 test('Option.value', () {
690 var option = testDiv.append(new OptionElement());
691 var model = toObservable({'x': 42});
692 bindings.add(nodeBind(option).bind('value', new PathObserver(model, 'x')));
693 expect(option.value, '42');
694
695 model['x'] = 'Hi';
696 expect(option.value, '42');
697 return new Future(() => expect(option.value, 'Hi'));
698 });
699
700 test('Option.value - oneTime', () {
701 var option = testDiv.append(new OptionElement());
702 bindings.add(nodeBind(option).bind('value', 42, oneTime: true));
703 expect(option.value, '42');
704 });
705
706 test('Select.value', () {
707 var select = testDiv.append(new SelectElement());
708 testDiv.append(select);
709 var option0 = select.append(new OptionElement());
710 var option1 = select.append(new OptionElement());
711 var option2 = select.append(new OptionElement());
712
713 var model = toObservable({
714 'opt0': 'a',
715 'opt1': 'b',
716 'opt2': 'c',
717 'selected': 'b'
718 });
719
720 bindings.add(
721 nodeBind(option0).bind('value', new PathObserver(model, 'opt0')));
722 bindings.add(
723 nodeBind(option1).bind('value', new PathObserver(model, 'opt1')));
724 bindings.add(
725 nodeBind(option2).bind('value', new PathObserver(model, 'opt2')));
726 bindings.add(
727 nodeBind(select).bind('value', new PathObserver(model, 'selected')));
728 return new Future(() {
729 expect(select.value, 'b');
730
731 select.value = 'c';
732 dispatchEvent('change', select);
733 expect(model['selected'], 'c');
734
735 model['opt2'] = 'X';
736 }).then(endOfMicrotask).then((_) {
737 expect(select.value, 'X');
738 expect(model['selected'], 'X');
739
740 model['selected'] = 'a';
741 }).then(endOfMicrotask).then((_) {
742 expect(select.value, 'a');
743 });
744 });
745 }
OLDNEW
« no previous file with comments | « packages/template_binding/test/custom_element_bindings_test.html ('k') | packages/template_binding/test/node_bind_test.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698