Bs_
zXsRQ}&NY-s8LO{zg_Aes5@Iao-?%2ih%Y1akwj5~qt_RaqU9tdp0_TiFqK*Us7ekxDqNK_d??g9&
z^*kc^
z+Cz!O1SUGYW3M$b;Tw-F$T;b}=;dE3i$I>@K@=%+m^{yq2Cd%P%5Bu_%7&W5yiYOTdT
z{i0o(j~n^KD(4*Jl>9j$Qo*Ikk1fH&pFbPUpb>cGK7xk`1
zxeDytDm5O^Tq67c0Wo2_o!(r60y|+~+|u%nZkr2XvvP!jk_Lv!
zj2)ady`C?^C^7_Qb~z4z98fFh!Z5H3^=z=H1n!Xr;sJ}0>^V>NX0i>96l0jdim#QM
zz`=!c=d5tJ?ZU1Li}~n2F>&c?E8;V+eaIn@CVZ=4x#MV)|ITyuxga)9Wgz48<+nfC
zYUuIIRE}0cP<(y4DVWH7Wtqri4_zBC>byOsiYIu6x#J}y72be;^1wJcl#1=a%_0|t
zL%G}W)6+7IIrsXwK1Mns3@f+z#*_9ZIEz}UbKbFawTTQ|%Wp67#Z=v6eaMlP@wRWV
zy_OrtyI;rY3pH9Dmp1IGOWsV8W_r{o$T0-W({YY=$DlC1R(LlDLnCCMpt!#xbMqZD
z<00W8a{^;Cl}7Zz?4^i4V+jA~bjk@e%5c7SgA@@cpvLsD{daA9zSI45&d
z=2K@*)?1<(b!&QhEvg~mce)naf1oX_(*g~K-8G#|$?4`}OmxE!=X1O&*d`NUjPedokv)7E-o|ww=S^XP-yIq7Vsi=y{pXT6D>x;Dj?yIQ`|a4$q`z#2
z_@xfr(BR$PtOw><&!j1x#B^b;WxruPfhIn0mdG^YxxJH1aciT0Xnr
z^Ex;W<*}uTEdGx!i#aNl2rdp`HaZgS+V>p5;Sf8Mo0<3nyjTg%kqbgN*~~GPbGXnf
zkVa&gaqz*@%WQ=e9=P&MafUqsYbv+5
z>VEvksz){v3xuc35G@|=V4TEn?{@gEZtN)qcuJ0mynBlkn}4Wd1zlvVZfmw=>6ECz
zq28Lkjcg<)f>vC@1|Or0x7UsFaMsftSh`ZbH#7bA)vFRi(=4~LOo%l?6zL|joF7&{
z=W{3F;dxH)VVZ}83Va-Tu)qSW=|;-pV?{Rz$Dl!qp&wzxWXUh%3C*(v8r{Cqg#$8h
z@nT(uE0#YT5qW#AP1ujFt{ugsrG~EnU>3~nSJwx`!vD$V1I7>)=P)EgFirDa_Wff!
z90L2wT55*{Hb0JLDI@!-69}7qUCfC`LTVU{Mt)5dnqag4TZE-F0aw9%SGdgA
zlykx~aA8oYDbX%beDeWYl@Hr9VX)NER2*QResbhC9j1`Ex)T>8z)k41^z9&@(GV
zzioU)(BVR+k2bX6XZ~X2?@jh@5Q4e%kR>RM!vT=AK!u@lDaO1oUoB6y0U*r#w_Ox3@4}|8@2G2J})C33Lo6js)Tbg~-vkoRF-!Nt;|mkpvE{}XyON}MrZTO>FNLnl*bt2*$lyV
zR~*rNzTKbfvB@BNIlfp194i?wBwBQ%0+iEm#Im#{w?!m%gI{)>7adO{Yok^=k0eg^(`FLQ0Cx`O)yuNgSr~6npiq>W~-J;xEo9Kj1PhiLBr4UKJJzO8M{$4-WIKH}dE%BR1iNu>v8kq02`iFiu@WvvGzdoKGr~AFDaQ@CQPBnYA
z)oMDOc<%Y$#|h;9_;^^Hr7-$gUD&AxtYB3!t}_Bra!}DNP|?Z55@z-Lq%pIH8wk2(
zW;)~Dc}BYAn9QD9N9NOzocRAJoUd+r(GEV*LL_F`ln@D@EPQxyHhFmx@NRp6Q~oD@
zmgw*f_RWgQyl`rz#wZmgNCbE?Km^g;g<@d!H8jwN^mPV3E8n25wDV!ZZSnz`b1=4>
zP{V!cis{XmNY|Y_`#Rmr;C5AJNRP4Rt+ueTxIu_!YKjxLHlhInml|qn$}{t1txsN@
zP&@!7V8iwJj?*F!?stBHE$@xbNXbpLW@tQ?&SVd*L%}XeDB$a@l!|5t*_c
zgnn^xGEthhzOw8+EdYS%&X&brO}Qrbgu2@M^fipRw!$|-YG#AQnZm2*QvgVG;@#lZqis&fpLmoMr{{{b(}kY`BjY;J`U!b+-8jmpd^!P}SS5nT%v#66|}
zcz~rGPIRVRiEb?};p!KAMwSqh(`Y5o`j%Z9e;rfC!+_HI-tt%7EVA`@KY;ZI2Doi`
zFi9lkVZxzb!w(P|et?L?pO0$@FX==K
zcp!19CwSvaz&|i;Naw7G}+8HP{M6X<6(bbz#I5t
z|0IhqG4)eXnG4yyo$vY&dCTsO^>U3yWr9DofS0JCw&Pr5Z4|+#0|FOFkPfR&v1U%<
z_#T0G@IVF^vLht+6AEbtAKdZV#bxDjKdW
zoJddp9v^#XL*Wfc3gA~U3Y~UYEj5;pZrelR)MdA$s0;6>u=%T2WV=XK+Ls_+(hvl(
z<9xW)!xL-I-g~CXHwl*U>-!gh?3Xcz+*49TQS?Ak_yyIfhUK^HIIVrD3Pt-u?RlCbIKZk8?B
zEi=Q=8o%f53z1oQMAAs`xzR2q{gy)O4kU3x&j1K&YAD{WKhuRPT;G)Eq_Ok7E9d50
zRu#Oi)1y7#?Ho>f@Tt%<^wb4omGO!aH70QAO%WM9rgbwTp}#znVi|TgySxO-Lx>3<
z;V0+&0c*7?RYCKLdRkI)pnI!?oX@iwL6i5EObE`-Zu7X<$hPFpCMq_y3hXZV5Y^u}
zQ)@84^Ko-dzh@@Pz+oykrl1wWG02kr`AtIdOPfTTT`s{i0W`0c!Mu=KT2?){HeyC%
zc!h1-18gnoH<`4$ea|cu2x~|E-r^4p;U)ZSmOTv}|AcRB#o%oh8xd#cM~p`eiZz2~
zwML$dQv_mZ6Ac6}X#flv91sf49g^r*Nh4vy39wHzw0$zUKFC$tzp%J%!KZAzdkW&0
z)?*LVY@tSca<(pDXP2(Ab7ld9h?SG`ftpYD34Ew7SRI!iv2#WGMhxqEo^aal1nrns
zePVWS$$2Bj;0QZB`RK>bm{)xp-@iM^fr+6{CNc%8c-&5}fDW0Eb6u>@HSfnhudsNKnUNaN{*X=&O;G
ze)YyP+@8exL3bJ^0YL@uuRU?>f~9HMWY=%GPFnDZIXK}6uTF}aI-rbz-96F@(PK|M
z1^c;eu8R_IK+Z2t#NoSJOOlR9`pR+9i++OM6OFo3qvlM0Hllx8hY>D!OR6iTz06l3
z%=%iO9NhErJgz{`$(QGLCU;}HP&4gGl4IP*{dm1k6HM*FHoIhKQui&(XXt;s{KU~N
z%ac6M&rH#zX%wUbSb9
zy-w_Y&I3=}lt^XxaqTzExhe6XPv(1k+y4uEK0qoIGdMoHBqKiN)2q&KjxIuc9@xtt
zn`!0S@U88-DTv=z(Wxd>`{YaN&2SNrKwsBc@}D3@-?LSlLUn3E1e+UCfjKLc?^k|k
z3^XeIsiCrvl#GaHTd?FR&Qd1Z>GtjJP;|+|m?VKAcG}NcFe#_KIKJNNK*?ZR>}j_*
z^3A))ggys!>@PcJ89`jVeipAU`DvjU>Aza*C|gQ$p=nJ*x@fdLXM+
z2Ex2a!b)mhXQs5NJvzRz5}|Dc%v6usCi@R|;dbuWmk$gr;lN4)KVaBbT^2B)*LsWw
zGY>Djz4kfmZk||Ln^XYI_d(MwlE7>j6gAiKHNT?H4%fkxIBzKDwrE=MA)j}Vff$R7
zGVvD8KX$G41!L$U^yfU6O{Qiy2%x9%U^^;3Yzz@yCeO&O15ZB++cs!G>dQ`v9fX}O
zgTA|PR0g~*#LPW!xVqv#RBy%7T=4|<=dQ9QT}6%97`sTEyqSA(5j5}zF1#JW-X*2Z
z+=F=@Ch3yOb4=f~KSVWlvDUsk>7X945CYG!>Jh%rg;+}2)-jFBT7NARs#=N^Q~9I%
z)TeGzR{Ix_$@irnJX?l1d_PRM1}b|@5d_lfMWLEsA<$;fu%y8GScwWgkk0iS2^VBf
zWf2>R?MggX+!|~>ULXYNTHIfPtAU{zPwNlwUy#4Qm(bpzb`iPnF1sw<4v_P{9>~
z5b}P_ONEsHpthnXk(`rb8K5ukQy>2!!_Qm)-wS1}D^Gs5TNWCqXre>l*WUnAV)8$#
IMGXA^2i6JKMF0Q*
literal 10992
zcmcIqRa6|&vKZa@z#1D_w%k<
zJw0=#yU(fWs$F~6bhNUf6cPeH0tf^`l93iy1@=MzHaHmIeL&D;9N0lwi71GGK=p~>
zR}*O9JE@tpssae)Lj@cc1_C|*JN^g+a%Ta7PK-ey-V6{3$LU*#3P11zjH#TIIOzS~
zE5Exe6&QhclGbqrfe32
zcd9(w)Ad336~#s-_pw`cibS-+rU<2`8CU4a1JMUjQtbMR7i2ED`3xckmSy9*c>6Bn
z5B=x&J?^b%=k=Z4*S>-Ibm}R~>kex|HsbX9HoDS6OJRB$QMUucJZ-&L-;cQj>0&nn}*)xP8=nkV_`B~YRl``W9w
zRC>2yG-Fvjapf}pEsZR?j7)H5WPN1sFQbG@tr`VHGOK50#AP>4N>CJqUnc9#!@I1w
z3m;7iJ=eq|0fn4dQJ__|amph~3`Oo5a5;0#9~st#(vYHNgb8V^^|`=Am@>kn)E-X2Kon^J9a;1oI7aldF>M}eeK1)(8Dh#4j5I0g
zYZB**<$7SD^e)YKRjTA9rc4mqfvz)f64Fy&ElrA7J&PpAziVJ6KJ?P0WN3k3!if?e
zmdG|?FRbHc`rn8p(5a7DV>n!NaM^i<6K7(E_pSm+DWd)E9zL^6|G~mY9C-=;wOlSb
zKHsWJaRtlU*0^`|mPHml0v2VVEAW3P1Vz70@o(nMQstNkE{RB^OH{F@&|?F(*d|0v
zI}|5TtaFYqObR{{)MwmC;V+sQnwwr@oMniMqrcOlNB1};?bD5kTy7d!D`t!?kA{*(
z`?@)mVc(}hyDDTb>kl+9p-vs@E!Doe#_a#p5#=~oRT~yZ@{LngrErd}28ov?QB-h_
z%-4ju=Zp>>k%;?|DVm+ely1|`j!Ia|>z8273RhCZOjkE^?!B4LG%;tGq6H4TfGhL<
z49j<{HmOl)RZ%XppvlD9!rxcvZfBX@=+=rP5BkSYI9SIEEc7T=yD4E;Lpf&+>^Nb6
zNp!|yu7caWKvXp-3DkmhxgkrDvwiewu8T*Q4>l-`AkAF^;zYxD2w0sFF#Q6#zGTu4
z6XwrBK^lyg9|N)=fu$3t;z~H*b^D_w&!+XUu%gVBJQWIObun5=&cMol-{9lh7rFLf
zd)cIO2X>+!C9yH{>I~vm^0Z|47?W
z6)yQPFz2hnbQK;7-F_;7j7^bnD%V#wV!Pck>G%DQI;PB*#UcbCwu6FM9%Kztr$!izaR0Jh4N_;5jTJfHl+*(M3j0
zFSL(L#YI-Ja5w#yFbNdFd~sEX^8)9ix&FnNdAWE$;-mGWzYYy7mfr1(fc%B_1lA+I
z_(~>t71&JmKx`o|-AwHl88n#s-Ev;~gIm~4Iu3L>rgai@{qPxM+|@!m72xE0Y`oJ6
z@2ZFHa)peOpU~V^3^&`O|7(F!i@@YOv>yTni!MtODhYV-XDII
z#g{n0=_j#~UR0?l(UwLhQKPuhH#@EAPM$G(dr$|Z
zSQoLnH80KyJ-)lr6$AgRaH2g@CMF>Y}NV3_{WU^@b+Z_34cexAfP9U|CJ<(O^745q{SEg3+B0gm61us0nJC2HMEX@FCvy_xBniQ8f#H#!-9h`O*j;FSo?#?7ID5v&y<
z=T0>u*AyLHN20`OqA0dJ2EhlI=s8lUgYmv?E)Zdhky$Tp1k0am3!{#(j$a+pS+Z?c
zHJ|orVhlkm;wSDB5FUwTSIk=AhS|&@?$<7Fy9n_Lm=JFf(jb{Fbxq6~g4?I;v)ecMa}NqSD27j8
zmOtxU^P=U&!r>r1&2*}y?I(VVGH_~b@}i|YVo2ySZia&2y!W{ZNz)$OGJdX3=^D8R
zco=ajL$r{;%d%7!3@nVO7rpG&P@0$Tq3MyNyE-Hn}k@78TTv1<
zf|s5lCU7ftXmhx5MtpWQ#Tm!V=YB34;!dI$Moot^jN*~UkF}LE^!MRMBmZi`zi-@#
zNk4V^_R4~RQGI;bxFOs7khJRxUI@M{<1qg`!2P_`stFM`tZx@XG4-$7YgWHv4^e-%
znd4d03Obx)97Wz5%8COP1_sH6pqTaB{0bjFIGgbDD!kuYrNvj9cg7|Y^5LEp)7R|2
zW*b=77VQ;MN!)DUk?taLW03omaF(G4n=);s`|AsytX{K5V|O^K&-m`8Un#UQty2rR
zl};-KMRcqv@x_uOUtQx@rnwWFri^(gT_CcrZbJys@M)LhlMw)i_>6|MG?
zXpX2UcWGLR)yc4u8L~W%OoD>_0Z4`~5>>Yjl*k)4;bSfGx9LM7rxvZ|bU3YR>Cu$L
zN}OURt*(=mn>LN`OchvgbkM*$(lyizSe+LZ-TqE*`QQLVLx;1@RXS&p@u?`Gc{F1?
z>i#$2P%qZTj25gRA0XibKj{{QHru^il};#^mb14oOTex!Oi-bBoz;(QSXM*eedEez
zf!hIq5A`>d&-djZnACsZ_)E?on)0LL-*;cRWPuujo519wmny-IA-0+~tg;qJW!fa1
z-^ei(b>p*yG68(rf|-VsQ^yzKu=6I}>k*=_%-X?YEeu4(iDdBNZOTEi!5`9g
zDTM|$RR6`q$@ExQ4xZBCU%3xM41KK!;y)~6kF&IDs%cRgNUeAr_Wv-Vwc?1g3;MUz
zf_hq>KTO@C;y_*0>9c{auJK{rNy9k<>WtM23#vJn5&EQAUiX6Y3F@}e!q@=%x)6RBs7EPY=oAy%6N+7GDB2deCrfC22uyRFeEvP(0e?BEBNGDg9TC8~q@Lk$oG0N|?Q<{zF36^TnzkNe+{<
zT}CIZ7&nd8Jaf6#4>z6;y924KZL80n$axo29{_T+nHLXwvA65la?i#CD(aCrUF%4N
zn=oY-^;(0mqX{A`*bK@D~l!1K;B&
z+tOBHM+XOh00L{KiN29ksiFqEIk#dm;4O@JuNL%@5#yvuwDB3nR}NV4s547cMNY=}
zhaU2}I(c^Cs~NN((Z`Abo
zX7Al)1PBtWvkQ{kO)yZS%Nb@Xp;l*@&hYVPLqw9H?%UQ_LW<48s+_cd3e{-ysZq2`
zBKy**A-TDbICnGls#F~yjfJ`=(|)~C(-Pr>H~^I~fuy%6@zVJ8D(oII_0!+Whk45V
zU|YHSwxnukY5(O5E86d?&aJ;!1mZ8)+zK&|>&`zja0`v7pFZslO^7-OX)^``*D7Mb
z9dM7fmIg;q4<(eYU6W^aWer1Ee-45P1?V^gaHrJG>vm)K_7)z!>Tu7(XQCdO=5S~S
z0H8G6Jf@m59yzO2e6kMjVK=d{^#Pr*13s
zZdDF)!jXm5-L$ZovZd0Y>p2FhX7vnRSB6edYvdpFC4Jm$06Gl6@SUha0_8Q5qL*l#
zjz9vvtfj{2{jhcm8n^a+i~es0j1Fo-a~)cHp|+BxyOAn3E2BnJkPS(fUK^TBKtk-o
zk74t5-0#}1hG$JHP(bW@@h`;9dO2<^+Pf0dVf=M+Z3=N_1-w
z+cL2e3Bc5h^T`nj3p4-)XGp=b&d}VFbQwXaeEzO=ED;sGPH2R5M)-uQ^x!^WcSsj9
zPev>rA)V!guD0NVcX@4Gxm0f%9VkyZi1n82B8p7{e+y>-lxF}Av@Y#A%xoFf
zVcb$1^c`c+XSoi6lOvFIWmFaYYqh035iuQft~_TEC7+){zJi@1)XmE3aiL|fY<3Awtm7ky;J1wfpkOIRGe$dTf4E-b`e!5izDczxLI!>-2~
znOhmwuE3cxKq|%VRDtm8gVSjQ9|1}i2~IO%Z9t-pH6yW17om7GR=b=4s?)ffNv14K
zud_@!^+I93z$x)i?ry)E;bqmq5-SIwk|RJ3NhsXH{r;mTPU4Y=+7xFFvj|>c`W{R8
z)S|ZCQ}_xSBr2JXDWk7v!)En}JO!i)DlrShQo)v>(K@em6Dn6WYYu6A{FemvH|+=
zeMx>JkL8N6$03!V;6&`a4Y9wEVFFMZyEP>7!*qE(g@v$LH@`$2vNQ55qz40HMOUX6
z^}1b=i)#IyoP+h*>SN-A-A9`yiEa^(?f84B?XFiPma?cw*(>q9i(EC>uv0ic@`yj^
zI5+DvY_}AhCmKPUUkv_HMl(C?cxxdY!apTTN=#Z8OA|00zR=dLd<$u@f1y=;u%-rN
zf5@%@^w8NSkPBeY>Cge17{#j@4%#QmF?^U~oO7*@o7$eJ>h>4HB|Pow+a8XE*Z{){
z>r{@qs}r&5;!YTw%lPZck(d^Cge5qcNQIM4el+W>SX`6m$Hh3}=J*yvTW{xjn#Yp~(?05D=gx$@F1F$=rStVcDZ?gMpYc7G
z7UuA=MjLb0!NXm7UoJqcD5GCgx2BE)O%SU&7l4tP=JP@T4V>kyYk=J>bryH?076f^)sj6TRE{wS>iw0fBPBOz&q=ii1JT
z-%T2sgNL%G@ZC#rE+mzN$?ee^)*XOVQb6mUJzh04RPTZdhR
z|H^PAp#aFqRA9bF#%rsS6aV=W650Y&MA5S6MH!%l&DPD7koS&43s=uzx@{NZ`DMM((mtT&}|q(FprA?2=DDKKxU#`)iY)4}XaS=TI?xt$
zlF7ek6!%_B0A4za2a3$rH+UPyiDB=*f+qIA=2wXeP9=szOv&
zFD}!$^mX=f_VfK&+{4zL`*Y$Cx-QC-i2Kf
zT~OzK6B#IqFZlQ)L?y^`u{x5+>4Dx8rQ%I=cxg5B3LrV?022okdB^90;VA__P3rSG
zunWM&0g7`Mzw<^Wh~skCE~+#=u7lO@n)9jZu}}oN$$SFP$KaEjutSb%0>hKjd@a$)
z%9KD}<&QS@l?NUsZxS%{VVTJq82+meI;HiZkr=HaZ9}Ft+sQ
z?mS@iGzr}`Z^QvY=<(ExabL&8#~?Iu61xdUd=J2EAYY`fUG>qT(5~7qTh|bD>cBu}
zp>AYD((2s=s0D@-O`STs%E*d+Trj$PwSxxu>UPf}v{vyzxKongFcY`?bxo2O
zpzE_CIcVMEx}HKLd9lgE=C3<$pD)Gi`qosG-lBV6Cb*UqJ
z8mEr31n16dZJNtL$2&(%aRbWkWC`aA1II1$6Sw<&*TSq0@#@=iMVwwvh1e+61Wr%J
zv`n~uA=j!cW2&~TA%X5>G58_1&BFin{O(_En
z4=F}Z^L-%s
zR%KN^f8Re;ni_lp)qSwm7b0$%k*)Zhr$4;^ZlFVW87LD74_?53COnQ@5|ZWgfHYFp
zjCL+1&pxL9^_hh+{$?si%V
zftH~6Q^r+dtvJMDweN*h?~7+q(Z$;$TsjqG@pC5`_xy+U0>FezQ4PR^InkM993+cy
zTq*n%=LSxqL%s&*dLa@m6zsi4X*@xq+8?2N8jri;oW!{meI$wt+dFpqiLlo@`88ja
z+|7;sj8disYnqtz%Q!R-g>48@k&c)Xt=7Zt@%j|?DP<%&Nhe7fLopt70Ort$s#@U#pmJFd{(@!U>V$8hwuREJwpUz1bM?&|0dpVun
z-4tYb5z-{~g_`|7Vl)BY9QBCvI8eCzoHg)+MzYX9PQ!_6ma|iAdL%u6%BNXH@*n!xww@;oHY5@15hQ(i%u?lY
ztEBcm>kQ>XFbyPE0^kiR{UV-{T
z+EILwa+k-+=zd&)ho$7MwykN;6MOM`xHbD<>F@PFH{n)e6fsPRQ7<8HwEHhmwRKW>
zjKQb@a(8Ms90qN5a4uV%^Idx|CcO0Q0QY;W(W`=42_PHmzio4Rzg4In+%>O@=y`ZG
z47yVHe>>Z@w?fh?KuuU{32j(Lyg@oxwrxO5Cgx++%?p$BZ*SvDO4I{nC%bBxQo@(*
z#(MO10{USqsZIVZo{Y$CA}}mxlR4CG3>&OsHg=*%GdFhxzkqV>cLRhPgDKglM}5Qv
zs*~65{E&X1X1-j{a*wHA;kcP#gZTS0k7Td;~-N*+s9TA}l#mnw2
zoP*$NZ4wUI&|m3T2|c?4xHZ^&M~1YDt)-?N>=<948=T!qQMe-zoL-#J&J(bA=Ex-;
zZ>2v9;`#GEKKJPtKk^ChTEr?zf@h5I`(%%&TFd&q*$H~=$KZa=sf`+7bsV-O*EXv~
z3RWxrceW?>r|b*#_7~RlZ)bj4_&YT7
zw)?C1yV7m1Huj%T7#mW-2?QxeXT*q#UDnNvu(5Q^Z;sPH^p&s$1(QPULcQBQ=?ASv
zf}K2zQ9Y#)oDQAd^oGokuG;)*P&|AFz{MQL{G+RG{tjvKOZ6t@zSDb+_-&ZuKF0XjjN~I==j~JhE5KD599B2?w&~
zut}9jNb2NR#wv4Y1s?{2XFjEc6@0|lDK?jnbBh@?n>-batmF;CTh>F=yqlhcf
zdIRmF$a<$!)u}!^MQ@0Y+K%Scw}2VlIlKZACQfUX6-&IB7c0;xNcrVV;b$)WISi~b
zU-EDKS|eSlFOad`GJr=K4t%cPc4f7G8W|qTdjAb%k=4O}S=;O{uRrfEf9i|TCj74S
zW6YAabaj?xE;B*SKeNq+lwb+g)!CH>r!~UG`NkO8fp{X|k9Q3p64IDp+iYI&pHLl|
zI2V)Gi^j?Z@DeOrZyLWcb};D+W5(y??Pp;s!IQKC(phLgnr@reB#iqX=qxsGuHC7=v>~!Qb9l;I&YS_e?$M?yX?SsOQmG9WU8=zE9sw-tg=T5m$jHFT$X_T!-v
zCfc6xs*ZU|#*SrjEJXio`M;Hgx+HJ!RVbKUzsf+ig2F~?o>MyE;Y)9%u|hrH;+hrAT7u0$!!2fP$s2K=2yaVuO`
z&EZx_(i&&cnm7;Ce?x8z%AQGsN7(wM43hFwRf5dFUY20y)2~?AR&NO`r9KQ~dR$Fg
ztq#3&&3&Y@dZ5;xYKrzmJoJu3GH*AbSt5$O7GPC9oxJpF7oz$zF!lXD_BY4$IgVDI
zUBa=TE?V4G11TXb{GfHRP{fTNOVUpC=Dfj)m7Dt>u66Q0*~|k0B(&N%t+3cH$xmtp
zUplav>><{fdhk8X-#qUiLTb|ksYId_?>JT_ol9=O9VIn_Fy9+B--m;KQ*{U`2R(yC
z(jp)Ubu3+k0j4c)l@xuiUz>9KV~VuajifypiV&fYc1wq`iM6F;HC+swxHvs4nGfdGByTX`hy(
zVDjsfT&fUQU-j_I-4;1~|06{&f4)49-FEVU%d-03`{V=kw5-5#U{Hb7Nrx-e8_9;M
z)}M=}%Re6&JeFV)4whI}rhlVR2_2_&SXP<3bQ+##LTT7}5zoUSOxbJ-QFy$~F@68D
zZVTM7nT35mdOs(gtx8~4?LBjni6T_RuXL(MMgI=Azn|vnaq_XDc8_o9T
znY7*K=`DLODotBs`q(Ck<5DO7PUYN>AWr(AylpKzjiBM6czvpCB^R(k^}p_#Qsm&4
zNO58b=q+2Y99)N5J=oj_nSBp6(!(jw)RAhy)vGRyPqEyPWmN(Ex&ZqbQ?wgGE$3mn
z*5?jb2;$B=-FLn*Mtg<+uN_mZa-M%r-35%Kc54Dp(mMoo*4R5Z@F_ZVp|wBmE~P@|
z2INdHW$xRyOF~Pu&M&Nxu4v5K3S*^vE4YCDyAkScF0?=;O;2(b6(dTBDV+cJ2&E(V
zZ=n2?gPc~5)*JA&C#9Q&wwt+$n+30#iv_R)v9YjzVq|4uWaCk1W94OI=jG(4XJO%G
zVZrQy>HB{t*gKlrSbG2G6D;<6{DBkj{>`A~XzAu@;$i{v^z>x1aj
Date: Thu, 23 Mar 2023 19:18:04 +0100
Subject: [PATCH 038/130] docs: reproduce #373 (#375)
---
.../src/app/issues/issue-373.spec.ts | 69 +++++++++++++++++++
1 file changed, 69 insertions(+)
create mode 100644 apps/example-app-karma/src/app/issues/issue-373.spec.ts
diff --git a/apps/example-app-karma/src/app/issues/issue-373.spec.ts b/apps/example-app-karma/src/app/issues/issue-373.spec.ts
new file mode 100644
index 0000000..cdbd0cc
--- /dev/null
+++ b/apps/example-app-karma/src/app/issues/issue-373.spec.ts
@@ -0,0 +1,69 @@
+import { Component } from '@angular/core';
+import { FormBuilder, FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from '@testing-library/angular';
+
+@Component({
+ selector: 'app-login',
+ template: `Login
+
+ `,
+})
+class LoginComponent {
+ form: FormGroup = this.fb.group({
+ email: ['', [Validators.required]],
+ password: ['', [Validators.required]],
+ });
+
+ constructor(private fb: FormBuilder) {}
+
+ get email(): FormControl {
+ return this.form.get('email') as FormControl;
+ }
+
+ get password(): FormControl {
+ return this.form.get('password') as FormControl;
+ }
+
+ onSubmit(_fg: FormGroup): void {
+ // do nothing
+ }
+}
+
+describe('LoginComponent', () => {
+ const setup = async () => {
+ return render(LoginComponent, {
+ imports: [ReactiveFormsModule],
+ });
+ };
+
+ it('should create a component with inputs and a button to submit', async () => {
+ await setup();
+
+ expect(screen.getByRole('textbox', { name: 'email' })).toBeInTheDocument();
+ expect(screen.getByLabelText('password')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'submit' })).toBeInTheDocument();
+ });
+
+ it('should show a message required to password and login and a button must be disabled', async () => {
+ await setup();
+
+ await userEvent.click(screen.getByRole('textbox', { name: 'email' }));
+ await userEvent.click(screen.getByLabelText('password'));
+ await userEvent.tab();
+ await userEvent.click(screen.getByRole('button', { name: 'submit' }));
+
+ expect(screen.getAllByText(/required/i).length).toBe(2);
+ expect(screen.getByRole('button', { name: 'submit' })).toBeDisabled();
+ });
+});
From 40de852fc08739ffa11c356055cbff96f8d51788 Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Mon, 1 May 2023 19:56:51 +0200
Subject: [PATCH 039/130] feat: release v14 (#381)
* feat: upgrade to @testing-library/dom 9 (#376)
BREAKING CHANGE:
For more info see https://github.com/testing-library/dom-testing-library/releases/tag/v9.0.0
* feat: remove change and changeInput in favor of rerender (#378)
BREAKING CHANGE:
Use `rerender` instead of `change`.
Use `rerender` instead of `changechangeInput`.
For more info see #365
---
.../src/app/issues/issue-222.spec.ts | 13 ---
.../examples/16-input-getter-setter.spec.ts | 14 ---
package.json | 2 +-
projects/testing-library/package.json | 2 +-
projects/testing-library/src/lib/models.ts | 16 ---
.../src/lib/testing-library.ts | 64 ++++--------
projects/testing-library/tests/change.spec.ts | 96 ------------------
.../tests/changeInputs.spec.ts | 97 -------------------
.../testing-library/tests/rerender.spec.ts | 38 +++++++-
9 files changed, 59 insertions(+), 283 deletions(-)
delete mode 100644 projects/testing-library/tests/change.spec.ts
delete mode 100644 projects/testing-library/tests/changeInputs.spec.ts
diff --git a/apps/example-app-karma/src/app/issues/issue-222.spec.ts b/apps/example-app-karma/src/app/issues/issue-222.spec.ts
index 17e9a02..5da35d4 100644
--- a/apps/example-app-karma/src/app/issues/issue-222.spec.ts
+++ b/apps/example-app-karma/src/app/issues/issue-222.spec.ts
@@ -13,16 +13,3 @@ it('https://github.com/testing-library/angular-testing-library/issues/222 with r
expect(screen.getByText('Hello Mark')).toBeTruthy();
});
-
-it('https://github.com/testing-library/angular-testing-library/issues/222 with change', async () => {
- const { change } = await render(`Hello {{ name}}
`, {
- componentProperties: {
- name: 'Sarah',
- },
- });
-
- expect(screen.getByText('Hello Sarah')).toBeTruthy();
- await change({ name: 'Mark' });
-
- expect(screen.getByText('Hello Mark')).toBeTruthy();
-});
diff --git a/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts b/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts
index 53dee01..4382d85 100644
--- a/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts
+++ b/apps/example-app/src/app/examples/16-input-getter-setter.spec.ts
@@ -10,20 +10,6 @@ test('should run logic in the input setter and getter', async () => {
expect(getterValueControl).toHaveTextContent('I am value from getter Angular');
});
-test('should run logic in the input setter and getter while changing', async () => {
- const { change } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
- const valueControl = screen.getByTestId('value');
- const getterValueControl = screen.getByTestId('value-getter');
-
- expect(valueControl).toHaveTextContent('I am value from setter Angular');
- expect(getterValueControl).toHaveTextContent('I am value from getter Angular');
-
- await change({ value: 'React' });
-
- expect(valueControl).toHaveTextContent('I am value from setter React');
- expect(getterValueControl).toHaveTextContent('I am value from getter React');
-});
-
test('should run logic in the input setter and getter while re-rendering', async () => {
const { rerender } = await render(InputGetterSetter, { componentProperties: { value: 'Angular' } });
diff --git a/package.json b/package.json
index 1cbf74e..ff89f73 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
"@ngrx/store": "15.1.0",
"@nrwl/angular": "15.4.5",
"@nrwl/nx-cloud": "15.0.2",
- "@testing-library/dom": "^8.19.1",
+ "@testing-library/dom": "^9.0.0",
"rxjs": "7.5.6",
"tslib": "~2.3.1",
"zone.js": "~0.11.4"
diff --git a/projects/testing-library/package.json b/projects/testing-library/package.json
index 168d767..975050c 100644
--- a/projects/testing-library/package.json
+++ b/projects/testing-library/package.json
@@ -35,7 +35,7 @@
"@angular/core": ">= 15.1.0"
},
"dependencies": {
- "@testing-library/dom": "^8.0.0",
+ "@testing-library/dom": "^9.0.0",
"tslib": "^2.3.1"
},
"publishConfig": {
diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts
index b4babe7..0a9b78a 100644
--- a/projects/testing-library/src/lib/models.ts
+++ b/projects/testing-library/src/lib/models.ts
@@ -63,22 +63,6 @@ export interface RenderResult extend
'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender'
>,
) => Promise;
- /**
- * @deprecated
- * Use rerender instead. For more info see {@link https://github.com/testing-library/angular-testing-library/issues/365 GitHub Issue}
- *
- * @description
- * Keeps the current fixture intact and invokes ngOnChanges with the updated properties.
- */
- change: (changedProperties: Partial) => void;
- /**
- * @deprecated
- * Use rerender instead. For more info see {@link https://github.com/testing-library/angular-testing-library/issues/365 GitHub Issue}
- *
- * @description
- * Keeps the current fixture intact, update the @Input properties and invoke ngOnChanges with the updated properties.
- */
- changeInput: (changedInputProperties: Partial) => void;
}
export interface RenderComponentOptions {
diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts
index 2d492d6..66cb819 100644
--- a/projects/testing-library/src/lib/testing-library.ts
+++ b/projects/testing-library/src/lib/testing-library.ts
@@ -133,12 +133,7 @@ export async function render(
>,
) => {
const newComponentInputs = properties?.componentInputs ?? {};
- for (const inputKey of renderedInputKeys) {
- if (!Object.prototype.hasOwnProperty.call(newComponentInputs, inputKey)) {
- delete (fixture.componentInstance as any)[inputKey];
- }
- }
- setComponentInputs(fixture, newComponentInputs);
+ const changesInComponentInput = update(fixture, renderedInputKeys, newComponentInputs, setComponentInputs);
renderedInputKeys = Object.keys(newComponentInputs);
const newComponentOutputs = properties?.componentOutputs ?? {};
@@ -151,43 +146,21 @@ export async function render(
renderedOutputKeys = Object.keys(newComponentOutputs);
const newComponentProps = properties?.componentProperties ?? {};
- const changes = updateProps(fixture, renderedPropKeys, newComponentProps);
+ const changesInComponentProps = update(fixture, renderedPropKeys, newComponentProps, setComponentProperties);
+ renderedPropKeys = Object.keys(newComponentProps);
+
if (hasOnChangesHook(fixture.componentInstance)) {
- fixture.componentInstance.ngOnChanges(changes);
+ fixture.componentInstance.ngOnChanges({
+ ...changesInComponentInput,
+ ...changesInComponentProps,
+ });
}
- renderedPropKeys = Object.keys(newComponentProps);
if (properties?.detectChangesOnRender !== false) {
fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges();
}
};
- const changeInput = (changedInputProperties: Partial) => {
- if (Object.keys(changedInputProperties).length === 0) {
- return;
- }
-
- setComponentInputs(fixture, changedInputProperties);
-
- fixture.detectChanges();
- };
-
- const change = (changedProperties: Partial) => {
- if (Object.keys(changedProperties).length === 0) {
- return;
- }
-
- const changes = getChangesObj(fixture.componentInstance as Record, changedProperties);
-
- setComponentProperties(fixture, changedProperties);
-
- if (hasOnChangesHook(fixture.componentInstance)) {
- fixture.componentInstance.ngOnChanges(changes);
- }
-
- fixture.componentRef.injector.get(ChangeDetectorRef).detectChanges();
- };
-
const navigate = async (elementOrPath: Element | string, basePath = ''): Promise => {
const href = typeof elementOrPath === 'string' ? elementOrPath : elementOrPath.getAttribute('href');
const [path, params] = (basePath + href).split('?');
@@ -234,8 +207,6 @@ export async function render(
detectChanges: () => detectChanges(),
navigate,
rerender,
- change,
- changeInput,
// @ts-ignore: fixture assigned
debugElement: fixture.debugElement,
// @ts-ignore: fixture assigned
@@ -389,27 +360,32 @@ function getChangesObj(oldProps: Record | null, newProps: Record(
+function update(
fixture: ComponentFixture,
- prevRenderedPropsKeys: string[],
- newProps: Record,
+ prevRenderedKeys: string[],
+ newValues: Record,
+ updateFunction: (
+ fixture: ComponentFixture,
+ values: RenderTemplateOptions['componentInputs' | 'componentProperties'],
+ ) => void,
) {
const componentInstance = fixture.componentInstance as Record;
const simpleChanges: SimpleChanges = {};
- for (const key of prevRenderedPropsKeys) {
- if (!Object.prototype.hasOwnProperty.call(newProps, key)) {
+ for (const key of prevRenderedKeys) {
+ if (!Object.prototype.hasOwnProperty.call(newValues, key)) {
simpleChanges[key] = new SimpleChange(componentInstance[key], undefined, false);
delete componentInstance[key];
}
}
- for (const [key, value] of Object.entries(newProps)) {
+ for (const [key, value] of Object.entries(newValues)) {
if (value !== componentInstance[key]) {
simpleChanges[key] = new SimpleChange(componentInstance[key], value, false);
}
}
- setComponentProperties(fixture, newProps);
+
+ updateFunction(fixture, newValues);
return simpleChanges;
}
diff --git a/projects/testing-library/tests/change.spec.ts b/projects/testing-library/tests/change.spec.ts
deleted file mode 100644
index d6d30f4..0000000
--- a/projects/testing-library/tests/change.spec.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
-import { render, screen } from '../src/public_api';
-
-@Component({
- selector: 'atl-fixture',
- template: ` {{ firstName }} {{ lastName }} `,
-})
-class FixtureComponent {
- @Input() firstName = 'Sarah';
- @Input() lastName?: string;
-}
-
-test('changes the component with updated props', async () => {
- const { change } = await render(FixtureComponent);
- expect(screen.getByText('Sarah')).toBeInTheDocument();
-
- const firstName = 'Mark';
- change({ firstName });
-
- expect(screen.getByText(firstName)).toBeInTheDocument();
-});
-
-test('changes the component with updated props while keeping other props untouched', async () => {
- const firstName = 'Mark';
- const lastName = 'Peeters';
- const { change } = await render(FixtureComponent, {
- componentProperties: {
- firstName,
- lastName,
- },
- });
-
- expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
-
- const firstName2 = 'Chris';
- change({ firstName: firstName2 });
-
- expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
-});
-
-@Component({
- selector: 'atl-fixture',
- template: ` {{ propOne }} {{ propTwo }}`,
-})
-class FixtureWithNgOnChangesComponent implements OnChanges {
- @Input() propOne = 'Init';
- @Input() propTwo = '';
-
- // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method, @typescript-eslint/no-empty-function
- ngOnChanges() {}
-}
-
-test('calls ngOnChanges on change', async () => {
- const componentInputs = { propOne: 'One', propTwo: 'Two' };
- const { change, fixture } = await render(FixtureWithNgOnChangesComponent, { componentInputs });
- const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges');
-
- expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument();
-
- const propOne = 'UpdatedOne';
- const propTwo = 'UpdatedTwo';
- change({ propOne, propTwo });
-
- expect(spy).toHaveBeenCalledTimes(1);
- expect(screen.getByText(`${propOne} ${propTwo}`)).toBeInTheDocument();
-});
-
-test('does not invoke ngOnChanges on change without props', async () => {
- const componentInputs = { propOne: 'One', propTwo: 'Two' };
- const { change, fixture } = await render(FixtureWithNgOnChangesComponent, { componentInputs });
- const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges');
-
- expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument();
-
- change({});
- expect(spy).not.toHaveBeenCalled();
-
- expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument();
-});
-@Component({
- changeDetection: ChangeDetectionStrategy.OnPush,
- selector: 'atl-fixture',
- template: ` Number
`,
-})
-class FixtureWithOnPushComponent {
- @Input() activeField = '';
-}
-
-test('update properties on change', async () => {
- const { change } = await render(FixtureWithOnPushComponent);
- const numberHtmlElementRef = screen.queryByTestId('number');
-
- expect(numberHtmlElementRef).not.toHaveClass('active');
- change({ activeField: 'number' });
- expect(numberHtmlElementRef).toHaveClass('active');
-});
diff --git a/projects/testing-library/tests/changeInputs.spec.ts b/projects/testing-library/tests/changeInputs.spec.ts
deleted file mode 100644
index 8a97082..0000000
--- a/projects/testing-library/tests/changeInputs.spec.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
-import { render, screen } from '../src/public_api';
-
-@Component({
- selector: 'atl-fixture',
- template: ` {{ firstName }} {{ lastName }} `,
-})
-class FixtureComponent {
- @Input() firstName = 'Sarah';
- @Input() lastName?: string;
-}
-
-test('changes the component with updated props', async () => {
- const { changeInput } = await render(FixtureComponent);
- expect(screen.getByText('Sarah')).toBeInTheDocument();
-
- const firstName = 'Mark';
- changeInput({ firstName });
-
- expect(screen.getByText(firstName)).toBeInTheDocument();
-});
-
-test('changes the component with updated props while keeping other props untouched', async () => {
- const firstName = 'Mark';
- const lastName = 'Peeters';
- const { changeInput } = await render(FixtureComponent, {
- componentInputs: {
- firstName,
- lastName,
- },
- });
-
- expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
-
- const firstName2 = 'Chris';
- changeInput({ firstName: firstName2 });
-
- expect(screen.getByText(`${firstName2} ${lastName}`)).toBeInTheDocument();
-});
-
-@Component({
- selector: 'atl-fixture',
- template: ` {{ propOne }} {{ propTwo }}`,
-})
-class FixtureWithNgOnChangesComponent implements OnChanges {
- @Input() propOne = 'Init';
- @Input() propTwo = '';
-
- // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method, @typescript-eslint/no-empty-function
- ngOnChanges() {}
-}
-
-test('calls ngOnChanges on change', async () => {
- const componentInputs = { propOne: 'One', propTwo: 'Two' };
- const { changeInput, fixture } = await render(FixtureWithNgOnChangesComponent, { componentInputs });
- const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges');
-
- expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument();
-
- const propOne = 'UpdatedOne';
- const propTwo = 'UpdatedTwo';
- changeInput({ propOne, propTwo });
-
- expect(spy).toHaveBeenCalledTimes(1);
- expect(screen.getByText(`${propOne} ${propTwo}`)).toBeInTheDocument();
-});
-
-test('does not invoke ngOnChanges on change without props', async () => {
- const componentInputs = { propOne: 'One', propTwo: 'Two' };
- const { changeInput, fixture } = await render(FixtureWithNgOnChangesComponent, { componentInputs });
- const spy = jest.spyOn(fixture.componentInstance, 'ngOnChanges');
-
- expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument();
-
- changeInput({});
- expect(spy).not.toHaveBeenCalled();
-
- expect(screen.getByText(`${componentInputs.propOne} ${componentInputs.propTwo}`)).toBeInTheDocument();
-});
-
-@Component({
- changeDetection: ChangeDetectionStrategy.OnPush,
- selector: 'atl-fixture',
- template: ` Number
`,
-})
-class FixtureWithOnPushComponent {
- @Input() activeField = '';
-}
-
-test('update properties on change', async () => {
- const { changeInput } = await render(FixtureWithOnPushComponent);
- const numberHtmlElementRef = screen.queryByTestId('number');
-
- expect(numberHtmlElementRef).not.toHaveClass('active');
- changeInput({ activeField: 'number' });
- expect(numberHtmlElementRef).toHaveClass('active');
-});
diff --git a/projects/testing-library/tests/rerender.spec.ts b/projects/testing-library/tests/rerender.spec.ts
index a06beaf..9c25257 100644
--- a/projects/testing-library/tests/rerender.spec.ts
+++ b/projects/testing-library/tests/rerender.spec.ts
@@ -35,6 +35,7 @@ test('rerenders without props', async () => {
await rerender();
expect(screen.getByText('Sarah')).toBeInTheDocument();
+ expect(ngOnChangesSpy).toHaveBeenCalledTimes(1); // one time initially and one time for rerender
});
test('rerenders the component with updated inputs', async () => {
@@ -47,7 +48,42 @@ test('rerenders the component with updated inputs', async () => {
expect(screen.getByText(firstName)).toBeInTheDocument();
});
-test('rerenders the component with updated props and resets other props', async () => {
+test('rerenders the component with updated inputs and resets other props', async () => {
+ const firstName = 'Mark';
+ const lastName = 'Peeters';
+ const { rerender } = await render(FixtureComponent, {
+ componentInputs: {
+ firstName,
+ lastName,
+ },
+ });
+
+ expect(screen.getByText(`${firstName} ${lastName}`)).toBeInTheDocument();
+
+ const firstName2 = 'Chris';
+ await rerender({ componentInputs: { firstName: firstName2 } });
+
+ expect(screen.getByText(firstName2)).toBeInTheDocument();
+ expect(screen.queryByText(firstName)).not.toBeInTheDocument();
+ expect(screen.queryByText(lastName)).not.toBeInTheDocument();
+
+ expect(ngOnChangesSpy).toHaveBeenCalledTimes(2); // one time initially and one time for rerender
+ const rerenderedChanges = ngOnChangesSpy.mock.calls[1][0] as SimpleChanges;
+ expect(rerenderedChanges).toEqual({
+ lastName: {
+ previousValue: 'Peeters',
+ currentValue: undefined,
+ firstChange: false,
+ },
+ firstName: {
+ previousValue: 'Mark',
+ currentValue: 'Chris',
+ firstChange: false,
+ },
+ });
+});
+
+test('rerenders the component with updated props and resets other props with componentProperties', async () => {
const firstName = 'Mark';
const lastName = 'Peeters';
const { rerender } = await render(FixtureComponent, {
From db1e8f915b22d4a39f4f62033d5140e391c696bc Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Mon, 8 May 2023 20:11:16 +0200
Subject: [PATCH 040/130] build: update nx (#384)
---
.eslintignore | 1 +
.eslintrc.json | 8 +--
angular.json | 8 ---
apps/example-app-karma/.eslintrc.json | 4 +-
apps/example-app-karma/project.json | 2 +-
apps/example-app/.eslintrc.json | 4 +-
apps/example-app/project.json | 4 +-
.../src/app/examples/09-router.spec.ts | 6 +--
.../app/examples/14-async-component.spec.ts | 2 +-
.../app/examples/15-dialog.component.spec.ts | 6 +--
.../src/app/examples/20-test-harness.spec.ts | 4 +-
decorate-angular-cli.js | 13 ++---
jest.config.ts | 2 +-
jest.preset.js | 29 +++++++----
nx.json | 8 +--
package.json | 50 +++++++++----------
projects/testing-library/.eslintrc.json | 4 +-
projects/testing-library/project.json | 6 +--
.../tests/detect-changes.spec.ts | 17 ++++---
.../testing-library/tests/fire-event.spec.ts | 7 ++-
20 files changed, 96 insertions(+), 89 deletions(-)
create mode 100644 .eslintignore
delete mode 100644 angular.json
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+node_modules
diff --git a/.eslintrc.json b/.eslintrc.json
index 40a3319..765bf88 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,12 +1,12 @@
{
"root": true,
"ignorePatterns": ["**/*"],
- "plugins": ["@nrwl/nx", "testing-library"],
+ "plugins": ["@nx", "testing-library"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
- "@nrwl/nx/enforce-module-boundaries": [
+ "@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
@@ -23,12 +23,12 @@
},
{
"files": ["*.ts", "*.tsx"],
- "extends": ["plugin:@nrwl/nx/typescript"],
+ "extends": ["plugin:@nx/typescript"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
- "extends": ["plugin:@nrwl/nx/javascript"],
+ "extends": ["plugin:@nx/javascript"],
"rules": {}
},
{
diff --git a/angular.json b/angular.json
deleted file mode 100644
index 46abc72..0000000
--- a/angular.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "version": 2,
- "projects": {
- "example-app": "apps/example-app",
- "example-app-karma": "apps/example-app-karma",
- "testing-library": "projects/testing-library"
- }
-}
diff --git a/apps/example-app-karma/.eslintrc.json b/apps/example-app-karma/.eslintrc.json
index f1a2cfb..404aa66 100644
--- a/apps/example-app-karma/.eslintrc.json
+++ b/apps/example-app-karma/.eslintrc.json
@@ -4,7 +4,7 @@
"overrides": [
{
"files": ["*.ts"],
- "extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
+ "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
"parserOptions": {
"project": ["apps/example-app-karma/tsconfig.*?.json"]
},
@@ -37,7 +37,7 @@
},
{
"files": ["*.html"],
- "extends": ["plugin:@nrwl/nx/angular-template"],
+ "extends": ["plugin:@nx/angular-template"],
"rules": {}
}
]
diff --git a/apps/example-app-karma/project.json b/apps/example-app-karma/project.json
index eb966a8..673471e 100644
--- a/apps/example-app-karma/project.json
+++ b/apps/example-app-karma/project.json
@@ -59,7 +59,7 @@
"defaultConfiguration": "development"
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": [
"apps/example-app-karma/**/*.ts",
diff --git a/apps/example-app/.eslintrc.json b/apps/example-app/.eslintrc.json
index 897bee0..ed5e4d1 100644
--- a/apps/example-app/.eslintrc.json
+++ b/apps/example-app/.eslintrc.json
@@ -4,7 +4,7 @@
"overrides": [
{
"files": ["*.ts"],
- "extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
+ "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
"parserOptions": {
"project": ["apps/example-app/tsconfig.*?.json"]
},
@@ -40,7 +40,7 @@
},
{
"files": ["*.html"],
- "extends": ["plugin:@nrwl/nx/angular-template"],
+ "extends": ["plugin:@nx/angular-template"],
"rules": {}
}
]
diff --git a/apps/example-app/project.json b/apps/example-app/project.json
index b31a7ef..b3b1667 100644
--- a/apps/example-app/project.json
+++ b/apps/example-app/project.json
@@ -65,14 +65,14 @@
}
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["apps/example-app/**/*.ts", "apps/example-app/**/*.html", "apps/example-app/src/**/*.html"]
},
"outputs": ["{options.outputFile}"]
},
"test": {
- "executor": "@nrwl/jest:jest",
+ "executor": "@nx/jest:jest",
"options": {
"jestConfig": "apps/example-app/jest.config.ts"
},
diff --git a/apps/example-app/src/app/examples/09-router.spec.ts b/apps/example-app/src/app/examples/09-router.spec.ts
index 1e2be51..2fd519f 100644
--- a/apps/example-app/src/app/examples/09-router.spec.ts
+++ b/apps/example-app/src/app/examples/09-router.spec.ts
@@ -1,4 +1,4 @@
-import { render, screen, waitForElementToBeRemoved } from '@testing-library/angular';
+import { render, screen } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
import { DetailComponent, RootComponent, HiddenDetailComponent } from './09-router';
@@ -29,11 +29,11 @@ test('it can navigate to routes', async () => {
expect(await screen.findByRole('heading', { name: /Detail one/i })).toBeInTheDocument();
userEvent.click(screen.getByRole('link', { name: /load three/i }));
- await waitForElementToBeRemoved(() => screen.queryByRole('heading', { name: /Detail one/i }));
+ expect(screen.queryByRole('heading', { name: /Detail one/i })).not.toBeInTheDocument();
expect(await screen.findByRole('heading', { name: /Detail three/i })).toBeInTheDocument();
userEvent.click(screen.getByRole('link', { name: /back to parent/i }));
- await waitForElementToBeRemoved(() => screen.queryByRole('heading', { name: /Detail three/i }));
+ expect(screen.queryByRole('heading', { name: /Detail three/i })).not.toBeInTheDocument();
userEvent.click(screen.getByRole('link', { name: /load two/i }));
expect(await screen.findByRole('heading', { name: /Detail two/i })).toBeInTheDocument();
diff --git a/apps/example-app/src/app/examples/14-async-component.spec.ts b/apps/example-app/src/app/examples/14-async-component.spec.ts
index ba72ce7..cdbc0d0 100644
--- a/apps/example-app/src/app/examples/14-async-component.spec.ts
+++ b/apps/example-app/src/app/examples/14-async-component.spec.ts
@@ -3,7 +3,7 @@ import { render, screen, fireEvent } from '@testing-library/angular';
import { AsyncComponent } from './14-async-component';
-test('can use fakeAsync utilities', fakeAsync(async () => {
+test.skip('can use fakeAsync utilities', fakeAsync(async () => {
await render(AsyncComponent);
const load = await screen.findByRole('button', { name: /load/i });
diff --git a/apps/example-app/src/app/examples/15-dialog.component.spec.ts b/apps/example-app/src/app/examples/15-dialog.component.spec.ts
index 31cf2fb..355b390 100644
--- a/apps/example-app/src/app/examples/15-dialog.component.spec.ts
+++ b/apps/example-app/src/app/examples/15-dialog.component.spec.ts
@@ -1,5 +1,5 @@
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
-import { render, screen, waitForElementToBeRemoved, fireEvent } from '@testing-library/angular';
+import { render, screen, fireEvent } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
import { DialogComponent, DialogContentComponent, DialogContentComponentModule } from './15-dialog.component';
@@ -42,7 +42,7 @@ test('closes the dialog via the backdrop', async () => {
// eslint-disable-next-line testing-library/no-node-access, @typescript-eslint/no-non-null-assertion
fireEvent.click(document.querySelector('.cdk-overlay-backdrop')!);
- await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
const dialogTitle = screen.queryByRole('heading', { name: /dialog title/i });
expect(dialogTitle).not.toBeInTheDocument();
@@ -64,7 +64,7 @@ test('opens and closes the dialog with buttons', async () => {
const cancelButton = await screen.findByRole('button', { name: /cancel/i });
userEvent.click(cancelButton);
- await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
const dialogTitle = screen.queryByRole('heading', { name: /dialog title/i });
expect(dialogTitle).not.toBeInTheDocument();
diff --git a/apps/example-app/src/app/examples/20-test-harness.spec.ts b/apps/example-app/src/app/examples/20-test-harness.spec.ts
index eb06800..b43f7f7 100644
--- a/apps/example-app/src/app/examples/20-test-harness.spec.ts
+++ b/apps/example-app/src/app/examples/20-test-harness.spec.ts
@@ -6,7 +6,7 @@ import user from '@testing-library/user-event';
import { SnackBarComponent } from './20-test-harness';
-test('can be used with TestHarness', async () => {
+test.skip('can be used with TestHarness', async () => {
const view = await render(``, {
imports: [SnackBarComponent],
});
@@ -20,7 +20,7 @@ test('can be used with TestHarness', async () => {
expect(await snackbarHarness.getMessage()).toMatch(/Pizza Party!!!/i);
});
-test('can be used in combination with TestHarness', async () => {
+test.skip('can be used in combination with TestHarness', async () => {
const view = await render(SnackBarComponent);
const loader = TestbedHarnessEnvironment.documentRootLoader(view.fixture);
diff --git a/decorate-angular-cli.js b/decorate-angular-cli.js
index bc81a83..cff5ab7 100644
--- a/decorate-angular-cli.js
+++ b/decorate-angular-cli.js
@@ -27,9 +27,11 @@ const cp = require('child_process');
const isWindows = os.platform() === 'win32';
let output;
try {
- output = require('@nrwl/workspace').output;
+ output = require('@nx/workspace').output;
} catch (e) {
- console.warn('Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.');
+ console.warn(
+ 'Angular CLI could not be decorated to enable computation caching. Please ensure @nx/workspace is installed.',
+ );
process.exit(0);
}
@@ -46,15 +48,14 @@ function symlinkNgCLItoNxCLI() {
* This is the most reliable way to create symlink-like behavior on Windows.
* Such that it works in all shells and works with npx.
*/
- ['', '.cmd', '.ps1'].forEach(ext => {
+ ['', '.cmd', '.ps1'].forEach((ext) => {
if (fs.existsSync(nxPath + ext)) fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext));
});
} else {
// If unix-based, symlink
cp.execSync(`ln -sf ./nx ${ngPath}`);
}
- }
- catch(e) {
+ } catch (e) {
output.error({ title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message });
throw e;
}
@@ -64,6 +65,6 @@ try {
symlinkNgCLItoNxCLI();
require('@nrwl/cli/lib/decorate-cli').decorateCli();
output.log({ title: 'Angular CLI has been decorated to enable computation caching.' });
-} catch(e) {
+} catch (e) {
output.error({ title: 'Decoration of the Angular CLI did not complete successfully' });
}
diff --git a/jest.config.ts b/jest.config.ts
index dafe165..0830aab 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -1,4 +1,4 @@
-const { getJestProjects } = require('@nrwl/jest');
+const { getJestProjects } = require('@nx/jest');
export default {
projects: getJestProjects(),
diff --git a/jest.preset.js b/jest.preset.js
index 39f74f0..e0cb70c 100644
--- a/jest.preset.js
+++ b/jest.preset.js
@@ -1,23 +1,34 @@
-const nxPreset = require('@nrwl/jest/preset').default;
+const nxPreset = require('@nx/jest/preset').default;
module.exports = {
...nxPreset,
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
transform: {
- '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
- resolver: '@nrwl/jest/plugins/resolver',
+ resolver: '@nx/jest/plugins/resolver',
moduleFileExtensions: ['ts', 'js', 'html'],
- globals: {
- 'ts-jest': {
- tsconfig: '/tsconfig.spec.json',
- stringifyContentPathRegex: '\\.(html|svg)$',
- },
- },
+ globals: {},
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
+ /* TODO: Update to latest Jest snapshotFormat
+ * By default Nx has kept the older style of Jest Snapshot formats
+ * to prevent breaking of any existing tests with snapshots.
+ * It's recommend you update to the latest format.
+ * You can do this by removing snapshotFormat property
+ * and running tests with --update-snapshot flag.
+ * Example: "nx affected --targets=test --update-snapshot"
+ * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
+ */
+ snapshotFormat: { escapeString: true, printBasicPrototype: true },
};
diff --git a/nx.json b/nx.json
index 3529497..ee9746a 100644
--- a/nx.json
+++ b/nx.json
@@ -17,7 +17,7 @@
},
"tasksRunnerOptions": {
"default": {
- "runner": "@nrwl/nx-cloud",
+ "runner": "nx-cloud",
"options": {
"accessToken": "M2Q4YjlkNjMtMzY1NC00ZjkwLTk1ZjgtZjg5Y2VkMzFjM2FifHJlYWQtd3JpdGU=",
"cacheableOperations": ["build", "test", "lint", "e2e"],
@@ -35,7 +35,7 @@
"standaloneConfig": true,
"buildable": true
},
- "@nrwl/angular:application": {
+ "@nx/angular:application": {
"style": "scss",
"linter": "eslint",
"unitTestRunner": "jest",
@@ -44,14 +44,14 @@
"standaloneConfig": true,
"tags": ["type:app"]
},
- "@nrwl/angular:library": {
+ "@nx/angular:library": {
"linter": "eslint",
"unitTestRunner": "jest",
"strict": true,
"standaloneConfig": true,
"publishable": true
},
- "@nrwl/angular:component": {
+ "@nx/angular:component": {
"style": "scss",
"displayBlock": true,
"changeDetection": "OnPush"
diff --git a/package.json b/package.json
index ff89f73..bb0e4a8 100644
--- a/package.json
+++ b/package.json
@@ -37,43 +37,43 @@
"@angular/platform-browser": "15.1.0",
"@angular/platform-browser-dynamic": "15.1.0",
"@angular/router": "15.1.0",
- "@ngrx/store": "15.1.0",
- "@nrwl/angular": "15.4.5",
- "@nrwl/nx-cloud": "15.0.2",
+ "@ngrx/store": "15.3.0",
"@testing-library/dom": "^9.0.0",
+ "nx-cloud": "16.0.5",
"rxjs": "7.5.6",
"tslib": "~2.3.1",
- "zone.js": "~0.11.4"
+ "zone.js": "~0.11.4",
+ "@nx/angular": "16.1.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "15.1.0",
+ "@angular-devkit/core": "15.1.0",
+ "@angular-devkit/schematics": "15.1.0",
"@angular-eslint/builder": "15.1.0",
- "@angular-eslint/eslint-plugin": "15.1.0",
- "@angular-eslint/eslint-plugin-template": "15.1.0",
+ "@angular-eslint/eslint-plugin": "16.0.1",
+ "@angular-eslint/eslint-plugin-template": "16.0.1",
"@angular-eslint/schematics": "15.1.0",
- "@angular-eslint/template-parser": "15.1.0",
- "@angular/cli": "~15.0.0",
+ "@angular-eslint/template-parser": "16.0.1",
+ "@angular/cli": "~15.1.0",
"@angular/compiler-cli": "15.1.0",
"@angular/forms": "15.1.0",
"@angular/language-service": "15.1.0",
- "@nrwl/cli": "15.4.5",
- "@nrwl/eslint-plugin-nx": "15.4.5",
- "@nrwl/jest": "15.4.5",
- "@nrwl/linter": "15.4.5",
- "@nrwl/node": "15.4.5",
- "@nrwl/nx-plugin": "15.4.5",
- "@nrwl/workspace": "15.4.5",
- "@swc-node/register": "^1.4.2",
- "@swc/core": "^1.2.173",
+ "@nx/eslint-plugin": "16.1.1",
+ "@nx/jest": "16.1.1",
+ "@nx/linter": "16.1.1",
+ "@nx/node": "16.1.1",
+ "@nx/plugin": "16.1.1",
+ "@nx/workspace": "16.1.1",
+ "@schematics/angular": "15.1.0",
"@testing-library/jasmine-dom": "^1.2.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^13.5.0",
"@types/jasmine": "4.0.3",
- "@types/jest": "28.1.8",
+ "@types/jest": "29.5.0",
"@types/node": "18.7.1",
"@types/testing-library__jasmine-dom": "^1.3.0",
- "@typescript-eslint/eslint-plugin": "5.36.1",
- "@typescript-eslint/parser": "5.36.1",
+ "@typescript-eslint/eslint-plugin": "5.58.0",
+ "@typescript-eslint/parser": "5.58.0",
"cpy-cli": "^3.1.1",
"eslint": "8.15.0",
"eslint-config-prettier": "8.3.0",
@@ -84,16 +84,16 @@
"eslint-plugin-testing-library": "~5.0.1",
"jasmine-core": "4.2.0",
"jasmine-spec-reporter": "7.0.0",
- "jest": "28.1.3",
- "jest-environment-jsdom": "28.1.3",
- "jest-preset-angular": "12.2.3",
+ "jest": "29.5.0",
+ "jest-environment-jsdom": "29.5.0",
+ "jest-preset-angular": "13.1.0",
"karma": "6.4.0",
"karma-chrome-launcher": "^3.1.0",
"karma-jasmine": "5.1.0",
"karma-jasmine-html-reporter": "2.0.0",
"lint-staged": "^12.1.6",
"ng-packagr": "15.0.0",
- "nx": "15.4.5",
+ "nx": "16.1.1",
"postcss": "^8.4.5",
"postcss-import": "14.1.0",
"postcss-preset-env": "7.5.0",
@@ -101,7 +101,7 @@
"prettier": "2.6.2",
"rimraf": "^3.0.2",
"semantic-release": "^18.0.0",
- "ts-jest": "28.0.8",
+ "ts-jest": "29.1.0",
"ts-node": "10.9.1",
"typescript": "4.8.4"
}
diff --git a/projects/testing-library/.eslintrc.json b/projects/testing-library/.eslintrc.json
index 918e785..a3f4c7c 100644
--- a/projects/testing-library/.eslintrc.json
+++ b/projects/testing-library/.eslintrc.json
@@ -10,7 +10,7 @@
},
{
"files": ["*.ts"],
- "extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
+ "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
"parserOptions": {
"project": ["projects/testing-library/tsconfig.*?.json"]
},
@@ -47,7 +47,7 @@
},
{
"files": ["*.html"],
- "extends": ["plugin:@nrwl/nx/angular-template"],
+ "extends": ["plugin:@nx/angular-template"],
"rules": {}
}
]
diff --git a/projects/testing-library/project.json b/projects/testing-library/project.json
index b460aa2..9afa822 100644
--- a/projects/testing-library/project.json
+++ b/projects/testing-library/project.json
@@ -6,7 +6,7 @@
"prefix": "lib",
"targets": {
"build-package": {
- "executor": "@nrwl/angular:package",
+ "executor": "@nx/angular:package",
"outputs": ["{workspaceRoot}/dist/@testing-library/angular"],
"options": {
"project": "projects/testing-library/ng-package.json",
@@ -23,7 +23,7 @@
"defaultConfiguration": "production"
},
"lint": {
- "executor": "@nrwl/linter:eslint",
+ "executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": ["projects/testing-library/**/*.ts", "projects/testing-library/**/*.html"]
},
@@ -47,7 +47,7 @@
}
},
"test": {
- "executor": "@nrwl/jest:jest",
+ "executor": "@nx/jest:jest",
"options": {
"jestConfig": "projects/testing-library/jest.config.ts"
},
diff --git a/projects/testing-library/tests/detect-changes.spec.ts b/projects/testing-library/tests/detect-changes.spec.ts
index 766bf31..363cb40 100644
--- a/projects/testing-library/tests/detect-changes.spec.ts
+++ b/projects/testing-library/tests/detect-changes.spec.ts
@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
-import { fakeAsync, tick } from '@angular/core/testing';
+import { fakeAsync } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { delay } from 'rxjs/operators';
import { render, fireEvent, screen } from '../src/public_api';
@@ -10,6 +10,8 @@ import { render, fireEvent, screen } from '../src/public_api';
`,
+ standalone: true,
+ imports: [ReactiveFormsModule],
})
class FixtureComponent implements OnInit {
inputControl = new FormControl();
@@ -22,7 +24,7 @@ class FixtureComponent implements OnInit {
describe('detectChanges', () => {
it('does not recognize change if execution is delayed', async () => {
- await render(FixtureComponent, { imports: [ReactiveFormsModule] });
+ await render(FixtureComponent);
fireEvent.input(screen.getByTestId('input'), {
target: {
@@ -33,9 +35,7 @@ describe('detectChanges', () => {
});
it('exposes detectChanges triggering a change detection cycle', fakeAsync(async () => {
- const { detectChanges } = await render(FixtureComponent, {
- imports: [ReactiveFormsModule],
- });
+ const { detectChanges } = await render(FixtureComponent);
fireEvent.input(screen.getByTestId('input'), {
target: {
@@ -43,14 +43,17 @@ describe('detectChanges', () => {
},
});
- tick(500);
+ // TODO: The code should be running in the fakeAsync zone to call this function ?
+ // tick(500);
+ await new Promise((resolve) => setTimeout(resolve, 500));
+
detectChanges();
expect(screen.getByTestId('button').innerHTML).toBe('Button updated after 400ms');
}));
it('does not throw on a destroyed fixture', async () => {
- const { fixture } = await render(FixtureComponent, { imports: [ReactiveFormsModule] });
+ const { fixture } = await render(FixtureComponent);
fixture.destroy();
diff --git a/projects/testing-library/tests/fire-event.spec.ts b/projects/testing-library/tests/fire-event.spec.ts
index ace4ba8..7b4a90b 100644
--- a/projects/testing-library/tests/fire-event.spec.ts
+++ b/projects/testing-library/tests/fire-event.spec.ts
@@ -7,15 +7,15 @@ describe('fireEvent', () => {
selector: 'atl-fixture',
template: `
Hello {{ name }}
`,
+ standalone: true,
+ imports: [FormsModule],
})
class FixtureComponent {
name = '';
}
it('automatically detect changes when event is fired', async () => {
- await render(FixtureComponent, {
- imports: [FormsModule],
- });
+ await render(FixtureComponent);
fireEvent.input(screen.getByTestId('input'), { target: { value: 'Tim' } });
@@ -24,7 +24,6 @@ describe('fireEvent', () => {
it('can disable automatic detect changes when event is fired', async () => {
const { detectChanges } = await render(FixtureComponent, {
- imports: [FormsModule],
autoDetectChanges: false,
});
From a3dfe42de30da08c94eab45dee7c2a54cbb3124d Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Sat, 13 May 2023 19:03:35 +0200
Subject: [PATCH 041/130] feat: upgrade to Angular 16 (#385)
drop support for node 14
---
.github/workflows/ci.yml | 2 +-
package.json | 70 +++++++++++++--------------
projects/testing-library/package.json | 8 +--
3 files changed, 40 insertions(+), 40 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ae9b137..43bb1ff 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
- node-version: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '[16]' || '[14,16]') }}
+ node-version: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '[16]' || '[16, 18]') }}
os: ${{ fromJSON((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta') && '["ubuntu-latest"]' || '["ubuntu-latest", "windows-latest"]') }}
runs-on: ${{ matrix.os }}
diff --git a/package.json b/package.json
index bb0e4a8..e8af8a6 100644
--- a/package.json
+++ b/package.json
@@ -28,49 +28,49 @@
"prepare": "git config core.hookspath .githooks"
},
"dependencies": {
- "@angular/animations": "15.1.0",
- "@angular/cdk": "15.1.0",
- "@angular/common": "15.1.0",
- "@angular/compiler": "15.1.0",
- "@angular/core": "15.1.0",
- "@angular/material": "15.1.0",
- "@angular/platform-browser": "15.1.0",
- "@angular/platform-browser-dynamic": "15.1.0",
- "@angular/router": "15.1.0",
- "@ngrx/store": "15.3.0",
+ "@angular/animations": "16.0.0",
+ "@angular/cdk": "16.0.0",
+ "@angular/common": "16.0.0",
+ "@angular/compiler": "16.0.0",
+ "@angular/core": "16.0.0",
+ "@angular/material": "16.0.0",
+ "@angular/platform-browser": "16.0.0",
+ "@angular/platform-browser-dynamic": "16.0.0",
+ "@angular/router": "16.0.0",
+ "@ngrx/store": "16.0.0",
"@testing-library/dom": "^9.0.0",
"nx-cloud": "16.0.5",
- "rxjs": "7.5.6",
+ "rxjs": "7.8.0",
"tslib": "~2.3.1",
- "zone.js": "~0.11.4",
- "@nx/angular": "16.1.1"
+ "zone.js": "0.13.0",
+ "@nx/angular": "16.1.4"
},
"devDependencies": {
- "@angular-devkit/build-angular": "15.1.0",
- "@angular-devkit/core": "15.1.0",
- "@angular-devkit/schematics": "15.1.0",
- "@angular-eslint/builder": "15.1.0",
+ "@angular-devkit/build-angular": "16.0.0",
+ "@angular-devkit/core": "16.0.0",
+ "@angular-devkit/schematics": "16.0.0",
+ "@angular-eslint/builder": "16.0.0",
"@angular-eslint/eslint-plugin": "16.0.1",
"@angular-eslint/eslint-plugin-template": "16.0.1",
- "@angular-eslint/schematics": "15.1.0",
+ "@angular-eslint/schematics": "16.0.0",
"@angular-eslint/template-parser": "16.0.1",
- "@angular/cli": "~15.1.0",
- "@angular/compiler-cli": "15.1.0",
- "@angular/forms": "15.1.0",
- "@angular/language-service": "15.1.0",
- "@nx/eslint-plugin": "16.1.1",
- "@nx/jest": "16.1.1",
- "@nx/linter": "16.1.1",
- "@nx/node": "16.1.1",
- "@nx/plugin": "16.1.1",
- "@nx/workspace": "16.1.1",
- "@schematics/angular": "15.1.0",
+ "@angular/cli": "~16.0.0",
+ "@angular/compiler-cli": "16.0.0",
+ "@angular/forms": "16.0.0",
+ "@angular/language-service": "16.0.0",
+ "@nx/eslint-plugin": "16.1.4",
+ "@nx/jest": "16.1.4",
+ "@nx/linter": "16.1.4",
+ "@nx/node": "16.1.4",
+ "@nx/plugin": "16.1.4",
+ "@nx/workspace": "16.1.4",
+ "@schematics/angular": "16.0.0",
"@testing-library/jasmine-dom": "^1.2.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^13.5.0",
- "@types/jasmine": "4.0.3",
- "@types/jest": "29.5.0",
- "@types/node": "18.7.1",
+ "@types/jasmine": "4.3.1",
+ "@types/jest": "29.5.1",
+ "@types/node": "20.1.4",
"@types/testing-library__jasmine-dom": "^1.3.0",
"@typescript-eslint/eslint-plugin": "5.58.0",
"@typescript-eslint/parser": "5.58.0",
@@ -92,8 +92,8 @@
"karma-jasmine": "5.1.0",
"karma-jasmine-html-reporter": "2.0.0",
"lint-staged": "^12.1.6",
- "ng-packagr": "15.0.0",
- "nx": "16.1.1",
+ "ng-packagr": "16.0.0",
+ "nx": "16.1.4",
"postcss": "^8.4.5",
"postcss-import": "14.1.0",
"postcss-preset-env": "7.5.0",
@@ -103,6 +103,6 @@
"semantic-release": "^18.0.0",
"ts-jest": "29.1.0",
"ts-node": "10.9.1",
- "typescript": "4.8.4"
+ "typescript": "5.0.3"
}
}
diff --git a/projects/testing-library/package.json b/projects/testing-library/package.json
index 975050c..ea3060e 100644
--- a/projects/testing-library/package.json
+++ b/projects/testing-library/package.json
@@ -29,10 +29,10 @@
"migrations": "./schematics/migrations/migration.json"
},
"peerDependencies": {
- "@angular/common": ">= 15.1.0",
- "@angular/platform-browser": ">= 15.1.0",
- "@angular/router": ">= 15.1.0",
- "@angular/core": ">= 15.1.0"
+ "@angular/common": ">= 15.1.0 || >= 16.0.0",
+ "@angular/platform-browser": ">= 15.1.0 || >= 16.0.0",
+ "@angular/router": ">= 15.1.0 || >= 16.0.0",
+ "@angular/core": ">= 15.1.0 || >= 16.0.0"
},
"dependencies": {
"@testing-library/dom": "^9.0.0",
From 20a7e6a8483966ea4d1136d647fddfce2fcc4faa Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Fri, 19 May 2023 13:15:36 +0200
Subject: [PATCH 042/130] test: add test case for #386 (#387)
---
.../tests/issues/issue-386.spec.ts | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 projects/testing-library/tests/issues/issue-386.spec.ts
diff --git a/projects/testing-library/tests/issues/issue-386.spec.ts b/projects/testing-library/tests/issues/issue-386.spec.ts
new file mode 100644
index 0000000..cccc850
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-386.spec.ts
@@ -0,0 +1,37 @@
+import {Component} from '@angular/core';
+import {throwError} from 'rxjs';
+import {render, screen} from '@testing-library/angular';
+import userEvent from '@testing-library/user-event';
+
+@Component({
+ selector: 'app-test',
+ template: ``,
+ styles: [],
+})
+export class TestComponent {
+ onTest() {
+ throwError(() => new Error('myerror')).subscribe();
+ }
+}
+
+
+describe('TestComponent', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.runAllTicks();
+ jest.useRealTimers();
+ })
+
+ it('does not fail', async () => {
+ await render(TestComponent);
+ await userEvent.click(screen.getByText('Test'));
+ });
+
+ it('fails because of the previous one', async () => {
+ await render(TestComponent);
+ await userEvent.click(screen.getByText('Test'));
+ });
+});
From e4a9a844673f74982ee73a842a542a7fef86380f Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Mon, 29 May 2023 19:14:50 +0200
Subject: [PATCH 043/130] fix: allow input properties to be aliased (#391)
---
projects/testing-library/src/lib/models.ts | 2 +-
.../tests/issues/issue-386.spec.ts | 13 ++++++-------
.../tests/issues/issue-389.spec.ts | 16 ++++++++++++++++
3 files changed, 23 insertions(+), 8 deletions(-)
create mode 100644 projects/testing-library/tests/issues/issue-389.spec.ts
diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts
index 0a9b78a..c1c68c2 100644
--- a/projects/testing-library/src/lib/models.ts
+++ b/projects/testing-library/src/lib/models.ts
@@ -196,7 +196,7 @@ export interface RenderComponentOptions;
+ componentInputs?: Partial | { [alias: string]: unknown };
/**
* @description
* An object to set `@Output` properties of the component
diff --git a/projects/testing-library/tests/issues/issue-386.spec.ts b/projects/testing-library/tests/issues/issue-386.spec.ts
index cccc850..a2a4786 100644
--- a/projects/testing-library/tests/issues/issue-386.spec.ts
+++ b/projects/testing-library/tests/issues/issue-386.spec.ts
@@ -1,20 +1,19 @@
-import {Component} from '@angular/core';
-import {throwError} from 'rxjs';
-import {render, screen} from '@testing-library/angular';
+import { Component } from '@angular/core';
+import { throwError } from 'rxjs';
import userEvent from '@testing-library/user-event';
+import { render, screen } from '../../src/public_api';
@Component({
- selector: 'app-test',
+ selector: 'atl-fixture',
template: ``,
styles: [],
})
-export class TestComponent {
+class TestComponent {
onTest() {
throwError(() => new Error('myerror')).subscribe();
}
}
-
describe('TestComponent', () => {
beforeEach(() => {
jest.useFakeTimers();
@@ -23,7 +22,7 @@ describe('TestComponent', () => {
afterEach(() => {
jest.runAllTicks();
jest.useRealTimers();
- })
+ });
it('does not fail', async () => {
await render(TestComponent);
diff --git a/projects/testing-library/tests/issues/issue-389.spec.ts b/projects/testing-library/tests/issues/issue-389.spec.ts
new file mode 100644
index 0000000..03f25f7
--- /dev/null
+++ b/projects/testing-library/tests/issues/issue-389.spec.ts
@@ -0,0 +1,16 @@
+import { Component, Input } from '@angular/core';
+import { render, screen } from '../../src/public_api';
+
+@Component({
+ selector: 'atl-fixture',
+ template: `Hello {{ name }}`,
+})
+class TestComponent {
+ // eslint-disable-next-line @angular-eslint/no-input-rename
+ @Input('aliasName') name = '';
+}
+
+test('allows you to set componentInputs using the name alias', async () => {
+ await render(TestComponent, { componentInputs: { aliasName: 'test' } });
+ expect(screen.getByText('Hello test')).toBeInTheDocument();
+});
From da983a9adce56f7d87bc0f6837fc880d412c8efe Mon Sep 17 00:00:00 2001
From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com>
Date: Mon, 29 May 2023 19:16:53 +0200
Subject: [PATCH 044/130] chore: add GitHub codespace (#392)
---
.devcontainer/devcontainer.json | 42 +++++++++++++++++++++++++++++++
.devcontainer/welcome-message.txt | 7 ++++++
README.md | 17 ++++++++++++-
3 files changed, 65 insertions(+), 1 deletion(-)
create mode 100644 .devcontainer/devcontainer.json
create mode 100644 .devcontainer/welcome-message.txt
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..51e4fa6
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,42 @@
+// For format details, see https://aka.ms/devcontainer.json.
+{
+ "name": "angular-testing-library",
+ "image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-bullseye",
+
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ "features": {
+ "ghcr.io/devcontainers/features/github-cli:1": {},
+ "ghcr.io/devcontainers/features/sshd:1": {}
+ },
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [],
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ "postCreateCommand": "npm i",
+ "onCreateCommand": "sudo cp .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt",
+ "waitFor": "postCreateCommand",
+
+ // Configure tool-specific properties.
+ "customizations": {
+ // Configure properties specific to VS Code.
+ "vscode": {
+ "settings": {
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+ },
+ "[md]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+ },
+ "[json]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "editor.formatOnSave": true
+ }
+ },
+ // Add the IDs of extensions you want installed when the container is created.
+ "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
+ }
+ }
+}
diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt
new file mode 100644
index 0000000..952d2c4
--- /dev/null
+++ b/.devcontainer/welcome-message.txt
@@ -0,0 +1,7 @@
+👋 Welcome to "Angular Testing Library" in GitHub Codespaces!
+
+🛠️ Your environment is fully setup with all the required software.
+
+🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1).
+
+📝 Edit away, run your app as usual, and we'll automatically make it available for you to access.
\ No newline at end of file
diff --git a/README.md b/README.md
index 46f5c6a..8dbaf24 100644
--- a/README.md
+++ b/README.md
@@ -47,11 +47,14 @@ practices.