From 6521fe3416f0e127c4fd18cf036e976061f6c202 Mon Sep 17 00:00:00 2001 From: Kimi Agent Date: Tue, 12 May 2026 12:18:32 +0000 Subject: [PATCH] test(e2e): add download flow tests and fix status CSS classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Playwright E2E tests covering real user download journeys: - Search anime → choose episodes → trigger download (with toast) - Real file download via static fixture and verify completion in UI - Click new release on homepage → switch to anime search tab - Search for series and display mocked results - Fix bug in downloads_list.html: CSS classes used task.status (enum) which rendered as 'status-DownloadStatus.COMPLETED' instead of 'status-completed'. Use task.status.value for correct CSS class names. - Add static test fixture (20KB fake MP4) for reliable download tests - All 16 E2E tests passing (12 existing + 4 new) --- static/test_download/test_episode_01.mp4 | Bin 0 -> 20480 bytes templates/components/downloads_list.html | 4 +- tests/e2e/download_flow.spec.ts | 319 +++++++++++++++++++++++ 3 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 static/test_download/test_episode_01.mp4 create mode 100644 tests/e2e/download_flow.spec.ts diff --git a/static/test_download/test_episode_01.mp4 b/static/test_download/test_episode_01.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..085fd901ff73904b2826ebd2a52f74c5e7bd523b GIT binary patch literal 20480 zcmV(zK<2*?|L4gc6k9G3yd*~j1H_fb2|%!`JnkXU<^m~Y=G0Hz)h;d#et=NNYHTDC z$za$T?^%xE12I&LPB{A75+c6;-OhjJ6TIjDR#YIHq-67fQltK&er1R(EP^|t4NNvq z#8wG8^d`|c(+%3e+?gjpzitmPu5`?<8(-@RZ(JD2+Qhz0VXAyWY&u;$WsVono1{#Dmp2^?_y4Scc@QyTx>OM!6ld6(2FnN zE4GZ=%(+r)LaKh>yBUZO4?|xcR^TWyMT+6Rd2>-AGriulT=t612p5p5@ZF|OTAkK& zp?FxHpSN9xd4F)vZCYBpA@Jg;rwY&8$mk?tA-Y;!;@9!yQ$b9c*SE03CcRa z5NA31PDW<0xu=zwI8Wq%%Zv(Z!DP>>Hd9zPB5vKG^Y;RfrB`N}Ie91(D9MzpYTvq) z_S^ATO*wnG9S&cjwl>{>{h$bqU4ul4DX3k6pObGIL!;>UAWP?(d7#Yf>MO1g+EY{E z5gN>caP6ymd`no>Vy#Q_y+(O9Zg|8k@mqs7K$&6)W6ocmgjitsHjeakd>#go;t8H% z&Y|%JvR>hpO(H0GN!)V^X4T0FM-}Wf&p$gWr|LUh3Pvi?VdV-NDGz5sLb}oVp)NwD zj8SY_`WT0M&NY4?c_m|KbvCe9qxIZ31q*BZP=a!3z$gg0*9D8DA7_rbDaCC&8i+0I zN^q&dj(~B(LuDwFeGd@4F*wok(K=v`6EHHuDjQqNMt;HsOg=hGc_vyo)2DDu4-ifP zP+NsT^;_>nL26Pzv?_>$+sAS5ZOU0j23-U^w_Glt?UxTdXtA<+dVj99@!(N^gr2p03-c^>6%!;+!21OY5|D_fc)$ee!CRzT|l5#I`F(xIUke&|bTrs)EmvEGr z=Yb1D29i->{NVGpe|Kd^bO8sFModyjf#yEKl!^R7DN0+TEdz6FXC?kHGkva=Xb-Q( z0$j+!@2F@vuUS+4St)3>^m6Uj$T#%Xm^d6B_y!9nqD*GA7?zF9fjtP_M)7)WKIL@| zi)c~L{{K8lrO2wOLAzKe1p}lvWp6|#0l6b?C}1y6UBGJI7Y31Ov0>**a<5VH)wheM zKYQiJMDW0jQhrQ&?KhGC@1^RD=9b_LzPjr7L}tE-WN#7po&oe`L+Rxx9f4_G_)P5n zKoh#k^ESQV=z>Qe@+@5a=m$DV$4+|d!K0%6Lr3<5*FI@Dq}cf4XiK_{sK4bdWGJF} z=2zzvRfF5DLy-|*pHg$Ytzu9PfyzsL@gry)zxsQHVCH`#6v1W?Y>t1DSovjq8s?0< z13VTph6|4X9EBCgEsVa%X8AZE&=eQVdT)NhmWR8UH$Ly3=yDw>Kfg3SIBzx!EUMv{ zPAoII0&;cvJ7w>;zh6GW#4aX{2R4^`{=|k8@bgjwFNm1dIKs!Dy&dbqF?f$@Jgc>n zsUz!e7SD94QREiJLKo=f2H)aCtq6`|!5IO{^jW}D#5yA5$dja2{4xb;4*XWIdEy&x zO6nzdseo3GBIZCh9_et^ShIe9vq#g2;EL4eD#ho*+iQ= zWm(wr?zyc`t>k*{SFOjvCUup0_`^ewt=9nqK|KVv=PGK@BTpW|OKb1zVt5JJ>!H~* zR+D$3?^N3&8m=OxcLr;ls3UB(#h;+bXD<{nR|i zI+F4Wq>HUFTA=koxlXb&r=gw;VO6VoW+IHX@$83V1;ZqZ<5fT7UL9Rf-AM%n)VpYt zgl%}CyOuOJiUVy*q40&+VfY$HIwoMf^%>f6DlKkA%?btbh$IvzHq~vB|oP<^5E%~ zE^s5=VG|-?f2153-(jf46NjuQLLCSJ)sH;|&uHl>B+_XWV=0OBOv8+QT4Y%8-q6k+5 z<{_=2iR;$>Xj*%T3RlKY<;O&Y&_nc`^paw;LXgReG^PDcLI-BO%W5 zJ*oD)(q*v77b#F=IT6obzy;MpiWbcr#FsivZ&EQ^uGJGNBgbM1bCfLg3YNXH81UthURtDZsjjQc)qeJ!=XUf6Rn`zfpueF@#a-QF=v% zgzysx_^q{(&GrFf>s&0Jtp-a(;$fK+#}LH#6FEg75A9}KEV*G+BE*)-AHXT0?uzX; z5Mz;h3Ws3+nv*?XW`DN97|83ooGKeNgap*#f#@PF!dVG-2j3pa3rGkX`DYR8)~X)} zPeV+ZykqZwjEfNfk9)!?j3kb21jY-^j+Hlnm!UelK$YcDCtEp`@`|u$dDB{<03c#6 zw5s`{)|hAymZX|Pcpk~@p;O=HybGeQ3U#iM_Cr*ZKT2@=P@Yhs6tJT%7-Q8}iOoDQ zMamv)*M%93V80gAWzzqAR-9 z)uoAOQ_yeK%e96~b|KyL5eFxTOzmLG^5Mf#XqE1T=sMANVu*f#^%o&9%zs#I z`+62@%`$d@OVF;ErxR%ROixr;)e8ySRPE64fm7-d*hOC;1xm)$5=4 zjWsg#^QvO4o*l9<1=S|id1l{O#Nj3}dMQ?uBEdYtc`TUG*A*`9$CN%7S+bDo`TnT} zFuS=zr7X7e$4dh|nuCjc|7aGl=!uMpgV%9Z4U?33?&_q$FA=SEQz9dgG_y0HAOpTD1!}X7 z`l8F;^h+nL<<9}7t1YblIT08FMS>*A#0jWj;-fMu7U2JE82-Q9Yljay`8E?t4J`03 znDK4wH;lleznQaDa9frOfWC@a#sXji?rFj%RIg!8mr2j=dLmbe@5+~^aI28*OU@`ahaENT~l20&X*ik ztDwu}%DTnv9Vv)|jz;_2&P3(zUo>O{y?PNy7XiRqJKV+fs?g`&E8_LXacc|b_*eCw zR4>0%HhwkwQvbN1vaz|wju+NM<=D2hw>=Hl&7YtInG)qQ!rrVF_{tzoalWTr+to60 z7f{Ks#5}4HJwodT@|ikMW}H&whW_UJC#UATm)v(fno8Gm5iYcpga9d-HNVl?xC$5F zd^9Lcuw7)(Amo|LHvV-krEiGF_xrd_kO?@cs#={-E#e}JThguU$c`&dM{4UV2WuGa zEz}u00d=$+FqW*>#EB2;wDxGRiXBo;GWwd|xz>PpI(0hoZHH2r7>(;Uqaw&N)ZfCm z#cfnggn0v{oR}GLMDe4NjC-d#%R7WRd0)HWDky}aFERVWWR3V+-X1sfVkQ<)tRLAM zGse{yF=84fA^BBwz?@A;^fdSHcVL4(v)3%rn*>?$pW@11T&A3w!|wqAf{Mc|cS$Fy zox(2O9(EY$zdT@z;0p9uT;}ZIV*WYW+3^Tx*+JbaDO2~}X)H&n4)z{{I)gUAO#Yv#b5Hkld} z33}4cD9T{!vT19c!`h}y)%LG4^{O!;TsO^)5Uy68Nvjn%o$KMSOG4Pj?j;ww_6Q= ze*qbnQgLI6d)Kzo4hwa*Ft3I&rRus-V+RF@oNjvUtdY z2vUO13;-B!r4SvZ^lO0@u3>W=YOi7%=M&LPVe*QNXhM1gru?wjJ6FkaL204QEv6Rf21ALs$|J*A+=5n2zb_hB@>XNuT>or+$4wx4|9o zG?;-Yw(M~yG}!UymcH9r>$Ae+-vm{iBM9`&f>c+AS!ktF3@&?wZ?e2X+(bqprO{7I z4v7pWQ-!@J+I7g+ssb7K#8sNzO%<5KfJL{NAcv zmy$bUKMlsP5XQ%0vpM972&fzc%z0X`i1lV-mxlqG-et{C%Q4PV1 zK^wmsHi-wcB7k8U%1&2lY*1qc_2ypgz7o*Q=LjE^h@VI`pUqL`GhNf-mr}Q_CE8)( zTyvu6AzG7`If;EVh@(z;6f)Llbmvp3-%Vm-@ZQd`fxIFr_AFaz)Lned<%I~dKW(Bn zt`~#~ndYh{VuoyDD**Iu`+&H5vBR9o3sN$&EuAZWuaT^3r-lC;{Xw(I?RaqE33R&DK~W$ zUAnr14A8T>7QkWWpkl_B03AB3(Is@eWGygknqnEd-`B*GP4xM7MKfr7 zc8`F$EaJ{tLExaTs85o#a^lBF_-=qAptSrS0p+xP!e@T!SS&sHD`)Ia@?W(nN!8jNtkvG4q88SC5C2pIyCIyuqc|K)#y0{_`tX+*Ih;L@cy*aPR`u0#fO+?~Tv zm@FxxMU~RWYUKNrP+@3kD)<=3E1GD+^9Yq($~bpz!g?2yDpLR}E^gG`6fOA9Y*FJu zz~6Yu@~ukB_Z?}ARw9Tj+@&d}dg2QlH>SA_fwo$K`oHxEa;iS1>0+X^tsYONUqh$>}23ur1;lA9_R_Ukc!ZK)sl7t$Bx^DkA(M{g#@rc8b z6@+&dh4pAuGGG%N4S?F^naOf@wEn2Mu^VqfD&mauR?1Y3(!Fr6Z8SI zdiy$<8NJ9PlUasH#xFHX3*O_v+&4ey21lwR9V9jpIhgqnzrGr^QPDH#5b?(}WKyym zlRx#(w~2Ew8N9g)B6`|B;1KitqNi%Pravv?xnTc2e&IN&xSb7Co-KMton4#kYU6P? zlJ}O089+ff>*lm(wCZVl#?WaRwLX~up4Dn~EQU>rRkDFj`{HOL61I_d ziB`lzNW9FUD$^`0d@!!uT36ehY_Y}+V z_I0W1AB(O58+OK4xY8MLQN)TKzYkg|Bump1+rEM|+N3Gl2wCGeR+KYe;$hz*cY=K2 z_H|D`ESQpg341a??s29fXnW};rOcY9KN{wD`f3b&F*2Nkz*Lme99l&5j_nn*s00Ca z@m|MF0CAVg0Dl3S}mt@V#y#SN6qIHA#W@wAv^S=jpjhd z7caRe^UV3h!!51(5!b#Lt{Oohg05^wH{lyR*6O00XsRw+&0PiQ;P&41uWFVg^0@vb z03W`cWv>Ww#d%g$mfh*I*4UhFKUj)^j49pVf$?Ude33XGGLX!YNT_%sk0EWqLXsS+ z$#?&;^29m86@_NAv~qXPknh^L+i8Z*9KLYx6vlqMxz2Zc#LBaY8jLzzzL(0=aOgSs zvn=C)E4IXWvBFG*Ahp>bokGUue9F&?6m#>2?XHwg8_by2*o3L5ZWVEeZYVS!=3$jE6sVZigVz3Uc|JLjpM zbvaR2Z%V#^+Nyv;tCX3CBz>#m5H)@y8V@dNqd z`?GT|BVf3SmSpT!2#BKSkH|ktRRU5z4(=QHb$UUt_T}dpj4@s*Y=6wx*tM8Dmcxcm zT~yq68-+e%%JFtZ0olikJ3mz@75ftAyEd5x#Z zn_gwMoH~+r^kOeIJ@^8&P<)(nrtOS&5YX&`p5OU!gjQuEfGwB1kg~JophZf+6dk}! z2vh%ULbUdT_bw<(E5A!!td})xbOmdL)d(4xPJeO8PfPwwxwWD8)E$ze_|bbaB{t-N zDg_|PBFVPJ7^+R+Y^-gJEgMv$@iWP&LrmUP0XdO53ZY&9iDZt@U2hepUJx}&(6HMrcDt#}GJ#Bw)(F&WcDQ~95 zLrsO0#A53$cfBiUFU=LV5kB>~p1PsXS{TTo9mY;9bknu624NeoE84a8|R>i5Yvq%_6D7r|0ldZa)nbY%)oj>5$ z=o8C0|JL=+(+@;a(T@Z?V5#J#K0-EVQmRd8!$S`097yE+b!H%`m;g3kkStz9%*E-b zFk!h^DSYWyV5jgP4o-lrr}>qXhnl|FIfk^&tFHjhZB=?g$)})xIMCOTK?8nz)n_VgRm!g=j3~O zWS=^F=3j$Yt{cXJbZf-6fhCF2$E}8|_4eN) zui^=B9SfCz_w(yaI^q#=lb^}*=yvxt$Z`fR-bn2cQZrk~ z*c=bvl-MG!X|*zAX~pl&OC4n4L#4KObBXvCfyL9=CxXSN()DiNIZ7L%L$26}z5rJ}(y*Fe%91?sb-U2Mos%S&&~umfqq6zS3RYcjxxHElcg`4eMpi)#RY1A! zxQtA%0bl{lJV^YGu~lJKNeVP*P8eHydX>`={)X4|Klm+~nng#2IP^_R&09};lvAdI zK9$As9fm+tT~e8e+!~NebtC#jkHKdT{96ptPRh^x6KBlurFU0?ghcByNHj2Eqlif| z5A)}F>BNb}c?k;49h#q#%$YgpPs)PIUqY9b#0C8%hag@gDe&5i5G>;~7HTHFngcJIP&#gV*-F+fklbi92`T4G)^kVzlyN#uFI8qrz z{aW8@nRa`fQ#0EWUu67d+@$kQv@_!{lKRyIT@8Te$J+MI%u`tF{iqZ(ExE;C%QeFl z^xhjaDXP#jt%>bcFIhsul-_3=0u!{tt{4wxk=gBg6O4?tpC$V&dgr|KLzuVw+bbTf zG5-K37&NXsBf3M z0>Jsx!@3-S^CBk{#(?Ni#|qCKkH%gGaJ~ zSAeWX2CuK5Tyfs-NBi6jgkP%fx#Y$lppL)A6D17p6tI z+&h+)2>XyV8M+EQSf{(IYBvINEx5z6smCzjt}ND%mBv#%{xBReNZ&CbRSA<#GEj_D~%%bhIQmqJ)0Tq$8h#z2H#OmWhJ}5 z{TK?D>AF>%h14Iz`U1Cxj1qI26V9fy1BTKNJF7Y|qVm;W-6Aw;LMpHVje)2tS=Qt} zRZ@3k*?1)Kp`hY)B|XG?dh)jy9B?IVZ#&agNMVDBq7GD#s0v%6*bTxh3@BS*L27tw zt*g23lkcQqoPaC^m$eI!pCPaNC6a#yUpTDL-dR}Xj!mwtEv~J+1Uaivq{o(h10KH~ zbDFIyv$DI=Za^?u8PZ<379UIxk9~B=tOkHM6|)26+_Dt3WT9sg=+#izf(9n%yKFGg zvrGIV>`SF182O8Nr5zMBMwrB~|2mKTnwqY9Y@bpEY&Ea(Ffc6>OlntJP3l{>oi_rT z^1qXdl8W3)j3)TC2fr-BkC@?-o${sK<^_y#507;d?zyfU;IvVeK_S9>ejuBFR?(2} z>gFIU50AkL4-pQh62{ArwGHSR26%ePYI`4Y?;6lyn3(Qts;$90Ka6Gv3JRQQo@mw! z97R=5uR~QRqDZ0YM;vXkzF_a`l)j-Fzq4u{5<%XWajt?P#v5dUMQKPz^6Btm-xsZf z>kMQrVX#PtK zvHd?Q=Zeq+Vc`XUSe$}Gpkvmn{9?WB)Y*3R~S78E%a^M$w$&HW*Al}+x6ueVW7oLK?VH$5DSlfV``N;CU)1hAK7=Uxj6l1bwL z8&>7{WQL;Iy)DPJkoyX;KH)X%nGUXfYBvYQ~T*{ zvfe$AlyV=*v5wz3K-ouu=04;|IyC=!wYNlo-xi`!jbRMq5Z*1E{jztUkzXHW`eC1& zXf>2lo4MM15KVj34_jFcvn)xjk_h72=A*bts4S}a+pwagdw&iw^j8GuZ3_z>x~CXS zn&tDPJ|b9e_n&S@qkJ9|FZs{iiTuaV7a{o^x+G}FAvn;W(PRj;-vO%>f&>#MVOfQC z;61~M`wrNN$!Q&K6;V|dWXZ2YKBMsD}6#0@6- zgS|<-=+szXj5f%h){;E(Y#0M#$wnCpkH+OBT(yANzR`z-#rct`tN0XnF^m z2axs*h^R&}&BwfUAgoX(QFlrDBV##`Fj9A|#a9GUQluN1v5vvOwj(6homId%jdo9Hz=QQuNIU9LLeOP6)@}8;wI}VrgR4AqxLjsD- z?WD(fy)56M3gj%a7I_&2z9lWS*vAHC7t(olx(i1gA=f6OPMB2iqCB&}0FZs(g1=M< z;`k=g5ftRCm%R|f#DUg)M>aDgvHQYETR!?!hWI)wW! zy|-cmASarV`~4_Ff(7!BB^W_-Axgl&+K}rAn)#w@e@ZYa)k{mVfbvyM?=_nA))jtI zk(&9ql2W%gmk!tGzKssZDVFWIZPDE-J5A$vnMFm)(gSVpq(^YtxZ000BpHtDe8#_= zl?F!FXC-Y4|GBiYd|lJyEaz=+)9)!n@JCnawZ^)GMtvd;Jsm?Ol{cVjm)Ol^37;#; z#Zzf_JCUO$-*kpQUx*FhQ)^1I|(EbU3i?ex*1)`|XixX2*;oiD1=~KpE^G1892)O7iB+nG=!aM@-aTJZ}y%$HL(n$#L zS0rr*I$c&lyzh149atc}I!X0lm1`xU0H*aY{?2G6lFg>q#OQZW&`0tVYg}z%&tH#P zF$e%Kxqe850n~-iFk+VGtIKYRKK`5fbQ5LZLgNdca?Am4dGfk$zPL}ZGD4lHMhE8_ z=MFMis$i6KUZf&ye+lKyP`1vwC4D8qeh)xY+QVWX=+2TC1|Az*2j>~XU`(Y1#&y&j zWZNTTySUW@YPV6^dbt`k^~;pip+BcZ=tH~Gh2_L-=A zU@Yw}a)bT=&bVJ1dzFT2C5auihw{E8ZWy3wkB&OiDLqLFU$iCjc!L_$+}2Xc_s)-q zpxKT38g}8+CCWPpU-Sn(IT5#?sD^pNO}+AaX6MQcWoOzM7^V30u^|>Zw3A~)x&JaQ zQxJ!;v*)V2Q?6>ek*XSMvIdvdAEb?#mYZNkIKnu)ajZqm9=`xmRGE^1#Z=M-lAgSs z2}vMRC#n}eTQj7$XZNtJ>KKS*G?W0|5Q|}(>`Wr;B2s)C&_?5sSBA@IE)@ik+yh4% zCI%f^s=B8cRq4xP{fA;x<&(wiz9eH)%1S9HI8QPO)B?Y8o&gCuQFp*L+j9veL1Wv5 z$e)bm@%Rann7Dn=pJlfXwTO_IzJI`^4>8lTM;2ikG>G&^Aa_GOOO;6$ zAMe{w2|>QdnY;ao!wD)T**cX;?$pWofuIJ0Z+YNI9I_XdHIc2sN9`K$`xDi6!$HL^ z*oSMQ7_7L~pKBmA7*x2AVPsrRK&2ixJFQ zhg!#Lb=Eyy&rzoe5fxl06*Qtgq-*YiU-u!0Oyx<* z+3}hH9~!ctOC^#NO}h@?7-{2*L$7HhY)8~{Fc4run7GB2MeuL`R|k)M@Rn@4;DF>V zwZT>e&ylB;_M1Ph|K1_YmS_i)=LjB8ZsjeawRYc|BUX5gZb`?)O8_%HEJv@TfUgz! znC`>_uQfEx9oN~G#l$EKV^DE-Bp_H(a1a(F9kZ}6C~nJ;bV&xTMUc%?&s`dspN|3~ zXQ>`^bZahk3js;If(Nkh&&LAgbJ51VUBObQVf4Sd8X#`Gj-?V~8>zDaK71bcFyi+2 zFBFZd%>LD0nD4CIakjl9NHxKsXj3AEhg-n^3!Su`tW0vz&xJQ z*LtG`pj-j#fup^}pPqlg$PpHc&%gEyE`ctmVQZ*HpG?(i)fUvIa0UI_tc{d#-s6Vm z8ohGEB7M6*vd-2)s zG!V{guq?hC7#g7-`RTI_p?4ww0>j#iAx~(VHHtMWgYfd~ls(UKn?550%do4(>~J|0 z&tr(z@iTg9omX2B8dXDzwq6vc4OOlLF|h!LQ^bV-gLJ@iJup20rwn71y^yU3)!~n^i1tU~DNinp?8p z!9Ti7;2^k^Ad~lkDlBmkZ+i`@yM%nch1qaOigxN*UK4z}wi|ACaXrY+EMzcX%m_IN zA(Z_2>e!|1E!#gFo%|g$LlfdnwTTd8F&p2;<$>6vRj zKio15ZNrMZn4zuWTJ;Mdd4vQy#z|slR9BavA;ltcQC#a??>>S4!jlNp$^B{MA!Wd6 z$q`n;v@AlErM_j91_UfaAt^2}Lx4>K+nI3-c(92F`vA5XixV)A6w6DThuz3|N!f(| z3_Es<`awct`}jJ-=i~GW?%+P7yr~5=+rz5WFQ}wYd><5tD|2rwlgJ+l6rOUIAluaH zWTDd))NcEimSZwCwH_*%-HRf)!5Nbd2AZZ%`BqRwOMS)u+3&F6%2XY1{M+OuQF=5|lFP|+ezXWRU>N}G(e-u~c6&pQAerz`)EtGWwh=QTye08%2k}Q6+Hr4W9L#wN*k;fbWeKw6lK_L=nn39$ z8oSyPjbX^*a4b_R%)VaEuYm8#&Hz=b^A{LAu(|gYn+J}6aUh0R)uyAPQsCj#?KGn$ z!0vC|@frs{6GFlbFa7#m!dXH!jOP1*MLCzG|F$J`Xp{lS@O$4`Gfp~6nei}3K#uCr zU8NFJGG+bLW*|&_yWbNR_rMyf_gG%~qg@Ps@gX)`AX_LY9IGvGUr|V1bY^c|%Bnwh zx}=GT2t)H-e|8b88cTeBe>dq074o~A9cUo;aA8z9fgY9*Lbn97uJV8S3_U;1u;`@ZIVkz zixpDFNJtihy%SC5g{fO0!ua*J?SzgtM^!|02=1X^irxDdiMzb-2;47z?_dOFsh9zd zU|*`BFa~>Pm6lhmfzdA<$9#efG`!g8nd&*QQO&{k_^jcm8hn_vtQ{eHf}QB7fmyQz zQRT6~DS+~+3ZPS?Cy9*G*?JY>RJ}a)!r2c7caIGn6K~)gUffTF(xDYFP!&S5c&M0- z^0F9-%(iKbSh4Ht+DPmagLyHNf7=0J2i3MP+9&bZBJq&hTMbpA0!`prrQWxpqI9bY ziucpN*}V$6ML0K+><8`ae6Zx>W;{$915g%+Li9bS5ETL6=Hr`gH{>B>6+L0!5;K!0 zpLT(1isgfD<9CSDQgwBCx&9Cl^vHsH^BN+M<>8M)>fW#2JL5Ld{>UJgpAzSM!HfL# zmzY>%)Zr&e)4xeZ5qTa*CT7dp1D3LTZux}0Yl3)8{1e$sUqXxPiRb8~x@)VAldch} ze!ltLRMzl*5;PVR$RB4_5N&iCcNTqOkb5iB-vLx)UcD1IJSFbJ9zj7oW{nQ>PL8OY z1iCMQl_@WtsKs87KUyHfWFC8TNxhMPEAwU(1k5XUiWa@s2pJtdyrzUsMr_t)x+!Kf zLjt^1*}Wnb{2t}V4CfEorvkOcyAbIiFDqL-AC?xKlY8e_R~V+NoX`_aK=4aG;}Tup z?yC`58Pa_m>dr(BasAO9fXpM2-4Q!JT}JzwtmY6Eiq(FVUH(F&EUPIS^U?!?Kdg`? zG4=^H0OfH)ZcJ2)a@Is_nhE+Nm&r|&9ojb0^=-BHI*Lc)A{so-#L=8W#D@(US#M`M zMOeEbKFC`q!k7QnG<4|E^ssBMg9H=Keeajh+NF$m!WNWg&GnE4?3RZ=vffKD4olIC zSGDldwP33-E1T4n5E8H#G+7^gB*M$I#uO`f4<-Atdgc6O5SY;($IDf4zTX-aPSQ%A zw$Iw>$$bn#{(-HaairomzEcQ%HQEf9k1#NxiPFhlB_dbxQYXfF8zb!iT&jq!B1kaC zVhFAv{89!=Z_aA~h_9Bc5C4!b(TAE}x?tb6c9FAYUB9>T6`yC9@7Ayp?X&;$0eLt= zBPy1?*Bp)h*cdf8Zd&zUwQ<5l?V;CT_S6%8a@2#%OG*XL-sehc@?sij!_vQ7loaPT zczJLIPn!M0R)5^(6yR6;LkSGCs5VC-s>|QB)B{kE4yw&?l#R7TQV1|O6`2aFc$F>k=o zTfq(SxAg&A{xi5MNjgb!f#e!$oCK{LivFu;JM8k%FeiMoS7*f3Jg76;q!o__< zN2sOFd>Zb&G=e$Hs74;2&yd_i67rPfvSKD&MW@udUm)Y0;x8?O>$o)VLqwOjfa6*T z+|nx42%$4jH?OqnCX!gZ)?gH4G9 z#T>_Q!uT^SMa96{eV(0ZlL?6p?>qfdcqXX==G36&mxQi(HOh%58D+um`uq2LrdLc- z z#`k)|5e^Jx-qLv|tI5fEh?x|*wYjUJ1a|EN)f}Jl6AGvZVK{g-+<&atd|_-HnH5;Q zryC@L7YNizloKf`IpCnLi9}Plfg!>8#|S~%fBF!#zoYq|4a#do!lT!OI04%JHnQh^lmr;#i@j&Lb9DX8SD3-6TXe zp$QJ^3E1BcZe98Yi}hF#Ka_;x+gq~|yIW2UlZFi3c!V}AZn-ApodHb&jbLO7bZeE3 zTauJpb`1o1pY3I24ov^EczLXs{eoXol6Pd;Tw<4irX_!|!u1>3qR{v@N>r<)$*r(N zO<6pv?m4|Rsv_ONK@H_2*ha@aK*F?lmpJcoxZJwoxxWlrhK4Xuyhu~+^USQEe)ey{ zHT?Bw@v}eL8Ywb%Kz9q8(J%7DVA=E=T5STiZDVK(^T>zgY6>@9%(wI>yY%e&`Ul~s2c|@j)4{7iLtdZDBs@avNb1&3;K;X{C~65Ubi@i+`0K5m2XD8= zuVHNq?fQ;->}3e?ZVfGrds1K5s!+a1R}rS_Om?G_DN!(TkgePBS6@|>>%s(g(D#Ad zPG5(u(=f&^*?f;^ifj}KTdy1G=<_m?RCrV1>#m}v?BYmVV7>mP4G77@W6gVrvD_mY zY5|H{4S*0I)X65{y$4u_a(JPb-W1Lb!-#;tzyhVr@62lBY8(27H zOB6ylWN!C1#PxA{;bII;;4_Ux^MDTOX5=`D20m}*zi^sYpT?Em)!XE3wO+u@1L|a3 zzz3%$rB^gWY_xZ!lUa;UEDSC+UU&i+Ud$3?W||azyrM!vy#1n-EI!M%YT*ZTScg~X z#kYY9BD%)uR%}{dWth@;4S}r{v7j&s^{rIy4iUi5gqMkCRs=WB61t!g&u&h@sYhW3 z!70}269ROm&a&ssz0A`r02NAeYL#B_{pv^aA$_fwE`_v)GKBms=KFGwdB-z@1os&B zuU#4bW$4Kero}DHWBBbE2a`G%CgtcX_;tYVQ}-h!(Zc|LE7qS)Emdb!TyjIsA1*eD z&{^l%K1KrKQ|43l;w3w>&;QWH^vH|D?SGxy$o3C^Rfsd5i+*~dMAgp8Vu}_{w-RD% z)%7E^00wTJsho19ku#a8uwX!2F zN7BH?dPdg2Y2?svT+N_c?y=X4?U%8|_IiL}XvsGzmkp{Nc6B74Un$gEdVlb+Yu=I! zK68EDzuql-f`fURtAb10Af<}}PXua@Wk40g%FF zn$bn>pCm&>Z+r_2p=d3;xh>2VjF8%G_i^Y|OGIR48%$@RSwge-wbyxOPyz+R-4xK| z5euAGSw{Wl_|p)NI?-x18Kd`kA5o;mUV4=A2Lad4T2O6#-@WW(S3}*7W(smCgqU@E zOWL3%pYq7eA}mWbf2XT>{?0*(NI!24+b3l;5YGQf844ur=KWz^n%=PUYw)6@ zi_KVCs6yiZ4Xhdv`N=N6u|s2@U04F4sE#&(kgi4bVkzH{5fnh-VYL89j_iI=AHLq# zz7z@6Tq%O}er7&XIYL)ClIH35PrfiZL(>02bNqp$_QAgwPdhE^21K;v*5%6t8azdl z=vo#i@+S+MbLSD3smRe_YFZZSaya)i2KG_v8;Q{oc*tb=$m2bJnvO%8vf6(&tMM=% z^a=?Z3&&O+H(hKxgaI~q>Ht690X`#z?Ua5onNBt-d3ndt2?rAiv;p4MpHAL<>hJaM5emY|5 z{_;W(-z1yAri=19F}S1&&21FHAtYRXqO+ooK0?O68!=V&4wa$+o~fEj%>%nHhcJ6@ z$m%+`Onhf<2?PZkaP=Qav_d(Q!_3SQt#Ob>fcxpk#|5qVwV=PQ_DAeVfAoH8EXyeX zlbl9s%vY#(;?nWyqyg2^!Uh5_ag#DT&=YVhM6_zXnCmRfAaInQ6wJNvH*^s!kNOr& zZSq+k*5nc!4-PKJktf-=ZqaBqD5o}cAg{ZWo0Laj8lkyOgQTfexjz0ur0nm8h*S6} zThr?Y4ynvF+Gcqta`shlI^+b&}o%@r!epNfwqNb_KtUx_r z9dz=MWPXD|oH=|XST7Hr$wQ=Dq@d5QZ)WYr#29s>FX*smZEN7#G5gE}z=G4OoCvzG z7n2Mb&638NosW8vFv?C>ILz9b0F64Km{X;0fE;q1xCL6fOL*!3 zCi;7GaTxb_f6{J7&}e&*^SsRm>%QWlc>3+$qC@02+$uKlL3ne1^aa*kaX zw^6f{n%MP(0Lu(!*EH|z(tt8NBoC5(hxxyX=62BQJJ+PNG9wU<{x_3Y`dxM%zGYD! z(kc|(IFtE!Ca=c}&A|#@Fkg(lqyX7BIlkk(ypKSIB7lo*G6sYONR|hJt!wR>qH+%{C z40&=hLd&fvN`|4M4b5`?r%u1*zD|Rg(N9`)(y46%Quj`fi{RFy67#K+!6z&Fnb8Py zx-u_T@VmLaPs0{ilB z8wxhQzu*wPf#!VD&NNLNivh3Xad%Lr$@rAfT9v7S08}F|PFmH=UZZ=*CumhfpsJ2P zc*2TevQl5yJqPWH?R6y*Nu0MhDc9m$OqA^JRN5`8Md~Y$dVmpnU(^VP2XgAJk`#qa zk2vj1GYCN7gn&f5ekUl^W{RE(olApz zGo@;km~fpc9z|Bq%fO9K>btC&{%h#}P~=xU&H2rEmVyd8l?@+AO5LNdMVM%!aZ1#) zo8@H7;Y*P)DD%h=)r|mVJkHHkGuyW<+s$J~#W7DBk@T+fUTWHJI?=zOAkZt494 zbHJAI5-pApQ31Q|-E6y1q;i=RX7+QU^M4STlYY7pl*PeYV)J$hz%Pq|CAi`UF-D$a zO-FR6T-9;K#D6(jy6$;!l!qiLkO@BID5)zW-_lo*%u52bKC8s9{GXC8=y;Z#0@YsB7K8c!I;WmKjk|#V z<_57H4ba~#GsA$;%HrHryajMMx8X;`z$Hs{td?Z#--;@tjt}i)wDT64{pR&(YZ9fA z$W0HQ5M!jGi-Tu6(tj4Y>`z~%!o2{=Z1HgZg{yyl0#dsn)`xNNX&P^w;hna%2qFr5 zSX?8K5A(RXJSOsEH~F;h?eL@bk$N(znH}KbSg{IE^>K?Lod|ZI;+fi{J^(g-sGLda z-E@Q2=b=wszL8Q8Sd~<;Z2~hXZ=g$&YSm}|05aB%O{B$)kqo+f91mbNX8d;{ek}PS0Y0{pUd_J@y}P} zs;SxE6#ZSYsX}`vzTw1@BW?wPC8`HmI;O#uzRhCrmD;%|#7)01NXF&Qd}5ed{FTE1 z4>Qj4nE9<6TAO?2f0ud;=nIc=h-*RWQ)V}w|gu^lK zqmh+C#?)48dw@=O^4j0TU5iqS7Bao&M^=C#W~yLiw)i1jH-@!J;)WLk4C4Pbp+vFL zig}?KwnXt{Z=UUzSPAw8k%)!-YY;5J!V1T~p*7{$1_Ub7)EndgA z(!IlDtMJ0V2H;$%T-)QRMR}pj5=Vv&>OEa|@3qJ0NyS-oPF*A=8UoBd*8w&WILGuc zglp2KV^rTqC4p(&B4%zgmn}B;Kw95?S$whAh^ZUdbmBxQR$7a3TIG^zM~nPy*Mp;N zE&~}tZ!E+<7q6a+RNZ+8{*0#|bn0ylt6)fUAB7BqD4Bo%#@K?c1t0`|7_e4&@ehxZ z(A1M78xtFKbZ0Yo-w!CLU++ECw9&+XHRau7fhweYid4M;ULNcX-M?ef?Fc?>JV8r5 zycyk`Yp!|`HQMfbrvgoXRI(LrmCu^!(A7Od$O39e(0i62xQjPHqzMQOF(S1`BYtYW zrSUhYfEoHqNBxObSY3E+v@_vE5EyZH;Offhq)D)xT!IwoM9WVCQLYmNCmM1h4Xx`jwHy#tMX?mCEY zvKOW@xg0+cc2^?uFBS_=Q{gr@r^GEsI*6LDor%SLZX&MI#5W?$;=%fYb%e^ky6z{S zwN{6N;%qoRWOrD<^?r4ArxSVr8qXU`AKN*PIaR8)97mx$jl_E%a%W7h023s`}M^wTHy52HKJGJl^#~B4PbA*5{mb+Ia>Ac z2cURz)(~L3D~xw{CGi`gpIsa@QSxx)l%%Hp7ZqnfEjr=R<<$D%6LkjidlSr*7u(No zS73TeGhtiMT|NTl48O*t=D>QoN`v9fVcmp;9|>SnCptLVifW&@<~AG zN*YZ#2_367*Emm!V6b)59|O<1SN$I6exV7~F~>`q!qZhKVZ=&inGe#NTNL8)L0)Uj6^-)m0&NB4SLZ9Da?Dh2^_Z zX*BfEn+sKSt82M5bc6v(^-u(qFT1l%$PCj9VUjUaI| zMlMDN*&%;24UH#8vlepLS~HAYD|aCF`WWPo+qWvos&p6j!s%<(PoLfh7BB(OkNE%C z4GYFt{O)45oqjXK@J)hSCU)mMPi>U#&T7rZ@wBPDFQ2@z3KY3iaDDFLi-!_nHFl6M zUuPl)i5|wWmP43RS4QQ)QFZksheK{?)-f(b2HUt=gzxeuLvdj%7pT~ {% for task in tasks %} -
+
{{ task.filename }} - {{ task.status | upper }} + {{ task.status | upper }}
diff --git a/tests/e2e/download_flow.spec.ts b/tests/e2e/download_flow.spec.ts new file mode 100644 index 0000000..113261f --- /dev/null +++ b/tests/e2e/download_flow.spec.ts @@ -0,0 +1,319 @@ +import { test, expect, Page } from '@playwright/test'; +import { switchTab, waitForHtmx, collectJsErrors } from './helpers'; + +/** + * Download Flow E2E Tests + * + * These tests cover the complete user journey for discovering and downloading + * anime/series content, including mocked provider flows and real file downloads. + */ + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +async function getAuthToken(page: Page): Promise { + return page.evaluate(() => localStorage.getItem('auth_token')); +} + +async function createDownloadViaApi(page: Page, url: string): Promise { + const token = await getAuthToken(page); + if (!token) throw new Error('No auth token found'); + + const response = await page.request.post(`/api/anime/download?url=${encodeURIComponent(url)}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + expect(response.status()).toBeLessThan(400); + const body = await response.json(); + return body.task_id as string; +} + +async function deleteDownloadViaApi(page: Page, taskId: string): Promise { + const token = await getAuthToken(page); + if (!token) return; + + await page.request.delete(`/api/downloads/${taskId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); +} + +// --------------------------------------------------------------------------- +// Test: Episode picker + download toast (fully mocked) +// --------------------------------------------------------------------------- + +test.describe('Download Flow E2E', () => { + test('should choose episodes from search result and trigger download toast', async ({ page }) => { + const jsErrors = collectJsErrors(page); + + // 1. Mock search results with a full card including dropdown + await page.route('/api/anime/search?**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: ` +
+
+ + Frieren + +
+
+

Frieren: Beyond Journey's End

+
+
+
+ +
+ + +
+
+
+
+
+
+ `, + }); + }); + + // 2. Mock episode list pointing to local static file for real download + const testFileUrl = 'http://localhost:3000/static/test_download/test_episode_01.mp4'; + await page.route('/api/anime/episodes?**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: ` +
+
+
+

Frieren

+ 1 épisodes disponibles +
+
+
+
+
+
EP 1
+
Le départ
+
+ + +
+
+
+
+ `, + }); + }); + + await page.goto('/web'); + await switchTab(page, 'Anime'); + await page.locator('#tab-anime').waitFor({ state: 'visible', timeout: 5000 }); + + // Trigger search + await page.fill('#animeSearchInput', 'Frieren'); + await page.click('#tab-anime button[type="submit"]'); + + // Wait for search results + await page.locator('#animeSearchResults .sr-card').first().waitFor({ state: 'visible', timeout: 10000 }); + await expect(page.locator('#animeSearchResults')).toContainText("Frieren: Beyond Journey's End"); + + // Open dropdown + await page.locator('#animeSearchResults .sr-card').first().locator('.sr-btn-dl').click(); + await page.locator('.sr-dropdown-item:has-text("Choisir des episodes")').waitFor({ state: 'visible', timeout: 5000 }); + + // Click "Choisir des épisodes" + await page.locator('.sr-dropdown-item:has-text("Choisir des episodes")').click(); + + // Wait for episode list + await page.locator('#player-container .episode-item').first().waitFor({ state: 'visible', timeout: 10000 }); + await expect(page.locator('#player-container')).toContainText('EP 1'); + + // Click download on first episode and wait for the real server response + const [response] = await Promise.all([ + page.waitForResponse( + (resp) => resp.url().includes('/api/anime/download') && resp.request().method() === 'POST' + ), + page.locator('#player-container .episode-item').first() + .locator('button[title="Télécharger cet épisode"]').click(), + ]); + + expect(response.status()).toBeLessThan(400); + + // Wait for toast triggered by HX-Trigger header + await page.locator('#toast-container .toast-success') + .filter({ hasText: /Téléchargement lancé/i }) + .waitFor({ state: 'visible', timeout: 8000 }); + + // Cleanup the created download task via API + const body = await response.json(); + if (body.task_id) { + await deleteDownloadViaApi(page, body.task_id as string); + } + + expect(jsErrors).toHaveLength(0); + }); + + // --------------------------------------------------------------------------- + // Test: Real file download via static fixture + // --------------------------------------------------------------------------- + + test('should download a real file and show it in downloads list', async ({ page }) => { + test.setTimeout(60000); + const jsErrors = collectJsErrors(page); + + // Navigate first so localStorage is available on the correct origin + await page.goto('/web'); + + // Use the static test file served by the app itself + const testFileUrl = 'http://localhost:3000/static/test_download/test_episode_01.mp4'; + + // 1. Create download via API + const taskId = await createDownloadViaApi(page, testFileUrl); + + // 2. Navigate to downloads tab + await switchTab(page, 'Téléchargements'); + await page.locator('#tab-downloads').waitFor({ state: 'visible', timeout: 5000 }); + + // 3. Wait for the task to appear in the list + await page.locator('#downloads-container-inner .download-item').first().waitFor({ state: 'visible', timeout: 10000 }); + + // 4. Wait for completion (poll until status is completed) + await expect(page.locator('#downloads-container-inner .download-item.status-completed')).toBeVisible({ timeout: 30000 }); + + // 5. Verify progress is 100% + const progressText = await page.locator('#downloads-container-inner .download-item.status-completed .download-meta span').first().textContent(); + expect(progressText).toContain('100'); + + // 6. Verify filename is shown + await expect(page.locator('#downloads-container-inner .download-item .download-name')).toContainText('test_episode_01.mp4'); + + // 7. Verify completed actions are present (stream + download links) + await expect(page.locator('#downloads-container-inner .download-item.status-completed a[title="Streamer"]')).toBeVisible(); + await expect(page.locator('#downloads-container-inner .download-item.status-completed a[download]')).toBeVisible(); + + // Cleanup + await deleteDownloadViaApi(page, taskId); + + expect(jsErrors).toHaveLength(0); + }); + + // --------------------------------------------------------------------------- + // Test: Click a new release on homepage + // --------------------------------------------------------------------------- + + test('should click a new release and switch to anime search', async ({ page }) => { + const jsErrors = collectJsErrors(page); + + // Mock releases with a single anime card + await page.route('/api/releases/latest', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: ` +
+
+ Spy x Family +
+
+ Anime-Sama + Spy x Family +
+
+ `, + }); + }); + + // Mock empty recommendations so they don't interfere + await page.route('/api/recommendations', async (route) => { + await route.fulfill({ status: 200, contentType: 'text/html', body: '

' }); + }); + + await page.goto('/web'); + + // Wait for releases to load + await page.locator('#releasesList .hc').first().waitFor({ state: 'visible', timeout: 10000 }); + await expect(page.locator('#releasesList .hc-title')).toContainText('Spy x Family'); + + // Click the release card + await page.locator('#releasesList .hc').first().click(); + + // Should switch to anime tab and populate search input + await page.locator('#tab-anime').waitFor({ state: 'visible', timeout: 5000 }); + await expect(page.locator('#animeSearchInput')).toHaveValue('Spy x Family'); + + expect(jsErrors).toHaveLength(0); + }); + + // --------------------------------------------------------------------------- + // Test: Series search flow + // --------------------------------------------------------------------------- + + test('should search for series and display results', async ({ page }) => { + const jsErrors = collectJsErrors(page); + + await page.route('/api/series/search?**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'text/html', + body: ` +
+
+
+
+

Breaking Bad

+
+

A high school chemistry teacher turned methamphetamine producer.

+
+
+
+
+
+

Better Call Saul

+
+

The trials and tribulations of criminal lawyer Jimmy McGill.

+
+
+
+ `, + }); + }); + + await page.goto('/web'); + await switchTab(page, 'Série'); + await page.locator('#tab-series').waitFor({ state: 'visible', timeout: 5000 }); + + await page.fill('#seriesSearchInput', 'Breaking'); + await page.click('#tab-series button[type="submit"]'); + + await page.locator('#seriesSearchResults .sr-card').first().waitFor({ state: 'visible', timeout: 10000 }); + await expect(page.locator('#seriesSearchResults')).toContainText('Breaking Bad'); + await expect(page.locator('#seriesSearchResults')).toContainText('Better Call Saul'); + + expect(jsErrors).toHaveLength(0); + }); +});