-
Notifications
You must be signed in to change notification settings - Fork 9
/
temp.xml
2847 lines (2270 loc) · 453 KB
/
temp.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Rémi Bourgarel's Blog</title>
<atom:link href="/feed.xml" rel="self" type="application/rss+xml"/>
<link>https://remibou.github.io/</link>
<description></description>
<pubDate>Wed, 29 Jan 2020 22:30:46 +0000</pubDate>
<item>
<title>Test Your Jsinterop Code With Cypress</title>
<link>/Test-your-JSInterop-code-with-cypress/</link>
<guid isPermaLink="true">/Test-your-JSInterop-code-with-cypress/</guid>
<description><h1 id="test-your-jsinterop-code-with-cypressio">Test your JSInterop code with cypress.io</h1>
<p><a href="https://www.cypress.io/">Cypress.io</a> is a game changer in the world of web E2E test. So far it was dominated by WebDriver based framework but it has the following advantages :</p>
<ul>
<li>It’s easy to setup</li>
<li>It’s easy to integrate into a CI pipeline</li>
<li>The API are fine (I still don’t like the assertion methods)</li>
<li>The debug information it provides are golden and makes your tests easy to fix</li>
<li>There is a lot of methods for making your tests less flaky (you do’nt have to add random wait every 2 lines)</li>
</ul>
<p>The only disadvantage being the maturity of the tool so there is some missing pieces like built-in file upload or spying of fetch request but the community is quite large and there is always a 3rd party script/lib for fixing what is missing.</p>
<h2 id="how-to-test-for-a-method-call">How to test for a method call</h2>
<p>In cypress there is multiple methods for spying or stubbing the navigator methods for instance :</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nx">context</span><span class="p">(</span><span class="dl">'</span><span class="s1">window.console</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">before</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">Check console methods called</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nb">window</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">w</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">spy</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">console</span><span class="p">,</span> <span class="dl">"</span><span class="s2">log</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#btn-console-do-test</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">).</span><span class="nx">be</span><span class="p">.</span><span class="nx">called</span><span class="p">.</span><span class="nx">calledTwice</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>
<p>This test clicks on a button and then expect the console.log to be called twice.</p>
<h2 id="the-problem-with-jsinterop">The problem with JSInterop</h2>
<p>This would work very well in a pure js application. If the console.log method calls are done with JSInterop like in a Blazor WASM app :</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="n">jsRuntime</span><span class="p">.</span><span class="nf">InvokeVoidAsync</span><span class="p">(</span><span class="s">"console.log"</span><span class="p">,</span><span class="s">"test"</span><span class="p">);</span>
<span class="c1">//or</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"test"</span><span class="p">);</span>
</code></pre></div></div>
<p>This would fail with this error :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>blazor.webassembly.js:1 WASM: Unhandled exception rendering component:
blazor.webassembly.js:1 WASM: Microsoft.JSInterop.JSException: The value 'window.console.log' is not a function.
blazor.webassembly.js:1 WASM: Error: The value 'window.console.log' is not a function.
blazor.webassembly.js:1 WASM: at p (http://localhost:5000/_framework/blazor.webassembly.js:1:9063)
blazor.webassembly.js:1 WASM: at http://localhost:5000/_framework/blazor.webassembly.js:1:9605
blazor.webassembly.js:1 WASM: at new Promise (&lt;anonymous&gt;)
blazor.webassembly.js:1 WASM: at Object.beginInvokeJSFromDotNet (http://localhost:5000/_framework/blazor.webassembly.js:1:9579)
blazor.webassembly.js:1 WASM: at _mono_wasm_invoke_js_marshalled (http://localhost:5000/_framework/wasm/mono.js:1:165611)
blazor.webassembly.js:1 WASM: at wasm-function[6221]:0x11936a
blazor.webassembly.js:1 WASM: at wasm-function[1431]:0x402ee
blazor.webassembly.js:1 WASM: at wasm-function[636]:0x147cf
blazor.webassembly.js:1 WASM: at wasm-function[4996]:0xeb135
blazor.webassembly.js:1 WASM: at wasm-function[3247]:0xa0666
blazor.webassembly.js:1 WASM: at System.Threading.Tasks.ValueTask`1[TResult].get_Result () &lt;0x20a9640 + 0x0002c&gt; in &lt;5745b1bd6f4246d7aee8c81307e6355a&gt;:0
blazor.webassembly.js:1 WASM: at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync (Microsoft.JSInterop.IJSRuntime jsRuntime, System.String identifier, System.Object[] args) &lt;0x2081800 + 0x000e4&gt; in &lt;3eedf0ca90ca4e72bf6870618ca98c7c&gt;:0
</code></pre></div></div>
<p>This is due to this code in Microsoft.JSInterop you can find in <a href="https://github.com/dotnet/extensions/blob/master/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts">this file</a> :</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">findJSFunction</span><span class="p">(</span><span class="nx">identifier</span><span class="p">:</span> <span class="nx">string</span><span class="p">):</span> <span class="nb">Function</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">cachedJSFunctions</span><span class="p">.</span><span class="nx">hasOwnProperty</span><span class="p">(</span><span class="nx">identifier</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">cachedJSFunctions</span><span class="p">[</span><span class="nx">identifier</span><span class="p">];</span>
<span class="p">}</span>
<span class="kd">let</span> <span class="nx">result</span><span class="p">:</span> <span class="nx">any</span> <span class="o">=</span> <span class="nb">window</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">resultIdentifier</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">window</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">lastSegmentValue</span><span class="p">:</span> <span class="nx">any</span><span class="p">;</span>
<span class="nx">identifier</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">).</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">segment</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">segment</span> <span class="k">in</span> <span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">lastSegmentValue</span> <span class="o">=</span> <span class="nx">result</span><span class="p">;</span>
<span class="nx">result</span> <span class="o">=</span> <span class="nx">result</span><span class="p">[</span><span class="nx">segment</span><span class="p">];</span>
<span class="nx">resultIdentifier</span> <span class="o">+=</span> <span class="dl">'</span><span class="s1">.</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">segment</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`Could not find '</span><span class="p">${</span><span class="nx">segment</span><span class="p">}</span><span class="s2">' in '</span><span class="p">${</span><span class="nx">resultIdentifier</span><span class="p">}</span><span class="s2">'.`</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">result</span> <span class="k">instanceof</span> <span class="nb">Function</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">result</span> <span class="o">=</span> <span class="nx">result</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="nx">lastSegmentValue</span><span class="p">);</span>
<span class="nx">cachedJSFunctions</span><span class="p">[</span><span class="nx">identifier</span><span class="p">]</span> <span class="o">=</span> <span class="nx">result</span><span class="p">;</span>
<span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`The value '</span><span class="p">${</span><span class="nx">resultIdentifier</span><span class="p">}</span><span class="s2">' is not a function.`</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>You see the problem ? No ? Well it’s not obvious :</p>
<ul>
<li>In JS types are defined by window. eg : if you have an iframe then the type “Function” inside the iframe is not the same as the “Function” type in the parent.</li>
<li>Cypress uses iframes for running the tests (that’s why you are not limited like in WebDriver)</li>
<li>when you call cy.spy, it changes the definition of console.log, so its type becomes a “Function” in the context of the runner iframe, not the app.</li>
</ul>
<h2 id="how-do-i-fix-this-">How do I fix this ?</h2>
<p>Fortunately Javascript allows us to do very stupid things, like changing an object prototype on the fly ! After the fix, my Cypress code test looks this :</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nx">context</span><span class="p">(</span><span class="dl">'</span><span class="s1">window.console</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">before</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="dl">'</span><span class="s1">/console</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">Check console methods called</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nb">window</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">w</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">spy</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">console</span><span class="p">,</span> <span class="dl">"</span><span class="s2">log</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">w</span><span class="p">.</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">.</span><span class="nx">__proto__</span> <span class="o">=</span> <span class="nx">w</span><span class="p">.</span><span class="nb">Function</span><span class="p">;</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#btn-console-do-test</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">).</span><span class="nx">be</span><span class="p">.</span><span class="nx">called</span><span class="p">.</span><span class="nx">calledThrice</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>
<p>Notice the line after the spy (<a href="https://docs.cypress.io/api/commands/spy.html#Syntax">which is synchronous</a>) which changes console.log prototype, this makes the “instanceof Function” condition pass and my test run successfully.</p>
<p>I created the following Cypress command to reduce code duplication</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nx">Cypress</span><span class="p">.</span><span class="nx">Commands</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">spyFix</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">object</span><span class="p">,</span> <span class="nx">method</span><span class="p">,</span> <span class="nb">window</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">spy</span><span class="p">(</span><span class="nx">object</span><span class="p">,</span> <span class="nx">method</span><span class="p">);</span>
<span class="nx">object</span><span class="p">[</span><span class="nx">method</span><span class="p">].</span><span class="nx">__proto__</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nb">Function</span><span class="p">;</span>
<span class="p">});</span>
<span class="nx">context</span><span class="p">(</span><span class="dl">'</span><span class="s1">window.console</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">before</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="dl">'</span><span class="s1">/console</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">Check console methods called</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nb">window</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">w</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">spyFix</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">console</span><span class="p">,</span> <span class="dl">"</span><span class="s2">log</span><span class="dl">"</span><span class="p">,</span> <span class="nx">w</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#btn-console-do-test</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">w</span><span class="p">.</span><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">).</span><span class="nx">be</span><span class="p">.</span><span class="nx">called</span><span class="p">.</span><span class="nx">calledThrice</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>I’m happy I found an easy way to fix this, there is other ways with “new win.Function” or by overriding some method in window.DotNet but they require more line of code and are much more complicated.</p>
</description>
<pubDate>Wed, 29 Jan 2020 00:00:00 +0000</pubDate>
</item>
<item>
<title>Should I Peek Blazor</title>
<link>/Should-I-peek-Blazor/</link>
<guid isPermaLink="true">/Should-I-peek-Blazor/</guid>
<description><h1 id="should-i-peek-blazor-">Should I peek Blazor ?</h1>
<p><em>ALL THIS BLOG POST CONTENT IS MY OPINION. IF YOU HAVE A DIFFERENT ONE PLEASE POST A COMMENT</em></p>
<p>On Twitter or Reddit I often see question about whether Blazor is a good choice. In this blog post I will try to ask questions that might help you making this decision.</p>
<h2 id="quick-presentation">Quick presentation</h2>
<h3 id="what-is-blazor-">What is Blazor ?</h3>
<p>You will get a good description of Blazor from the official website : <a href="https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor">https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor</a>. It belongs to the family of the SPA framework like Angular, react or vue.</p>
<p>Blazor is a framework for building web UI using C#. It has 2 ways of working :</p>
<ul>
<li>Client-Side : This way of working looks a lot like Angular or React. Your netsandard assemblies are downloaded on the browser and executed by a .NET runtime build for WebAssembly. This runtime is called “monowasm” it is developped by the mono team (I think this is because they have the experience in working with environment with low ressources like a browser or a phone).</li>
</ul>
<p><img src="https://docs.microsoft.com/fr-fr/aspnet/core/blazor/index/_static/blazor-webassembly.png?view=aspnetcore-3.1" alt="Blazor client-side" title="Blazor client-side" /></p>
<ul>
<li>Server-Side : A SignalR connection between the browser and the server is opened, when something happens on one of the side (eg : a click on a button), the server sends the DOM changes that must be done to the client. I think this was created.</li>
</ul>
<p><img src="https://docs.microsoft.com/fr-fr/aspnet/core/blazor/index/_static/blazor-server.png?view=aspnetcore-3.1" alt="Blazor server-side" title="Blazor server-side" /></p>
<h2 id="should-i-use-it-">Should I use it ?</h2>
<p>It depends.</p>
<p>This is very broad question to which I will answer like a politician : with more questions :).</p>
<h3 id="should-you-use-an-spa-framework-in-the-first-time-">Should you use an SPA framework in the first time ?</h3>
<p>You never start a project by saying “I need a database”. You analyze your spec and read that you need to store some data, and you decide to use a relationnal or document database.
Here it’s the same thing. I make the distinction between a web application and a web site :</p>
<ul>
<li>A web application requires a lot of interaction with the user : video game (like <a href="https://www.ageofascent.com/">https://www.ageofascent.com/</a>), SaaS (like Office365), an intranet or a tool (like pgadmin), there is little to no need for SEO, first load time can be a bit long and most of the users are active.</li>
<li>A web site is a collection of web page where the user navigates between them with link. Most fo the workload is read only, there is a LOT of traffic, most of the users come and go, SEO needs is crucial, page must be displayed in less than 100ms …</li>
</ul>
<p>Most of the time a project is a combination of both : you’ll build a cms (web app) that will generate a blog (web site). Or there is a shopping cart (web app) part in a online retailer site (web site).
I would still suggest that either you split the 2 aspectss in 2 distinct projects using 2 differents technologies or you choose one and pick your toolchain with it.</p>
<p>2 examples :</p>
<ul>
<li>StackOverflow is a website even though there is some forms for submiting content, and searching things. So they adopted technologies for building web site : server side rendering with some jQuery.</li>
<li>Facebook is a web app even though there is a lot of readonly load and SEO needs are important. But this level of interaction would be very hard to obtain with jQuery.</li>
</ul>
<p>From the question above I can draw this conclusion : <strong>IF YOU ARE BUILDING A WEB SITE, CLOSE THIS WINDOW, YOU DON’T NEED BLAZOR</strong>, you don’t need Angular or react, just use server side rendering like ASPNET Core MVC or Symphony and you will be ok.</p>
<h4 id="isnt-it-silverlight-over-again-">Isn’t it Silverlight over again ?</h4>
<p>Silverlight or Flash were used for creating Web UI. But that’s all there is in common between Blazor and Silverlight or Flash :</p>
<ul>
<li>Blazor uses Razor template language, so it uses web standards for managing UI components : HTML, CSS, JS, WebAssembly, WebSocket …</li>
<li><a href="https://github.com/dotnet/aspnetcore/tree/master/src/Components">Blazor is open source</a>.</li>
<li>It doesn’t require any security settings on the client.</li>
<li>It doesn’t require any 3rd party install on the client.</li>
<li>It cannot do anything in the user computer that JS or WebAssembly cannot do.</li>
</ul>
<p>Results : this doesn’t change anything.</p>
<h4 id="isnt-it-webforms-over-again-with-the-infamous-viewstate-">Isn’t it WebForms over again with the infamous ViewState ?</h4>
<p>WebForms was used for building Web UI and was provided by .NET but there is a lot of difference with Blazor :</p>
<ul>
<li>Blazor does not create an abstraction on the produced HTML or JS or CSS.</li>
<li><a href="https://github.com/dotnet/aspnetcore/tree/master/src/Components">Blazor is open source</a>.</li>
<li>Blazor client-side is executed on the client.</li>
<li>There is no state exchange between client and server with Blazor server-side (like ViewState).</li>
</ul>
<p>Results : this doesn’t change anything.</p>
<h3 id="do-we-have-strong-seo-needs-">Do we have strong SEO needs ?</h3>
<p>Most of the search engine bot in the world don’t execute any client-side code. <a href="https://developers.google.com/search/docs/guides/javascript-seo-basics">Google Bot executes client-side code</a> because it uses a headless chromium, but they recommend using server side rendering because their robot will index your website faster.</p>
<p>Blazor server side provides server-side pre-rendering : on the first request, full HTML page is computed and send to the web client (browser or bot), so you shouldn’t have any SEO problems here.</p>
<p>But for Blazor client-side only the empty index.html is send to the client so the bot has to execute some code and I am not sure Google bot allows WebAssembly execution (I should test it).</p>
<p>Results : If you have strong SEO needs, Blazor client-side is not recommended.</p>
<h3 id="do-you-need-to-do-dom-manipulation-">Do you need to do DOM manipulation ?</h3>
<p>While it’s a bad practice to do it with a framework like this, some people will still do it. Here you can do it with JS interop but there is no guarantee that your change won’t be erased because Blazor keeps a state of the app in memory.</p>
<p>Results : this doesn’t change anything, I don’t think you should do DOM manipulation with a front-end framework.</p>
<h3 id="does-your-app-needs-to-be-available-offline-">Does your app needs to be available offline ?</h3>
<p>When you are using a web application on your phone, the internet conection is not always stable. In Google Doc this is not a problem and your change will be saved when the connection is restored. With Blazor client-side there is no problem as long as you manage the connection error on your code (and you can use <a href="https://github.com/App-vNext/Polly">Polly</a> for that), you will be fine.</p>
<p>But in Blazor server-side the GUI state is stored on the server so if your user looses the connection, the application will stop working and the user might fail to recover his session when the connection is back but <a href="https://docs.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-3.1#blazor-server">depending on the duration of the outage</a> it might not be possible. Here is the result for the user when reonnection fails :</p>
<p><img src="/assets/img/ScreenShot-Blazor-Reconnect.png" alt="Blazor server-side reconnect fails" title="Blazor server-side reconnect fails" /></p>
<p>Results : this is the biggest downside of Blazor server-side. If it’s a problem, depending on your load, you could increase the timeout before a session is destroyed after a disconnect.</p>
<h3 id="does-bazor-misses-anything-from-other-spa-framework-">Does Bazor misses anything from other SPA framework ?</h3>
<p>So far Blazor provides all the features you need for building a complete application : templating, forms, network, authentication, authorization, libraries etc … like with any framework, stop wondering “Where is the feature Y that exists in framework X ?” but read the documentation and follow the framework principles or you will miss its added value : if you stop reading about Blazor because it doesn’t provide decorator like in Angular you will miss features like CascadingParameter that are not in Angular.</p>
<p>Even though Blazor exists since only a dozen months, you can use all the netstandard library available in <a href="nuget.org">nuget.org</a> for your frontend project and that is a huge selling point. When a new technology comes around, you have to wait a bit until there is nice community project emerging or until the included libs are stable. This is not the case here : most of the Base Class LIbrary is copied from the mono runtime and the open source community produces a lot of great library.</p>
<p>Results : Blazor is already a great/stable/complete framework with many tools and don’t try to use Blazor like Angular (vice-versa).</p>
<h3 id="do-we-need-to-change-our-licensing-for-using-blazor-">Do we need to change our licensing for using Blazor ?</h3>
<p>Blazor is part of the <a href="https://github.com/dotnet/aspnetcore">ASPNET Core repository</a> and is released under the Apache 2.0 license. In my understanding, from a user point of view there is no constraint.</p>
<p>Results : this doesn’t change anything.</p>
<h3 id="do-we-need-to-upgrade-our-server-spec-for-using-blazor-">Do we need to upgrade our server spec for using Blazor ?</h3>
<p>Blazor client-side binaries are heavy : <a href="https://channel9.msdn.com/Events/dotnetConf/Focus-on-Blazor/Blazor-Futures-WebAssembly-PWAs-Hybrid-Native">around 2Mo for the default app</a> and most of it is the runtime and the system libraries, so when your app will grow you won’t see this number growing much. Unless you add reference to heavy library (<a href="https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/blazor/configure-linker?view=aspnetcore-3.1">and still there is the illinker</a> that will remove any unused part o the assemblies).</p>
<p>Blazor server-side can be scary from a scale point of view: all your user GUI state will be stored server side and you will keep an opened connection during their whole session. But the ASPNET Team is awesome. Seriously, they did some measurement and the result should be enough for 99.9999% of the app out there (don’t make architecture choice based on Twitter, Netflix or Pornhub scale). Here is a screenshot of a presentation Dan Roth (Program manager at MSFT) did in Jan ‘20 :</p>
<p><img src="/assets/img/ScreenShot-Blazor-Serer-Perf.png" alt="Blazor server-side performance" title="Blazor server-side performance" /></p>
<p>5000 concurrent connection is a LOT, if you have this level of load and every user stays 1 hour on your app, it’s 3,7 Million monthly users with a 1 CPU and 3.5 GB RAM server.</p>
<p>Results : server side performance are really great and shouldn’t need any investment</p>
<h3 id="does-the-user-will-have-a-good-experience-with-our-app-">Does the user will have a good experience with our app ?</h3>
<p>Client-side performance is a difficult subject as there is much more variable than with server-side. You have to take into account the client envionment : latency, bandwidth, CPU, memory, other app running … But there is some tools for getting an idea of the user exeperience.</p>
<p>I did some tests with <a href="https://developers.google.com/web/tools/lighthouse/">Lighthouse</a>. I decided to create a small example where I compare 3 app created from the cli, one Blazor client-side, one Blazor server-side and one with Angular 8. You can find the code <a href="https://github.com/RemiBou/Blog-angular-blazor-performance">here</a>.
Because the tests where done locally the Time To First Byte is not relevant. The build and run were done with production settings (Release configuration and –prod flag).</p>
<h4 id="results-with-cpu-and-bandwidth-throtling-on">Results with CPU and bandwidth throtling on</h4>
<table>
<thead>
<tr>
<th>Metric</th>
<th style="text-align: center">Blazor Client Side</th>
<th style="text-align: center">Blazor Server Side</th>
<th style="text-align: right">Angular</th>
</tr>
</thead>
<tbody>
<tr>
<td>Performance score</td>
<td style="text-align: center">65</td>
<td style="text-align: center">88</td>
<td style="text-align: right">98</td>
</tr>
<tr>
<td>First contentful paint</td>
<td style="text-align: center">1.5sec</td>
<td style="text-align: center">3.2 sec</td>
<td style="text-align: right">2.0sec</td>
</tr>
<tr>
<td>Time to interactive</td>
<td style="text-align: center">19.5sec</td>
<td style="text-align: center">3.3sec</td>
<td style="text-align: right">2.0sec</td>
</tr>
<tr>
<td>JS / Bin size uncompressed</td>
<td style="text-align: center">2100KB</td>
<td style="text-align: center">210KB</td>
<td style="text-align: right">628KB</td>
</tr>
</tbody>
</table>
<ul>
<li>The Blazor client-side size is not enormous (<a href="https://discuss.httparchive.org/t/tracking-page-weight-over-time/1049">as of July ‘17 the average page size on the internet is 3MB</a>) but still it’s 3-4x bigger than the Angular app and 10x the Blazor server-side size. The team is working on reducing this (1.5MB is the tartget for the release). But still you will have to be careful with the library your are using on your front-end project, just like you would do with Angular.</li>
<li>The time to interactive is quite high for the Blazor client-side, maybe the reduction of binary size or the rise of AoT will solve this.</li>
<li>Blazor server-side performance are close to Angular</li>
</ul>
<p>Note : This test doesn’t use gzip compression for the web server so you can reduce the Blazor ouput by 40% and Angular by 60% (from my experience).</p>
<h4 id="results-with-cpu-and-bandwidth-throtling-off">Results with CPU and bandwidth throtling off</h4>
<table>
<thead>
<tr>
<th>Metric</th>
<th style="text-align: center">Blazor Client Side</th>
<th style="text-align: center">Blazor Server Side</th>
<th style="text-align: right">Angular</th>
</tr>
</thead>
<tbody>
<tr>
<td>Performance score</td>
<td style="text-align: center">100</td>
<td style="text-align: center">100</td>
<td style="text-align: right">98</td>
</tr>
<tr>
<td>First contentful paint</td>
<td style="text-align: center">0.2sec</td>
<td style="text-align: center">0.2sec</td>
<td style="text-align: right">2.0sec</td>
</tr>
<tr>
<td>Time to interactive</td>
<td style="text-align: center">1.6sec</td>
<td style="text-align: center">0.2sec</td>
<td style="text-align: right">2.0sec</td>
</tr>
<tr>
<td>JS / Bin size uncompressed</td>
<td style="text-align: center">2100KB</td>
<td style="text-align: center">210KB</td>
<td style="text-align: right">628KB</td>
</tr>
</tbody>
</table>
<ul>
<li>Blazor client-side performs better with better CPU and better bandwidth (no sh#t !), the Time to Interactive is now acceptable.</li>
<li>Blazor server-side performance are better than Angular</li>
</ul>
<p>Results : You have to decide, depending on the kind of hardware your user are running on (maybe you are creating an intranet for a very rich lawyer and they all have great hardware or it’s a public school and they didn’t change anything since 85), the bandwidth and the kind of UX you need to provide (20 sec to wait at the beginning of each day is not a lot). Performance IS a requirement, ask your stakeholder what they need. Blazor server-side seems to be performing very well.</p>
<h3 id="do-we-need-to-setup-some-training-for-blazor-">Do we need to setup some training for Blazor ?</h3>
<p>If your team already uses ASPNET Core MVC or Razor Pages, there isn’t a lot of things to learn. If you already know ASPNET, you will feel like it’s just an other library that will enable a lot of things. From a tooling perspective you can still use Visual Studio, nuget, dotnet cli, Azure DevOps, your favorite lib …</p>
<p>Resuts : that’s a HUGE selling point for Blazor. ASPNET Core is now a full stack framework, you don’t need multiple skills or development environment for building a Web Application.</p>
<h3 id="how-long-will-it-take-to-set-our-servers-up-">How long will it take to set our servers up ?</h3>
<p>Blazor Client-side : there is no additionnal setup to do if you use the hosted template because your ASPNET app will embed the client-side binaries and all the necessary configurations (mime type setup …). If you need to publish your app in a standalone app then you can read the instructions <a href="https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/blazor/webassembly?view=aspnetcore-3.1">here</a>.</p>
<p>Blazor Server-side : You need to setup WebSocket because it’s the most efficient way to handle SignalR connection. You can find more informations <a href="https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/blazor/server?view=aspnetcore-3.1">here</a>.</p>
<p>Resuts : that’s again a nice selling point for Blazor, it will change nearly nothing to your deployment scripts and server setup.</p>
<h3 id="is-blazor-production-ready-">Is Blazor production ready ?</h3>
<p>Blazor client-side :</p>
<ul>
<li><a href="https://github.com/dotnet/aspnetcore/issues?utf8=%E2%9C%93&amp;q=is%3Aopen+is%3Aissue++label%3Ablazor-wasm+">There is currently</a> 67 Opened issues (12 bugs) on the ASPNET repo. You can browse the opened one and see if they are showstopper for you. The only thing that might be a showstopper is this <a href="https://github.com/dotnet/aspnetcore/issues/5477">one</a>, where some people reported that their firewall blocked download of client-side binaries because their url ended with “.dll”. It’s planned to be fixed on 3.2 preview4 which is planned for April (before the first release in May).</li>
<li><strong>Blazor client-side was not released yet</strong>, you might want to wait before using it in production. But it shares a lot with Blazor server-side which was released 4 month ago, the team will certainy not published a lot of breaking changes in the syntax or the framework mechanism.</li>
</ul>
<p>Blazor server-side :</p>
<ul>
<li><a href="https://github.com/dotnet/aspnetcore/issues?utf8=%E2%9C%93&amp;q=is%3Aopen+is%3Aissue++label%3Aarea-blazor+">There is currently</a> 453 Opened issues (87 bugs) on the ASPNET repo. You can browse the opened one and see if there are showstopper for you.</li>
</ul>
<p>Results : if you prefer Blazor client-side, then I suggest you wait until May for the release. For Blazor server side, it is production ready and used (<a href="https://channel9.msdn.com/Events/dotnetConf/Focus-on-Blazor/Welcome-to-Blazor">see some example here</a>).</p>
<h3 id="is-it-compatible-with-most-browsers-">Is it compatible with most browsers ?</h3>
<p>Blazor client-side :</p>
<ul>
<li><a href="https://caniuse.com/#feat=wasm">88,53%</a> os the internet user base uses a browser that can execute WebAssembly.</li>
<li>Given the output size, you need a decent bandwidth for having a nice user experience (2MB takes 8sec to download on a 2Mbps connection, <a href="https://en.wikipedia.org/wiki/List_of_countries_by_Internet_connection_speeds">Avg worldwide bandwidth is 5.1Mbps</a>).</li>
<li>Like any client-side executed code, the user experience is dependant on the user CPU. Don’t forget to test your app with slow CPU to be sure that the user experience is still OK. I can’t find any data about the average CPU in the world, so you might need to add some APM to your app, like Application Insight, that will centralize browser loading time so you have a clue of the user experience on your app.</li>
</ul>
<p>Blazor server-side :</p>
<ul>
<li>it’s compatible with almost any browser as <a href="https://docs.microsoft.com/en-us/aspnet/signalr/overview/getting-started/introduction-to-signalr">it uses many connection technique</a> (long polling, WebSocket) and choose the best available for your browser.</li>
</ul>
<p>Results : like with performance, that’s your call. For me the size argument is relevant only if you are targeting mobile users.</p>
<h3 id="can-it-open-new-opportunities-">Can it open new opportunities ?</h3>
<p>Yes hundred times. Blazor seems to be the next UI framework for Microsoft as <a href="https://channel9.msdn.com/Events/dotnetConf/Focus-on-Blazor/Blazor-Futures-WebAssembly-PWAs-Hybrid-Native">they are working</a> for desktop and mobile app, so in the near future you will be able to build a full application for any environment with .NET core and Blazor.</p>
<p>Results : that’s a good point for Blazor.</p>
<h3 id="will-we-develop-faster-with-blazor-">Will we develop faster with Blazor ?</h3>
<p>That’s highly subjective but I can give you a few arguments :</p>
<ul>
<li>The fact that you can share code between client and server will greatly improve your productivity. Imagine this : you code the same validation for client-side or server-side, the object you exchange between the client and server are defined only once, even your constants can be shared :D.</li>
<li>The fact you use only one IDE has a few advantages.</li>
<li>The fact that you will use one syntax accross your fullstack will improve onboarding of unexperienced developer.</li>
<li>Refactoring becomes way easier (for instance renaming a field)</li>
<li>You can debug both type of app just like you would do with a MEAN stack : on the browser for client-side code, and on the server for server-side. But this will change before the Blazor client-side release in May as you should be able to debug it in Visual Studio as well.</li>
<li>The fact that the deployment story doesn’t change will make your DevOps work more on the Dev than on learning how to setup nginx or build a production angular app.</li>
<li>When you need some JS function or Browser API it will need a bit more work.</li>
</ul>
<p>Results : after reading this you should have decided to use Blazor :D</p>
<h3 id="should-we-use-blazor-with-our-nodegospringetc-backend-">Should we use Blazor with our Node/Go/Spring/etc backend ?</h3>
<p>Well that a tough one. Honestly I don’t know, the point of Blazor is mostly its integration in the full .NET ecosystem, using it with Spring seems pointless. BUT I have been doing some Angular developement for a while and honestly I prefer Razor / C# syntax to Type Script and Angular templating. But here it’s a matter of taste and there isn’t any objective way for answering.</p>
<p>Results : there is no reason to not use it, see with your team members. If they like Java maybe they will prefer Blazor to Angular or React.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I can see one situation where the choice would be obvious for me : an ASPNET Core web APP (not web site), I would do the GUI with Blazor. I would pick server-side if the connection problem does not apply, client-side else.</p>
<p>On every other case I would follow my question list and do the choice with the team. But still I beleive in this project and I believe it will be around for a long time.</p>
<p>We often compare framework and library with hammer and screwdriver while this is not wrong, there is no right choice. Angular, React, Blazor, Vue are different brands of screwdriver, there is some difference, but most of the time the decision is based on feelings, taste and experience.</p>
</description>
<pubDate>Thu, 16 Jan 2020 00:00:00 +0000</pubDate>
</item>
<item>
<title>Make Your Blazor Development Faster</title>
<link>/Make-your-Blazor-development-faster/</link>
<guid isPermaLink="true">/Make-your-Blazor-development-faster/</guid>
<description><h1 id="how-to-improve-your-development-experience-with-blazor">How to improve your development experience with Blazor</h1>
<p>At first Blazor development can be a bit slow. When doing a change on a razor file you need to do the following steps :</p>
<ul>
<li>rebuild your front end project</li>
<li>rebuild the backend</li>
<li>restart your backend</li>
<li>refresh the browser</li>
<li>go back to your work</li>
</ul>
<p>In this article I’ll explore a few things for improving life of Blazor developers.</p>
<h2 id="improve-build-time-by-disabling-illinker">Improve build time by disabling Illinker</h2>
<p>The Illinker is a great tool as it removes all the unused things from the dll files produced for your project. The second advantage being that it will find diamond dependency problem (dependency A and B depends on different version of dependency C). But it slows your build down : for my <a href="https://github.com/RemiBou/Toss.Blazor">Toss project</a> it increases the build time by 766% (3sec -&gt; 23sec) !! If I build my project 100 times per day, I lose 33 minutes only in build time every day.</p>
<p>It seems that the documentation is not right about the linker and it runs even when you are in Debug build configuration. You need to add the following lines to your client project for disabling it :</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt">&lt;PropertyGroup&gt;</span>
<span class="nt">&lt;BlazorLinkOnBuild</span> <span class="na">Condition=</span><span class="s">"'$(Configuration)'!='Release'"</span><span class="nt">&gt;</span>false<span class="nt">&lt;/BlazorLinkOnBuild&gt;</span>
<span class="nt">&lt;/PropertyGroup&gt;</span>
</code></pre></div></div>
<p>With this the linker will be enabled when you build your project with “-c Release” on your build run or publish dotnet command and disabled else.</p>
<h2 id="auto-rebuild-of-backend-project">Auto rebuild of backend project</h2>
<p>Despite all the unit/integration/e2e test you can do, you need to actually see your GUI for validating it. This involves a lot of back and forth between your code and the browser. Right now when you change your razor file content, you need to build the backend project so it has the latest version of your blazor project dll. This can take some time and can also lead to stupid debugging session during which you try to find out why your changes doesn’t work until you realize you forgot to build your project (don’t deny it, it happened to all of us).</p>
<p>Right now for running your project, you cd onto your backend project and hit :</p>
<pre><code class="language-cmd">dotnet run
</code></pre>
<p>We can improve this by using the watch subcommand, so the project will be automatically rebuild when we change a .cs file</p>
<pre><code class="language-cmd">dotnet watch run
</code></pre>
<p>The problem is, the rebuild won’t occur if you change a razor file on your client project. To do so, you need to add to following lines to your backend project csproj :</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ItemGroup&gt;</span>
<span class="c">&lt;!-- extends watching group to include *.razor files --&gt;</span>
<span class="nt">&lt;Watch</span> <span class="na">Include=</span><span class="s">"..\ClientProject\**\*.razor"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/ItemGroup&gt;</span>
</code></pre></div></div>
<ul>
<li>ClientProject is your client project folder</li>
<li>Now “dotnet watch run” will rebuild both (front and back) projects when you change a razor file</li>
</ul>
<h2 id="refreshing-browser-windows-when-rebuilding">Refreshing browser windows when rebuilding</h2>
<p>It would also be nice to refresh the browser when a change occurs, a bit like with “ng serve”. With this you would be nearly sure that you are always executing the latest version of your code. For this <a href="https://weblog.west-wind.com/">Rick Strahl</a> created a package named <a href="https://github.com/RickStrahl/Westwind.AspnetCore.LiveReload">Westwind.AspnetCore.LiveReload</a>. But this package has a few bugs or is not working correctly for me, so I decided to fork it and change it for my usage. You can use the originalpackage from nuget or use my fork. For using my fork you can execute this git commands</p>
<pre><code class="language-cmd">git submodule add -b RemiBou-better-refresh https://github.com/RemiBou/Westwind.AspnetCore.LiveReload
</code></pre>
<p>Then on your backend project file add the following project reference</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt">&lt;ProjectReference</span> <span class="na">Include=</span><span class="s">"..\Westwind.AspnetCore.LiveReload\Westwind.AspnetCore.LiveReload\Westwind.AspNetCore.LiveReload.csproj"</span> <span class="nt">/&gt;</span>
</code></pre></div></div>
<p>Now in your Startup.cs file add this line to ConfigureServices method</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="nf">AddLiveReload</span><span class="p">(</span><span class="n">config</span> <span class="p">=&gt;</span> <span class="p">{</span>
<span class="n">config</span><span class="p">.</span><span class="n">LiveReloadEnabled</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
<span class="n">config</span><span class="p">.</span><span class="n">ClientFileExtensions</span> <span class="p">=</span> <span class="s">".css,.js,.htm,.html"</span><span class="p">;</span>
<span class="n">config</span><span class="p">.</span><span class="n">FolderToMonitor</span> <span class="p">=</span> <span class="s">"~/../"</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>
<p>And in your Configure method (before any middleware related to static files or to blazor).</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">app</span><span class="p">.</span><span class="nf">UseLiveReload</span><span class="p">();</span>
</code></pre></div></div>
<p>This will do 2 things :</p>
<ul>
<li>Create a websocket that send a message when a file matching the ClientFileExtensions is changed, so when you change a .html or a css file, the page will be automaticaly reloaded</li>
<li>Change every outgoing html file with the LiveReload scripts that connects to the websocket, listen for change and execute the page refresh. It will also detect disconnection, try to reconnect and reload the page when the reconnection succeeds.</li>
</ul>
<p>Now you can test it, run “dotnet watch run” on your backend project.</p>
<ul>
<li>If it is a .cs or a .razor the server will be shutdown, the project build, the server started and your page reloaded automatically</li>
<li>If it is a .html, a .css or a .js, the page will be reloaded without the need for compilation.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>The developer experience when using a framework is very important. The more manual and repititive task you can remove the more efficient your team is. I hope the aspnet team will work on built-in feature for making this easier.</p>
</description>
<pubDate>Fri, 08 Nov 2019 00:00:00 +0000</pubDate>
</item>
<item>
<title>Test A Blazor App With Cypress</title>
<link>/Test-a-Blazor-App-with-Cypress/</link>
<guid isPermaLink="true">/Test-a-Blazor-App-with-Cypress/</guid>
<description><h1 id="how-to-test-a-blazor-app-with-cypress-using-docker-compose">How to test a Blazor app with Cypress using docker-compose</h1>
<p>On my <a href="https://github.com/RemiBou/Toss.Blazor">Toss project</a>, I chose to have some end-to-end (e2e). End-to-end test on web project are tests that automate a browsing session on a web browser. Most of the time it works by using API provided by an existing browser (like chrome). Those kind of tests have many drawbacks :</p>
<ul>
<li>Force you to add ids everywhere on your html code so you can find element on your test code</li>
<li>Are often flaky because some load time might vary between two test run or you can change your front-end code without thinking about the changes needed in the test</li>
<li>Are hard to build first because you can forget some steps, like scroll down or click here.</li>
</ul>
<p>But they are important for making sure that your app works and you don’t have a huge error when your app startup because you miss a js reference or something else.</p>
<p>At first I started my tests with Selenium. Mainly because it’s the most common way to do this and because there was some example on the aspnet core repo. But Selenium tests are hard to write and very flaky (just look at my <a href="https://github.com/RemiBou/Toss.Blazor/commits/master">git logs</a>) for many reasons :</p>
<ul>
<li>An element must be displayed on the screen before accepting interactions, so you must code the scrolling up/down, size screen etc …</li>
<li>You can only use the browser like a normal user so you can’t add code for waiting the end of an http query or check the status of a xhr.</li>
<li>There isn’t much debugging information provided : screenshot are hard to get and videos impossible.</li>
</ul>
<p>I heard a lot of good thing about <a href="https://www.cypress.io/">cypress</a> so I decided to give it a shot after an unsuccessful battle against Selenium.</p>
<h2 id="start-the-application-and-dependencies">Start the application and dependencies</h2>
<p>Because my previous E2E test used Selenium it was a .NET Core project. This was usefull for launching RavenEB.Embedded. Cypress running on the browser runtime, I won’t have this project anymore. I needed an other way for starting my ravendb server : so I picked docker-compose, which I could use for starting my whole system : my app and the dependencies.
I really like docker and using docker-compose for running E2E tests seems like a good idea. For running my app in a docker-compose I need to build a docker image for it, here it is :</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> mcr.microsoft.com/dotnet/core/runtime:2.2.7-alpine as runtime227</span>
<span class="k">FROM</span><span class="s"> mcr.microsoft.com/dotnet/core/sdk:3.0.100-alpine AS build</span>
<span class="c"># import sdk from 2.2.7 because we need it for running ravendb embedded</span>
<span class="k">COPY</span><span class="s"> --from=runtime227 /usr/share/dotnet /usr/share/dotnet </span>
<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="k">COPY</span><span class="s"> ./Toss.Client/Toss.Client.csproj ./Toss.Client/</span>
<span class="k">COPY</span><span class="s"> ./Toss.Server/Toss.Server.csproj ./Toss.Server/</span>
<span class="k">COPY</span><span class="s"> ./Toss.Shared/Toss.Shared.csproj ./Toss.Shared/</span>
<span class="k">COPY</span><span class="s"> ./Toss.Tests/Toss.Tests.csproj ./Toss.Tests/</span>
<span class="k">COPY</span><span class="s"> ./Toss.sln ./</span>
<span class="k">RUN </span>dotnet restore ./Toss.sln
<span class="k">COPY</span><span class="s"> ./Toss.Client ./Toss.Client</span>
<span class="k">COPY</span><span class="s"> ./Toss.Server ./Toss.Server</span>
<span class="k">COPY</span><span class="s"> ./Toss.Shared ./Toss.Shared</span>
<span class="k">COPY</span><span class="s"> ./Toss.Tests ./Toss.Tests</span>
<span class="k">RUN </span>dotnet <span class="nb">test</span> ./Toss.Tests
<span class="k">RUN </span>dotnet publish Toss.Server/Toss.Server.csproj <span class="nt">-c</span> Release <span class="nt">-o</span> /app
<span class="k">FROM</span><span class="s"> mcr.microsoft.com/dotnet/core/aspnet:3.0</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> --from=build /app .</span>
<span class="k">EXPOSE</span><span class="s"> 80</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["dotnet", "Toss.Server.dll"]</span>
</code></pre></div></div>
<ul>
<li>We can see a really nice feature of Docker : I was able to import the 2.2.7 runtime into a 3.0 sdk docker image . I needed this because my integration tests runs RavenDB.Embedded which is not compatible with runtime 3.</li>
<li>With multi stage build I can use the sdk for building my app then only the runtime for executing it which will make my final docker image smaller</li>
<li>I first copy the csproj and sln files for optimizing layer caching : if I don’t change anything to my csproj files, then the “RUN dotnet restore ./Toss.sln” line will use the cached version of this layer, making my build faster (even though Azure DevOps doesn’t provide docker layer caching yet).</li>
<li>“RUN dotnet test ./Toss.Tests” runs my integration tests, not the E2E.</li>
</ul>
<p>Then I need to create a docker-compose file describing my app and its dependencies :</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">3.5'</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">web</span><span class="pi">:</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">GoogleClientId=AAA</span>
<span class="pi">-</span> <span class="s">GoogleClientSecret=AAA</span>
<span class="pi">-</span> <span class="s">MailJetApiKey=</span>
<span class="pi">-</span> <span class="s">MailJetApiSecret=</span>
<span class="pi">-</span> <span class="s">MailJetSender=</span>
<span class="pi">-</span> <span class="s">RavenDBEndpoint=http://ravendb:8080</span>
<span class="pi">-</span> <span class="s">RavenDBDataBase=Tests</span>
<span class="pi">-</span> <span class="s">StripeSecretKey=</span>
<span class="pi">-</span> <span class="s">test=true</span>
<span class="na">depends_on</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">ravendb</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">80:80</span>
<span class="na">ravendb</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">ravendb/ravendb</span>
</code></pre></div></div>
<ul>
<li>I expose the port 80 so I can access the app in my dev computer</li>
<li>“test=true” tells my app to use fake dependencies for things like external services (mailjet, stripe) or non deterministic data (random, datetime.now)</li>
<li>The good thing with docker-compose is that the services run on their own network and are accessible inside this network accessible via their name, so I can run ravendb inside this network and it won’t have an impact on the other ravendb I might be running on my dev computer.</li>
</ul>
<p>Now I can run my app with the following command :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose up <span class="nt">-d</span>
</code></pre></div></div>
<p>Maybe I’ll use it as a starting point for hosting my app in production but it is not the point right now.</p>
<h2 id="cypress-test">Cypress Test</h2>
<p>First I create the folder “Toss.Tests.E2E.Cypress” in my solution then cd on it. You need npm installed on your computer. Then type on your terminal :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm init
npm <span class="nb">install </span>cypress
</code></pre></div></div>
<p>Once installed I got a cypress folder containing multiple folder, I cleaned up all the samples in the folder called “integration”. My new test will be a js file inside this integration folder :</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// &lt;reference types="Cypress" /&gt;</span>
<span class="nx">describe</span><span class="p">(</span><span class="dl">'</span><span class="s1">Toss Full Test</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">polyfill</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">uuid</span> <span class="o">=</span> <span class="nx">Cypress</span><span class="p">.</span><span class="nx">_</span><span class="p">.</span><span class="nx">random</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="nx">e6</span><span class="p">)</span>
<span class="nx">before</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">polyfillUrl</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://unpkg.com/[email protected]/dist/fetch.umd.js</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="nx">polyfillUrl</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">polyfill</span> <span class="o">=</span> <span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
<span class="p">});</span>
<span class="p">});</span>
<span class="nx">Cypress</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">window:before:load</span><span class="dl">'</span><span class="p">,</span> <span class="nx">win</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="k">delete</span> <span class="nx">win</span><span class="p">.</span><span class="nx">fetch</span><span class="p">;</span>
<span class="nx">win</span><span class="p">.</span><span class="nb">eval</span><span class="p">(</span><span class="nx">polyfill</span><span class="p">);</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">SubscribeEmail</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">tosstests</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">uuid</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">@yopmail.com</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">SubscribePassword</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">tossTests123456!!</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">SubscribeLogin</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">tosstests</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">uuid</span><span class="p">;</span>
<span class="nx">it</span><span class="p">(</span><span class="dl">'</span><span class="s1">Full process</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">server</span><span class="p">();</span>
<span class="c1">//used for listenning to register api call and getting the recirction url from the http headers</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/api/account/register</span><span class="dl">'</span><span class="p">).</span><span class="k">as</span><span class="p">(</span><span class="dl">'</span><span class="s1">register</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/api/account/login</span><span class="dl">'</span><span class="p">).</span><span class="k">as</span><span class="p">(</span><span class="dl">'</span><span class="s1">login</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">route</span><span class="p">(</span><span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/api/toss/create</span><span class="dl">'</span><span class="p">).</span><span class="k">as</span><span class="p">(</span><span class="dl">'</span><span class="s1">create</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">disableCaptcha</span><span class="p">();</span>
<span class="c1">//this could be long as ravendb is starting</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#LinkLogin</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">timeout</span><span class="p">:</span> <span class="mi">20000</span> <span class="p">}).</span><span class="nx">click</span><span class="p">();</span>
<span class="c1">//register</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#LinkRegister</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#NewEmail</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="nx">SubscribeEmail</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#NewName</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="nx">SubscribeLogin</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#NewPassword</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="nx">SubscribePassword</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#NewConfirmPassword</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="nx">SubscribePassword</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#BtnRegister</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">wait</span><span class="p">(</span><span class="dl">'</span><span class="s1">@register</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">@register</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">xhr</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">status</span><span class="p">).</span><span class="nx">to</span><span class="p">.</span><span class="nx">eq</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
<span class="nx">expect</span><span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">'</span><span class="s1">x-test-confirmationlink</span><span class="dl">'</span><span class="p">]).</span><span class="nx">to</span><span class="p">.</span><span class="nx">not</span><span class="p">.</span><span class="nx">empty</span><span class="p">;</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Redirect URL : </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">xhr</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">'</span><span class="s1">x-test-confirmationlink</span><span class="dl">'</span><span class="p">]);</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">'</span><span class="s1">x-test-confirmationlink</span><span class="dl">'</span><span class="p">]);</span>
<span class="nx">disableCaptcha</span><span class="p">();</span>
<span class="c1">//login</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#UserName</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">timeout</span><span class="p">:</span> <span class="mi">20000</span> <span class="p">}).</span><span class="nx">type</span><span class="p">(</span><span class="nx">SubscribeEmail</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#Password</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="nx">SubscribePassword</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#BtnLogin</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">wait</span><span class="p">(</span><span class="dl">'</span><span class="s1">@login</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">//publish toss</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#LinkNewToss</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="kd">var</span> <span class="nx">newTossContent</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">lorem ipsum lorem ipsumlorem ipsum lorem ipsumlorem ipsum lorem ipsumlorem ipsum lorem ipsum #test</span><span class="dl">"</span><span class="p">;</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#TxtNewToss</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="nx">newTossContent</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#BtnNewToss</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">wait</span><span class="p">(</span><span class="dl">'</span><span class="s1">@create</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">//publish toss x2</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#LinkNewToss</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="kd">var</span> <span class="nx">newTossContent2</span> <span class="o">=</span> <span class="dl">"</span><span class="s2"> lorem ipsum lorem ipsumlorem ipsum lorem ipsumlorem ipsum lorem ipsumlorem ipsum lorem ipsum #toto</span><span class="dl">"</span><span class="p">;</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#TxtNewToss</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="nx">newTossContent2</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#BtnNewToss</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">wait</span><span class="p">(</span><span class="dl">'</span><span class="s1">@create</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">//add new hashtag</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#TxtAddHashTag</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="dl">"</span><span class="s2">test</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#TxtAddHashTag</span><span class="dl">"</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="dl">"</span><span class="s2">{enter}</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#BtnAddHashTag</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">.toss-preview</span><span class="dl">"</span><span class="p">).</span><span class="nx">first</span><span class="p">().</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">.toss-detail .toss-content</span><span class="dl">"</span><span class="p">).</span><span class="nx">should</span><span class="p">(</span><span class="dl">"</span><span class="s2">contain</span><span class="dl">"</span><span class="p">,</span> <span class="nx">newTossContent</span><span class="p">);</span>
<span class="c1">// logout</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#LinkAccount</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">#BtnLogout</span><span class="dl">"</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
<span class="nx">cy</span><span class="p">.</span><span class="nx">url</span><span class="p">().</span><span class="nx">should</span><span class="p">(</span><span class="dl">"</span><span class="s2">eq</span><span class="dl">"</span><span class="p">,</span> <span class="nx">Cypress</span><span class="p">.</span><span class="nx">config</span><span class="p">().</span><span class="nx">baseUrl</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">/</span><span class="dl">"</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">})</span>
<span class="p">})</span>
<span class="kd">function</span> <span class="nx">disableCaptcha</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">cy</span><span class="p">.</span><span class="nb">window</span><span class="p">()</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">win</span> <span class="o">=&gt;</span> <span class="p">{</span>
<span class="nx">win</span><span class="p">.</span><span class="nx">runCaptcha</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">win</span><span class="p">.</span><span class="nb">Function</span><span class="p">([</span><span class="dl">'</span><span class="s1">action</span><span class="dl">'</span><span class="p">],</span> <span class="dl">'</span><span class="s1">return Promise.resolve(action)</span><span class="dl">'</span><span class="p">);</span>
<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li>Cypress uses mocha for running the tests, so your test fixture must be a call to “describe()” function and inside each test there is multiple calls to a “it” function. As in every test runner, there are hooks for running things before/after each/every tests.</li>
<li>Cypress is able to read every XHR request done by your site, but Blazor uses fetch for http call. We need to remove the current implementation of fetch and replace it by a polyfill that uses xhr.</li>
<li>We can see with the route() and wait() call how you can wait until an http request is done and how we can read its content : here I needed a way to get the confirmation link after a subscribtion, so server side I send this link in the http response header (only in test mode) and I read it on client side and then visit() the link.</li>
<li>For E2E I prefer to write one big test that does a lot of thing, it’s just a mater of taste. I don’t practice TDD with E2E test it would be too hard, I prefer to create it when the development is done just for making sure it will keep working after my future updates.</li>
<li>The method disableCaptcha is used for disabling recaptcha. Be careful here, I create the method with a “new win.Function” for a specific reason. When you do interop C# -&gt; js, Microsoft.JSINterop (used by Blazor) <a href="https://github.com/aspnet/Extensions/blob/master/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts">check that the argument send is a function</a> with this code</li>
</ul>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nx">result</span> <span class="k">instanceof</span> <span class="nb">Function</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">result</span> <span class="o">=</span> <span class="nx">result</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="nx">lastSegmentValue</span><span class="p">);</span>
<span class="nx">cachedJSFunctions</span><span class="p">[</span><span class="nx">identifier</span><span class="p">]</span> <span class="o">=</span> <span class="nx">result</span><span class="p">;</span>
<span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`The value '</span><span class="p">${</span><span class="nx">resultIdentifier</span><span class="p">}</span><span class="s2">' is not a function.`</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here is the catch : on my test if I write “win.runCaptcha = function(action){return Promise.resolve(action);}” it would throw an error “The value ‘window.runCaptcha’ is not a function.” because class definition (like Function) are namespaced by window so my method would be a function in the namespace of the cypress window, not on the namespace of the tested app window (it took me no less than 2 days to figure it out).</p>
<p>Now for testing this I cd on the test directory and enter</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./node_modules/.bin/cypress open
</code></pre></div></div>
<p>Which opens a web UI where I can run my test and see them while they are executing. This GUI is really nice because you can go in the past and see the state of the UI or even browse the DOM. Now that my test passes I want to integrate cypress into my docker-compose so I can run it without installing cypress, which will help when I’ll run it in my CI environment.</p>
<h2 id="integrate-cypress-into-the-docker-compose">Integrate Cypress into the docker-compose</h2>
<p>I added the following service to my docker-compose.yml</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">cypress</span><span class="pi">:</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">cypress/included:3.4.1</span>
<span class="na">depends_on</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">web</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">CYPRESS_baseUrl=http://web</span>
<span class="na">working_dir</span><span class="pi">:</span> <span class="s">/e2e</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">./Toss.Tests.E2E.Cypress/:/e2e</span>
</code></pre></div></div>
<ul>
<li>I mount the cypress tests as a volume, so I can also get the cypress test execution artifacts back (screenshot and video)</li>
<li>The “depends_on” is really usefull for starting things in order (ravendb -&gt; web -&gt; cypress)</li>
<li>By default cypress tries to test http://localhost, here I change it for the service url with the env variable CYPRESS_baseUrl</li>
</ul>
<p>Then for running the test I run the following command from the root fo my project</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose up <span class="nt">--renew-anon-volumes</span> <span class="nt">--exit-code-from</span> cypress <span class="nt">--build</span>
</code></pre></div></div>
<ul>
<li>”–renew-anon-volumes” is used for cleaning ravendb volumes before each run, so I start with an empty DB.</li>
<li>”–exit-code-from cypress” is used for telling docker-compose to kill all the other services when the service “cypress” is done and it’ll use the cypress return code as its own.</li>
<li>”–build” will build our service with build specification (only “web”)</li>
</ul>
<h2 id="integration-on-azure-devops">Integration on Azure DevOps</h2>
<p>Now that I got a fully running integration I can replace the existing one for this. Because it uses docker-compose it’ll be easier to maintain (I can test it on my dev machine and on multiple OS) and will also help for running other servcies. Here is my new “azure-pipelines.yml” file</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pool</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Azure Pipelines</span>
<span class="na">vmImage</span><span class="pi">:</span> <span class="s">ubuntu-16.04</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">DockerCompose@0</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Run</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">docker-compose</span><span class="nv"> </span><span class="s">command'</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">containerregistrytype</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Container</span><span class="nv"> </span><span class="s">Registry'</span>
<span class="na">dockerRegistryEndpoint</span><span class="pi">:</span> <span class="s1">'</span><span class="s">dockerhub</span><span class="nv"> </span><span class="s">remibou'</span>
<span class="na">dockerComposeFile</span><span class="pi">:</span> <span class="s1">'</span><span class="s">docker-compose.yml'</span>
<span class="na">dockerComposeCommand</span><span class="pi">:</span> <span class="s1">'</span><span class="s">up</span><span class="nv"> </span><span class="s">--renew-anon-volumes</span><span class="nv"> </span><span class="s">--exit-code-from</span><span class="nv"> </span><span class="s">cypress</span><span class="nv"> </span><span class="s">--build'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PublishPipelineArtifact@1</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Publish</span><span class="nv"> </span><span class="s">Pipeline</span><span class="nv"> </span><span class="s">Artifact'</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetPath</span><span class="pi">:</span> <span class="s">Toss.Tests.E2E.Cypress/cypress/videos/</span>
<span class="na">artifact</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Cypress</span><span class="nv"> </span><span class="s">videos'</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">succeededOrFailed()</span>
</code></pre></div></div>
<ul>
<li>I use an ubuntu image because it’s lighter than windows and I don’t think I’ll run my app on windows server so it’s better to do my build and test on a linux OS.</li>
<li>You need to create a docker endpoint on Azure DevOps so it can login into docker hub for pulling public images</li>
<li>I publish the cypress video as artifact so I won’t have any problem for debugging it when it fails</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>I hope this new test suite will make my E2E tests less flaky and more enjoyable to maintain and improve.</p>
<p>I can also say that Cypress is waaaay better than Selenium in every possible way :</p>
<ul>
<li>installation : a single docker service or an npm package</li>
<li>development : no need to add wait everywhere and you can watch XHR requests</li>
<li>execution : a web UI or a single command line</li>
<li>debug. : on the web UI you can browse all the test execution and you have a video of the whole thing.</li>
</ul>
</description>
<pubDate>Fri, 18 Oct 2019 00:00:00 +0000</pubDate>
</item>
<item>
<title>Migrate Protogen To Blazor</title>
<link>/Migrate-protogen-to-Blazor/</link>
<guid isPermaLink="true">/Migrate-protogen-to-Blazor/</guid>
<description><h1 id="how-i-migrated-protogen-to-blazor">How I migrated protogen to Blazor</h1>
<p><a href="https://protogen.marcgravell.com/">Protogen</a> is a web app for working with the serialization format protobuf. It provides two features :</p>
<ul>
<li>Generating code from .proto file (which describes data structure and API)</li>
<li>Reading serialized data (binary) and displaying it in a human readable form</li>
</ul>
<p>The current website works well, but a few weeks ago Marc Gravell, creator of <a href="https://github.com/protobuf-net/protobuf-net">protobufnet</a>, asked on Twitter if someone could migrate it to Blazor. I opened a <a href="https://github.com/protobuf-net/protobuf-net/issues/546">issue</a> in GitHub, asked a few questions and started having fun.</p>
<p>The main interest in migrating it to Blazor is to try to execute most of the logic on the client side and go to server side only when needed :</p>
<ul>
<li>The code generation in C# and VB.net can be done client side because protobufnet targets netstandard. But the code generation for other languages (php, python …) uses an executable called “protoc” from Google written in C++, so this part needed to be kept on the server side. I could’ve tried to execute it on the client with some C++ to wasm compiler but it was a bit too much for now.</li>
<li>The binary reading can be done client side as the library targets netstandard.</li>
</ul>
<h2 id="the-poc">The POC</h2>
<p>Even if protobufnet targets netstandard, I was still not sure I could execute the code generation on the client-side. I created this Proof Of Concept (in this <a href="https://github.com/RemiBou/protobuf-net/commit/1f6ce4fe6cad89fcebbc295f4bac683d62d021a4">commit</a>) for validating the first idea</p>
<div class="language-cs highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">@using</span> <span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Http</span>
<span class="n">@using</span> <span class="n">Google</span><span class="p">.</span><span class="n">Protobuf</span><span class="p">.</span><span class="n">Reflection</span>
<span class="n">@using</span> <span class="n">System</span><span class="p">.</span><span class="n">IO</span>
<span class="n">@using</span> <span class="n">ProtoBuf</span><span class="p">.</span><span class="n">Reflection</span>
<span class="n">@inject</span> <span class="n">HttpClient</span> <span class="n">Http</span>
<span class="n">@page</span> <span class="s">"/"</span>
<span class="p">&lt;</span><span class="n">h1</span><span class="p">&gt;</span><span class="n">Hello</span><span class="p">,</span> <span class="n">world</span><span class="p">!&lt;/</span><span class="n">h1</span><span class="p">&gt;</span>
<span class="n">Welcome</span> <span class="n">to</span> <span class="n">your</span> <span class="k">new</span> <span class="n">app</span><span class="p">.</span>
<span class="nf">@foreach</span><span class="p">(</span><span class="kt">var</span> <span class="n">file</span> <span class="k">in</span> <span class="n">codeFiles</span><span class="p">){</span>
<span class="p">&lt;</span><span class="n">b</span><span class="p">&gt;</span><span class="n">FILE</span> <span class="p">=</span> <span class="n">@file</span><span class="p">.</span><span class="n">Name</span><span class="p">&lt;/</span><span class="n">b</span><span class="p">&gt;&lt;</span><span class="n">br</span><span class="p">/&gt;</span>
<span class="p">&lt;</span><span class="n">b</span><span class="p">&gt;</span><span class="n">CONTENT</span> <span class="p">=&lt;/</span><span class="n">b</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="n">pre</span><span class="p">&gt;</span>
<span class="n">@file</span><span class="p">.</span><span class="n">Text</span>
<span class="p">&lt;/</span><span class="n">pre</span><span class="p">&gt;</span>
<span class="p">}</span>
<span class="nf">@if</span><span class="p">(</span><span class="n">codeFiles</span><span class="p">.</span><span class="n">Count</span> <span class="p">==</span> <span class="m">0</span><span class="p">){</span>
<span class="p">&lt;</span><span class="n">b</span><span class="p">&gt;</span><span class="n">NO</span> <span class="n">CODE</span> <span class="n">FILE</span><span class="p">&lt;/</span><span class="n">b</span><span class="p">&gt;</span>
<span class="p">}</span>
<span class="n">@code</span><span class="p">{</span>
<span class="k">private</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">CodeFile</span><span class="p">&gt;</span> <span class="n">codeFiles</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">CodeFile</span><span class="p">&gt;();</span>
<span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">OnInitializedAsync</span><span class="p">()</span>
<span class="p">{</span>
<span class="kt">var</span> <span class="n">schema</span> <span class="p">=</span> <span class="k">await</span> <span class="n">Http</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="s">"https://raw.githubusercontent.com/protobuf-net/protobuf-net/4b239629f5f9dbe4770a497f2c81465ab0669504/src/protobuf-net.Test/Schemas/descriptor.proto"</span><span class="p">);</span>
<span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">reader</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringReader</span><span class="p">(</span><span class="n">schema</span><span class="p">))</span>
<span class="p">{</span>
<span class="kt">var</span> <span class="k">set</span> <span class="p">=</span> <span class="k">new</span> <span class="n">FileDescriptorSet</span>
<span class="p">{</span>
<span class="n">ImportValidator</span> <span class="p">=</span> <span class="n">path</span> <span class="p">=&gt;</span> <span class="k">true</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">set</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"my.proto"</span><span class="p">,</span> <span class="k">true</span><span class="p">,</span> <span class="n">reader</span><span class="p">);</span>
<span class="k">set</span><span class="p">.</span><span class="nf">Process</span><span class="p">();</span>
<span class="n">CodeGenerator</span> <span class="n">codegen</span> <span class="p">=</span> <span class="n">CSharpCodeGenerator</span><span class="p">.</span><span class="n">Default</span><span class="p">;</span>
<span class="n">codeFiles</span> <span class="p">=</span> <span class="n">codegen</span><span class="p">.</span><span class="nf">Generate</span><span class="p">(</span><span class="k">set</span><span class="p">,</span> <span class="n">NameNormalizer</span><span class="p">.</span><span class="n">Default</span><span class="p">).</span><span class="nf">ToList</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<ul>
<li>This was created in a brand new blazor app with a reference to protobufnet</li>
<li>It loads a sample .proto file from github</li>
</ul>
<p>And … it did not build because of this error</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Fatal error in IL Linker
Unhandled Exception: Mono.Cecil.AssemblyResolutionException: Failed to resolve assembly: 'System.Private.ServiceModel, Version=4.5.0.3, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
</code></pre></div></div>
<p>This error is caused by the ILLinker. The linker browse your whole code and tries to find useless part in assemblies and remove it so the application binary size get smaller. The consequences of such browsing is that if illinker cannot find one of your transitive dependency (dependency of a dependency), it’ll throw an error. Here the protobufnet dependencies requires System.Private.ServiceModel which the linker cannot found (it might be a bug that’ll get resolved once Blazor package embed the latest illinker, <a href="https://github.com/mono/linker/issues/604">source</a>).</p>
<p>As a first step I disabled the linker with the following tag in my csproj and it worked.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt">&lt;BlazorLinkOnBuild&gt;</span>false<span class="nt">&lt;/BlazorLinkOnBuild&gt;</span>
</code></pre></div></div>
<p>Then Marc told me he split protobufnet in two part protobuf-net (all thepart with dependencies to System.ServiceModel.Primitives) and protobuf-net.Core. I don’t think he did it for me but it helped greatly as I could change my dependency to .Core and enable the linker, which reduced the app size from 7.3MB to 2.7MB :)</p>
<p>Once this was done, the POC was working nicely, so I decided to move on and finish the job.</p>
<h2 id="architecture">Architecture</h2>
<p>I decided to go for a classic ASPNET Core API that would host my Blazor client side app because I still needed a little API, and that would keep the deployment as simple as it is now (I guess Marc does a right-click publish from Visual Studio as there is no pipeline defined on the repo).</p>
<p>I kept the existing protogen.site project and created a protogen.site.blazor.client besides it. I had to add the reference to the blazor project from the server project :</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nt">&lt;ProjectReference</span> <span class="na">Include=</span><span class="s">"..\protogen.site.blazor.client\protogen.site.blazor.client.csproj"</span> <span class="nt">/&gt;</span>
</code></pre></div></div>