From d202fe699d9444f1d8029505874451dd500e7aec Mon Sep 17 00:00:00 2001 From: Sandip Date: Sun, 21 Jul 2024 02:09:15 +0530 Subject: [PATCH] feat: revamp puzzle page ui, remove sudoku page --- assets/icons/heart.png | Bin 0 -> 17200 bytes lib/app/view/app.dart | 17 +- lib/assets/assets.dart | 3 + lib/home/bloc/home_bloc.dart | 9 +- lib/home/bloc/home_state.dart | 5 - lib/home/view/home_page.dart | 8 +- lib/l10n/arb/app_en.arb | 25 +- lib/main_development.dart | 11 +- lib/main_production.dart | 11 +- lib/main_staging.dart | 11 +- lib/puzzle/puzzle.dart | 2 + lib/puzzle/view/puzzle_page.dart | 338 +++++++++++++++ lib/puzzle/view/view.dart | 1 + lib/puzzle/widgets/mistakes_count_view.dart | 66 +++ lib/puzzle/widgets/widgets.dart | 1 + lib/sudoku/bloc/sudoku_bloc.dart | 65 --- lib/sudoku/bloc/sudoku_event.dart | 21 - lib/sudoku/bloc/sudoku_state.dart | 53 --- lib/sudoku/sudoku.dart | 2 - lib/sudoku/view/sudoku_page.dart | 161 -------- lib/sudoku/view/view.dart | 1 - lib/sudoku/widgets/sudoku_block.dart | 21 +- lib/sudoku/widgets/sudoku_board.dart | 33 +- lib/sudoku/widgets/sudoku_input.dart | 48 ++- test/app/view/app_test.dart | 10 +- test/helpers/mocks.dart | 11 +- test/helpers/pump_app.dart | 14 +- test/helpers/sudoku_helpers.dart | 433 ++++++++++++++++++++ test/home/bloc/home_bloc_test.dart | 31 +- test/home/bloc/home_state_test.dart | 8 - test/home/{ => view}/home_page_test.dart | 23 +- test/puzzle/view/puzzle_page_test.dart | 314 ++++++++++++++ test/sudoku/bloc/sudoku_bloc_test.dart | 261 ------------ test/sudoku/bloc/sudoku_event_test.dart | 47 --- test/sudoku/bloc/sudoku_state_test.dart | 118 ------ test/sudoku/view/sudoku_page_test.dart | 141 ------- test/sudoku/widgets/sudoku_block_test.dart | 33 +- test/sudoku/widgets/sudoku_input_test.dart | 28 +- 38 files changed, 1412 insertions(+), 973 deletions(-) create mode 100644 assets/icons/heart.png create mode 100644 lib/puzzle/view/puzzle_page.dart create mode 100644 lib/puzzle/view/view.dart create mode 100644 lib/puzzle/widgets/mistakes_count_view.dart create mode 100644 lib/puzzle/widgets/widgets.dart delete mode 100644 lib/sudoku/bloc/sudoku_bloc.dart delete mode 100644 lib/sudoku/bloc/sudoku_event.dart delete mode 100644 lib/sudoku/bloc/sudoku_state.dart delete mode 100644 lib/sudoku/view/sudoku_page.dart delete mode 100644 lib/sudoku/view/view.dart rename test/home/{ => view}/home_page_test.dart (94%) create mode 100644 test/puzzle/view/puzzle_page_test.dart delete mode 100644 test/sudoku/bloc/sudoku_bloc_test.dart delete mode 100644 test/sudoku/bloc/sudoku_event_test.dart delete mode 100644 test/sudoku/bloc/sudoku_state_test.dart delete mode 100644 test/sudoku/view/sudoku_page_test.dart diff --git a/assets/icons/heart.png b/assets/icons/heart.png new file mode 100644 index 0000000000000000000000000000000000000000..07dad0f038351c914d2ee52e467de2109f304f60 GIT binary patch literal 17200 zcmd^ni9gg|)c+cF)=`l)Aw;M~O0t9yB1;sqk0r|>j6FrjPWJ5Cw~;Jah89`I zGWN3XyP4#j-)zD4X@wuDFGZ$;f!^1=T#Vb2k%V$p3 z;*Ksh$*YQ-5X1-FN8Zx&Oj#NCN_lGSdwqS$BYjJ;@;N;}ja9%KRcdx^X_a?jU7F8t zsX4zGe`4~)IHBfoaq)}E$3>1s4#iJ0|4{!FZoT*(dY{z$7N&A^qn5^Vhh#eivWmbRVlE#RbG=SF;7By*fuU=Ksn^$mJUfQm3OJmz`AHh%?hR z^VfS^A{xRTCiM7A!FSa=ZMC|7tv|9ub4p9OEH0{FWraB7s%wTawG&M#pGQbmk|)j| z5t$j2Xw9kh1S7hvD1`m#f75McZ$WegUAMBm?**JN$deEHW*r8?lYcv3uR|m|eZPda zZl_i*Bi+kl_*-0q6N*=NjqEaEOv=WZK)NuEH(liV+!9z%JwIvMEz1YcA)xCLxXO(c6JT3wLJWkj*HL zGUJT76#60cu?8}28=2O-I!U5k3lvg!Yo`uMKelRKIHF-O6vF0E_r`adE04Kw87e~f z%tOUUug7}+t7OWqQ3nN|pEluLu2*0v_*A#+mpHq3c1o`|`&WgV&=IadmG%pq!CdHc zxaavoWf!!X)yk;ZD4puR%;I9#o0DdRh=j03Ylb>zY(d^?ww)}}FRL)-)@;M8%yr*? z%ZgSC%)vnve62v(Vp<>FSbRv&VIQ)t5gMvpDXeJFJkZVPm!+Rg8+uTD>U0i{>B}q; zr7)*+=plHbqx>UnMV*tna33nfOJ)ag+VgG1xGIM*IiVP@Ljofn}u7|$sWRKL1 z;w$7~MUxNw`BT@tctL-M6(b5K4C9z3`$e2e3;jeIP%H1N+qB(YJm~4^MmVAuFHqEC zQEB<|hT_Uqg6U>+R+VOE10x4^crROfL&!oMk6&uMJ>`CNo?F}D_Kh{oyBYyDnTKKq zJ9ZVX#eSR2ej{4wz!YJzukG~Kl=5m4V#X}MHarg( z6Dz%jRe;7Q%>!2yCFk$#+@fgy`xIp)YbL9a{>aYmeLhhV%?fegy{4@^GSOr?KQ~&$ zF2shOTc4vaFl+kg|6v-4ozm@K)7xw|wYlpxg=f-=;@RyK%yGk&c%m+kr+LfDRc0=S zS3C<5$4cnED^9U1iq6%Q+Sts=cnCs6$tIS>ak2R3Yo(=Y=p0*V4N-_OcV*@M$)kTU zgL{+ou~PO6_X8{oy+fDk)8qAvP{VY02D5t-w8Ru}p_Hrpop{`Pa%%I}N-^1Uw`ykP z;DqFot_9rEzXR3U@3ysu5aOO=)6VCLHGgeAN2&o^1ug11=GtHSCy_uM#!>8?Hy!Mf zAMZBOM|2%wl^^IY6j?kF4-Hh_|J(vKGa9YQ1Wltf}jLi={g9dZAekQs@{t zZtgu7%gn!5YO1Sm@9@G>>=Yhwa_=bG_soSH&0VtWHGL%PyM5f;+;nR`OV<)s_UZS} z%_n5XQU-|5pY%Qitx9h07(Cr=^mWxw>izqhb~5v(NrOea z)!Y{!hC4VU+-N4rP_T~TZcF4s)pIU#^IignUhsW{i)qtS$BXfns zDX*rcJp*NqRA3(}qrYblXF5s1H=H*aPxqugexyJ7jVv@!$JXxX=FHLL^sB^;w|l(Rur??EE>(1Yx0<*jV!-o- z`x5R;Qxi^vl|+H%9l_nst`tb`oNBG1;bCRm;EMHsa@Bdd(|WEuR!lVGmjx^z;o#Ws z-9EhlYwSm{R%iv79jd&3sMgtUZxbfqVAoK#e6;7>tvunQZ_i_qhn;dlHqWqc)gO$H zHxx08?HEFZNMJpp{adRPWnC!N@VIOSucC(MeVq7P)1Q8idZ*@3(C~Pro(T>}g{xKa z5LA&U%vuZaYbej*Q}=f{$>rR9XVv@qdKCL#!?}+C<#1-y7ycft|B8BByZ*%3k!_F) z+o~-2GP3oHJxjE#PAbfj$9r|uZt~_Xy#Nl1j2-&9sY5~J5!IDu9xT1_E3d_1wbs(A z;nSx#*JbJ^-kddq)_K4Vvs;?dJH3-w*=$rc{xf7yMXzyz6|M7g7u5*_#o{xeB~ba} z`*VD)+{FSy?zkY+oCB8nSN@HdRM9^? z%Zg_Fxx4%QY32Ydw#dOIbN>;6W5~G#_^#?voW=VIw_k3`@Bcj_oCky}X3I+u+#R_^GcDa^^gRJp%0s?tT%0Bb^ew(9s6e>OczS>d~W8tAVa5h-;{%@t^Z zi=LEHrgdSKB|AiwOB<5&+~}BSuvb~0JP}8sUqQbdY&fblDDw+oCTvbzD&*^m8G7*W z_HOzLhX%{uYer3nM)+G_KlSx(-HeTba22V+%UwrG*Lf8l5B4(?t|CVzmjC@LT%;0Y z29wfq%~vvFrSV3Z_sR21sV-}vBQH?>9@M8K{<^{%&=#6`a)QmgGd26E)!y01zu>d+ zUgHYivjJnr)ETb{0q8j%cN?`L##&z5EsH=ra?&&FZ+jEc0l!M#H1~lbZDt#m_(Gv4 zqa!LvBb8bs@#;3(|3)2`+Y!oKS!?QxRppG_1FOM8HhZeG?|?%xV)ZgaZ#mfTA2DNT zI-~V>bRg*7oJMptjrZ#zgPkCz5DJ1cW$u+DsJ9|@PSu1h@p|#KbpuP+D5jmgJF<=? ziozM%1zn{kmLIwc^%d-G&IVT3r0a;Y9@SP!qzYlwU=W`Qj`4+3X*mfe4a)$uJ3_ai-cvbWu=7*UYsrx?dqEr}2aT6xcqSBrBm zzwLiKxS`!d8j>A%d&}SPS6sCOhW3uvD0(Z5m_O?vDIA=%2+k@>~=J0kZ|B>I!o2hzV${|{9mB#Ug zHFBq@LB1r~*)doCg*0QYn8|DM5v4Ei2&`E`a`NO$g-+XeW8=wBi1aV1LDMsl_N)ll zL!W}QB?Zk+-IW#nr}Erpq7d2fpFdhEI}VTUXrw+#T1}V!{xXv7h5hGc2^AD(%*dqq zIKJ94Qm?#tii~q(AQWQ%J=Zb4IP4drQ_rfS%XS%83YN*%GMMuP8!k>hbWL-JKl3h^&z@4lNz&jWJVm68T#a{Tlkw502YnV{g7tzP&s(;x^ii9=5&g}OD~>>Vx<%b zEmck9<=k^}r<5WlRZ^n6a}5(Yzea_Y9v+=awt=ign>?okvVDET0Y9ckJF)2v4&4#2 z<*WAidESXBkCDM>kuAdil|qN}DW&9CJHW_nfh6V3?fnT|N~OKz3$JP~uG6S$6jGy0 z44(l-7JuVgKUwP>-}X+u~AUV%$c(gseBQ~sr$9=MWMupp2@?(C|Szp zbw2e+`p_{iX+H)w#;~X=_^?oMD~51%!fYo~vN*J}i!Z*J`~&aUUyB`reOgSu94ul- z+Y!2c=BGhZhKabpFj)NF%e$8x98!x4r-<>3;G>gDn-5|Q!uvF;cZ8|Y@ackyvwV&39Xeu4mlx9C zwDO`UogT|ZrRMuJ4vcXfR`ysJ<8u(37brA%-rzF)c@m{h$Ub5Q3gQ=xJb> zN91hG@YAqfe7;Tvfvi)%gRxGkw+0RlZk9fzo-?oc#d1bx-%cPmA8iZqZBUpKQvdab zpjtcbb{!vl=RAgGXqTm;HCL!x3~5(nE|b)hy?N29&rD;^BDr z&GtZFM{HYibFb|SI=AxDsC>GzUU2pLnV<9XNy3|Zx6DFquRiO8=ND#oj4|e%Tn~JU zja~HU?(3_8+0xBfbb9PRm@e{5>x47-GARf&Jw4f!)yc@7on}X&(dc;$pEu;(&|Y(h?+Evb%6Ts^bj(=F}u3Pz=5O(@;Kr3e=mJ`ei%MH zV`s?+QOi9M_La;}l1V;q0EHJ9Cy;sC5!ea~3hcMvyM@vls6*}F>-;j;Ctms{nu!w2 z^TqT3EHFmJ3!LxnQ0Y9tiljH~juJZK-(D(#*a!Rh|B}RGyIEWKi|1B0*zC&{0hSQGO>lIq|O5Rey;J{}#aZ zjrZNnBsR;Fw-qzTm(Y@IXR}@wxpGF#k8C8V(2TY9Y@V1L4lL|e&zc%&|MQQ%-8Em zv&q*eo*(4=Ji6Uvt8!ikLS7uUA$6$CMO^+k%e2BLFx_;4;a_G{pFWL`HZW4^|RIpxA;CaRbOG&^&#AB zJF#dfS=E9x&RwO~8DwX?5Cg*<4iZlsX?L<<7+)B}p!uXuNfBA@Lz?aLI@t;yqtPn* zxd`3?yd$qmiJoK2&1{t{EtTxq7hlQ*BT5eg1|*(Xmy1KAe^C5-W*2598#fb%OqiCoBrQxK#YumOz_E0% z>2KZBEzCbw@D6Gj8ToWg?Ar1D#49K+#@|MX6xxJ4nPy+YEsUYl0s?v6Ld(6m5&w@| z@?YPR-f7G(22@_7)VF{2^o+RF( zU#s#QN4q?eea~ZM`ZeiF78d_>lV0`dVL!&=^7083%{1vB8c!3WvrLV}Mn5m5UxqUY z=Gy&Aw_qGDZ$nX253zvVnA&K$@}=q|t?=n{DFz*xVPPn3u}aI#-*-9fLT3r&iEw}x z0}p-Vw|X;_ZoG5bdcJy)e%pGI6g1=Spt*0CCFiR8R_1X?Riv(X%Xmj{CjvzQsW{;! z{tUvRuG~0rn^a2MPk01xG=k}=%~4DP*_w11A%mDlVb;vIgxk7l%%FGj~4&1AC%k zaEp}-txxQbKt?NX2lU=-6QyVIZ@!P~SXUD{%6GogNF__RLWT^2fIJyh?5R~g6W5FG zIsGCr2|iX=;ana!P9o-gu_GIJsX(qHGsw<#xPfiSsUO1ys|j8gNCTA_a)#knns*-> zNEVFs))6br%rLl#5SF6al|lbK)b&IdDy%0AmHMuQZ<=Z(HvnHFG9hG!sZ$akk^It~8Z7EF0HPR*t8T!E5NCquM} z`b>t4%G;`l7;RB%^ivO0GQCpj#jQ){JV-~oum;qK<}D<%TrvgLplLg4sU+D(414BV z7Ub#fUJ7Fvn?IiEq}u%v`?lwntV2a=AV!FpBSNa48nbaiKou2d88SoA5Y_H?8JX`B zhczZ8ihFy$`1=^l?bz~z%Mip{+cDa%#wSie2*E1VW%8HBRBN&vE}4;ixZci(sJLbB z#j3K?9rf^G?4*t0CWT6ZL^>5=3@ff}#yhNr&rQA#3#G?L0|M(K`-0KfS`I{M1*>@? z!VL0>O%6Ktx&N0I{b~fhy>~juD%kFzPh4{TMiDH8A55kic%u5b_}uo4!$7J+-E=zU zQD3Z%gJ;C6$s16jV2pfBy+7c8md9!A$6qQ?FvjXv+dz6dEd8=Fs)uw^ zKPjM^v1`28IINQLiy(i0bHpp-X;|9W`jZOxelR(Qo*ZspI zkqDxmgCIW`g@Qro$gaU^sT9pmG!WX{K<6$&_MZMSzOywQ^OwAu7d#hqNm`-JWBO-o z&gfO;qOH(0-XJ1EJXk^^w~a5u!PfZJojxDofJ~bkpXy7LrwVkt;jjNa)ypu9riK4L z6?5VaNYrs(QfKrK#!xq+RAoSldCnz+m-nLY6oq*w9bOWH`78Zxue2<6>U2otz4aE` zb_+s6{*C@1r6bWY;Oj3Uk+Cen9B5dq7!+$tmVs?=HA9M2Le5?n_|i+cmf!c|HE$!C zUtFgryAcbLLL0gGE*u2a^2HT#$)E4G?pP+=`YYkr3OKW6&yqYQYrbbSRFTsR+0xSW z0RJlDK}@qzpk`!@Lg(W=K^pJ_72S%gfc}cr^un(y#YJ%6VD7fQo1=|ZI*~5(RWLYY zVvJJJYqk{TyPSM;@_z@%naZa8^M|_TvJTkI*Cx6N{>zD{l$Xz_{ms3vC-Eiq+0|H+ zljnL8XSPT;#+xquWRKfb(ToR7fjwlM&Kudrhugmp0zCP0#a2X-994 z-HeWiL|U@utbIaUn>-!mLJ46T2TBux#W;raCNWLO#PAd^&+xX(oIxTbS##8&=0*8x zDcZ{)r66=_)BC;Ug5_e~F9K>A{WlG~O4Eas8FdEZ1@s_DBYL_B9h(gMub2aK{{vs1 zI42e_ECm}J;~&@)>D*nN=!-u=B4HwG%g!$yd`~o>{!yHBRu>C8OHzs(&%e7C$|Da? zlA!@1fqmt}yF%<#+W?W+eY*{F5^@otUWn;~GUEEvDOl0Jq&j-bQelv`us$m#@E@RR zkaRV3^8;GGPnW5`Xj#N*nSGmZi?IHT?huJ&jYB+!2y(Fsta2_OTs=p=9BKgP`Z}h1 zR~zfg8P%*#Tzu~fj$ii|(QdX|C~C2R#WM15!mk#BdXJpZHZ^*&_sltxMDCNiHbF$z zy@*j-wBY%ukrgK7su-jk*m4hvWadqK`ROScITS{qRhgwm=cm0;AY)`~H{iGUMIuAU zR;!jkDl#r>>gO4Ce1H<=M^|!BS^W@!5UjGZVA9$M3z&BD5kBSiHZ=r_n9%9Fty}hL zo@ZIY#)etz;Czq`gu&SAF{?;g&_GmW5Ur}dz+MCriQLd$_q8RbLF>#3p!uE8fmfp* zi3k27K^X{9s13^jArPGLmO5fkq0EKkkBF)R9vU#_QXryU3WBcU-34UYxC`JJXsFDO zoe!+5h1){ZkeSD+7DD%Ou&Qemng~RAW42;@s{lp!sO37zoY^FuOx_E&g7G1;h9kUT3@TKA>QpKJ zd*z9d%+FF142?l99w;;P!5Iql-vNO-gsT7Gz3#*3a_&+%1zHM1BBkwh(uq`1K2JFi zU91QM=c-5zz+k2`{~ihL$0nQlpJkM{RcQN;Fo2l<*8KNqN}Fqy2xA8YKz74Z>D{9y znA4__$k0H1LBglYpZjNCKrW&5np6n{s=7OZ12q5e;JfTWf(YQNzIDdaK~wn)ne0LB z{+(x?fN{MCo9V;|M0L*EzuZ>gD@LwR6uL9b2?Tv3DsWAWINS360AYNpJhRmQZQEbB z-S*{GUMO<$Ii5ok>?Wapm8Mz>2vC~EN0~=~l={u;T0b7=RMEu(eST7PPGtf`3#6Xb zB-wbxrXoc}??P!V1j^yDN5KdLqQNCCG#fmj8t~t>jG40!{cj=Y3`1SPc?67=MIKd+*Uh+EHU7XcZ*z zF=gOOM#$X1fVs$&r1>0~R(|TFL?NY9dBKE=vXpUP{@;84&lTFt5k}Cpq+Zwjlx<)I zSE}>0(Y)zQBuW|a%2gnyY9JfWR#hqgzgPr82<0knpjBUE_k5Qhbp2L_&a6jB{VBO= zuh3b5tcQfK?m!iGT>#h5QP0gS`dKILH%+qC+64L-N8? z4;ok4^#u_}HaGVPrS=PQsrT4GgYV_13(Ua8CqU0e6M3wW{Fe=~JXP}Z zw*Ccg1SBVyNP4vEwJWrD!hB#(kt4RJ{z%_oVy|3wzVkRO`n#1k2rBBcG-#l2EQKxQ zr(!8=(`}D_)3oV<5OA@bD7T!fOtln>l>oqq=5s2@IQ9nW;7?rJQbJ^k2w&e=|7yk< zI5rrKM25YcT_ARHWZ+RDQy5j4lVT|wquImzNMv5ZRwGh*Pr2kr;t^2CXpn-PcJ?6R z&?%RksV-mBk9Y+U{SVhx{U%V+r!Pa0&Pv&-_1@Kl+4nCn9W;}^bHzTzbA*Uor^r~A zv$MAWUL+Q;FLZGpGnrY74Fd7W03^44i-XEk>vni~VCeh13F>ljSYINQ0HX7yot*%Q zNOA`GDe#Ftv?@qurp`wdf7j&L{-)Kv8kfY&hlo6;fSUV($6_YCG^y2|)xuPV*c-)N zJzp*iy?WT^ky+n)O7RUDGZy@&m**AEye>{zCu#nox z*S2NZss4VP829~+ijKwT1Ge%fY)0DNKS&(LfXOzdU@l9-(`ey1=wDpmGCP*<7>#fR%>H*t_CAG zNltK{f*^eyoPp6uPjhO?Pq2r5>s;MTw4t~U#sPM&`Opy|ey&4P#X6&dW2{RKs4jF1B}w%Ug$n3swp4{+YWRj z6Cc|zknq*xL~H&AF11=JQuQ+pTFPOR3wz6Hy=LsG3F z8w99%m+$uj$TmxQSXb!NyzOMBT=bUMzoi~LPLdUzt08l)1v;o)-Y*s-6urw5G2GwE zt75&8&UjFcm2SX8(_lyo_@JU;U{+3%2V7`3 z3AIZIYHX$pN5iAWo=Nj-2$HSB9hr++P}kyMKm2d?DWCVFo!0%sA|+cAE1%s#JiVob z1ba(C908sx>ysQPg{ft}9ToG_x+Jiv^=O2ee-{);u5ojl!jF4Td8k#G_u@LHn!+rn zNIko4@h9a{F4zJju@6O#$dUsOhd)fW`D*5MBuaF5$A=l6OUj3v({a_(U>~1yFfFv; zHH>TZ*G9Ow-S+j(dUcISANo`(A%RsM22x~}^_uJi+n_{6x@E0OwEOu{hRS{+KNpmS zpsORxsnGRPSu?ZA_qY8qi4G!NVUHd2v?K!4d-=fR)Ky1h9fEBG>+s30_ofGt3ald! z_N40TQNuVLhcFQ!>s&38FnZxsRl~2*5${N0r=&PJ&wK3AU;F}faeKr;KHaB$=1&up z72Tt^9A`v~ae|r%8VHgbSsr5c{PW+_De$yJ@~HE`&w}@gU^Ud$9iN5)C3*0sko=<8 zaCfV}VFh+UaBrLIOR9iSm>2{Rtgd;uvZ8^S3r%kEUN^AXOBo&YuSgZR7bXr>xa0^n zlNe7)w^uL`cU|WVcoN$WJbtJB!kI@|VD;LiKkksBqXHFUtXOEb_44clv28wF(POvK z%d9L_z%~rHIRg6@KXmDIlCP)?y|0=R2Ql}8BzZYKlSO|#(t)gY7R~az66^u*)W4c7 z7#nCP@RAlef1LmHFRnfXHE?z?2*Of4R&oE&R-XZVHFuPhq#6qT3gv8IsT>y0Ja|cs z{%qu`f+YSYri>L~=CYQ!eU9bA9uTbx#>e}Iaf}~+gQMzI8n%hEAd&!T0y7}g?3P?t ztN*ua2!(}*Df{>F@?S4jlf5baJ?0aSQ4D7$)-dU~04voIBG}g1E2xXg)UPVoc)0ua zp`x$mxG;#;d;#Tu{3-2)lv<}wZ1!e9D$cM_az4!Ux_#|!;rBuXv4v_-r+m&&D%f%j z&~@9->di4?bzD>=oogxu#eDXO`TKHIYfb0rNl(AcV({%?_3mBvB`kAjwxCW)`01q=Z61^o`?K=E!6wZgffmow> z3z~2k+l$fOJk_%<>vH=s9~n0;Tubt}Z)5Y)LtWhgSlDTEmGL0%)CIP=m+5`2Hi!lw#I}iRX%e#7V z9p5*EG!f_1vsPV9ES-rE1h!DeC7z1XKW*N?x2j{i z4_`JV^Fr~TqnsR>6X&b)eMwl_X%u+(Z(rS?jySl0S-x3WvMR=PtQ7@1#{dpzt~Uws z?I_%3)N$vYQzv2B8C#ytI-Ii`dR3EpsoM|=;j^-tf+{Rt|(CEN6OQWi^&hX0z_jcQ z96oo#pL88>G#vL z5k~0nFhX^iP~99*Bsss~m21qHGI{LM#8~#Gg_Rt6|63Bl z2L~j+Off+HG^2KQ;F_!i^iA9 zq=$AcoMh&^ryM=^@bCvtU(OQ7F0eNzLAt2-U-AKRaNO;H&ixP}fz9p4j~{DBaEu;` z=gO!-1-}9`hG3_ybtE%S+AN!r!7TRM=%`5UxId{BfL|1FkX`@)bo8^;v8dostO#l~ z+}1i=Ym+9Pal?*#VTJ3sD2O6My~6I#F?-?KX%-$>)1vqFgGn?X{jp6iDRq2s@R@?z zqjg4=`1mJJlti%UctH9f0FW7EqT77hCEVOg8c?qc42twVm};{)QD$_H1=A=~GhWxr zdUJ(td7-~m*+&MUoSNL%92Vs74jQe{=)}ZdwunjnQ*a2zc9|}|XV_H8>46!INvx@E zq}N>B>Z`PlC)T16?8pBiD`p^2mkP$*Rf8>GVRk~R?LV2q%w90rld&l_BPFhg-v6cs z!D97`o@x^wukV{@8491`+d5k(&kJN#bu@+s`8xruQ5?V}@gRv^z%y_rW&5=E*Wa*I zlFDm1TB>%v$iWMHlL5eCFJPW}mvm^i*&-C%`zxMw*7-m6@Q`9Vp@@)z_?_6qj~_sG zz+Y$SiWFJcoi0%k*%h-CfS22+iUA{j?C#zuHS{6JirCxmXR-Bo@qid1ga#(ABdjYg&GG_A}ntbr4P?jjxl zr^Z%WWOngP#!ksmb7e#QA3W~eb%3C=`cpD~hQLiguw@OLY9LV5?|Tkv{LoJH7QJhI z{VK5Qs9H)aqO|USNqEyAWEJXUJhMzU-8(*iY@;SXJ)uwhBZwms!?<@S0GP}?#Y0}$GKJwP zQ-g(8qvLvp^@XrArv9HWUBs}Y-a)Aw3hC9U!TV?Uu33%OH0;&oe4br^6nBgKO7<4q?exN3TR?-hIJ-xa<## z;gI`uWOeq8GsP3g$(i=YRc_&ga`}WA0`IN6NDva0rwF_^0f9il{=(KTLnci~H_pzQ zt?{J(wuLzVi-Vk7L#}^HNgzMpe&j>W@a(i_&9Z-OpTzXz!8&=%-y-J}iB$afNoTd| z06~t7;hCmdrvdqXEeI+);7~lxo->WCE}q<3hNP&8<40 z;_aO|0NwdDBYmVDc8<1c%i4_za9Gd*rs>}oE68I0Hvw)++ zjM;+r2Rq0OP#Z@6qP2*A=|6*Fr{}h(cq3a+0N}lwAW^y(X+&>c4kUYPL%%%F@LTJn z?X<_Mos<>wmK>yAmk#+zEfI0XQe2B_;T z&_86yx`Cac)ZO$2k<%&C?NldRG`0Ghs)MxM%b|;U z+4V52CjjI%)qYi;Kz;+sZMADd!&-T2a3Nr?6?9-DzU?(Yk$~Ty!6L^q{U94EISSnZ zx+`%CKoo_}8oabK7H%d%XiD(K0W%ghvD29bh9Ml4ZQ4Q{fqQCw2D=w<3K>HDJc}dO zMpKjZA8s`+#mERu56a?qvoTkT*aY zAV^FvIWBIo!@xLj!3DpsgrbA@5kw1Y=8i)_b^-)SGfYiFT2z z6Lpb#c;n>gsIE(*oGW&mqqA-`rKBVs?9q=_xQfJ&*K_f%xr2XD@`!~YdAiHi*KFNP z@#R90+D-b`)D+AIn*J6X=Ejx}`|NPu>#a7xQP@6%W!x$@YxL~+tns`_z~wAdi(pUx zZf+GynL!41ojB;eN?!G7O7*3``V?LHU(>-q0LsCq;~ z>VE$P6Snu)IDT(!s4VXn4x0%(yZcjPx?E?Xy!t+`u8ILvs&qLt@S@o7phqxG>c!6! z4$G1o2b0!Ot#33aXH41XY$yF)2nZLb#)xP=(f>t{oKEj2jVYk ztrmX|8pYaa?a1sz2W=$La&l9JSo1yFY>v4Z40jpp6=dPE^t)D4y;~(URE!6vt$LwF z_gC3igRRc^fxG*jHS*Ps@gWXI>3Z>wI&sc@47RXB7^IY3vv$4(Q=D7-%tKAD)NK~{ zftDTabAJr6p8Hcg_itxsPb|Mwd6Kd2ubx?6zCG{jT9b~}M`thTvol)Nma5qL=r4=J zm%TUM&sD!`Wocp7?16H*9SyPqQQO^+JNWV{Efh^RN2?U)LI?UusFq_{}b6V-E1{yp=k$E<1OI>%wqX9yUNZ{u;sqxAxDg2F;`-?Q;C|f#s&B?yuLJ z+zT{tJ?kPBwb>fgj0tC~)LF`eEyZOq*18Ji9P)wBk|MbAvMEQPO#2qF49VAS|9)2A zFF86#_S@j7QcINFJv!)P99v-H8X)hR&MG0{31Zq>pR_$TF(CmW(?$neMSmkT=Rv|l zw-o)mRu*rEBoX8VDq@_J3QlHTY)jVO&;eMx*=CFDBU^7a zv3iRqz9&zp@b(nY3Oa$*4K6uL*)@~n=7Y^J!U7Y$@bpoUpy(JUvIui)M z{(#!=j3;X$%nBDun!R_o?NPvDGFL@okc^Y*! z9f`lvco}T&&er&2$m7pDrzl^|SUT(fP}(HA%Om&sHCb0Pe`@L)crNzyL{*V=cIH&G z>(bH!t<4fa?kAJqem?DXr+>=+8s_d-79;&M=>jj1wMYW(t&<~DdG?l9)budSx#UQ; zd(st|HwTfNC#KuiKCp#o} zt2z}cL_LTt3Gld(0U~Br{#8VpU2o{ThX<)>MKq2_6k@JdW0iP%<5KcTHKYstYQ;n0 zhNC8j){t|cew*p`48|wYTxma|$`om|0D)dDe?>cH**WS(>g4#zO+cluA=LG8CxWd% zG3mP^AzT#14k3HQW)VnDI!01cTRl(ycAvA7N^v>M@T;&K5^daR2u1x~>p~FtQ?(p4;)pcDE4@9Bk>{E(n>9$ZmJBJzb zdaBjm5PWn;ha7Cir(>t@?>w%9t51P1qrlXI2t*pq@j+l(8fZJ+cH^RfRB;|3#G6oA zOMbupi|mDgmL*+l=|jINLEz6~Du=W$-fXn`-u_-HiLUwbJ?H00l!8hRm|*R6f@`c$ z_~-vv{8w)`Dz ziZg$Vgy~@`*mR?^+DGIY(#qi%015G(A4I)o_BG~}$!&p@|JESG#n*{j-rlQa+`*@p zmxNF}MveR7AcYq;u=)T50nu4Sdd&o$yHM{y#m{*b%#rgS8}Z*iJ0nX#5#?tOqgRJ> z%G}ofW`?$(_F19!pZ_uPz3H`??)Hfz5WqhxbX1j$jDgE~UhOn3)Xv?{kTD>S@JRF6 z8C;8H5%B?;*WCyT^!X{OhYuYc-BKiA`M`6Y*Z7nOQ!2~ITL58M!Dc2S zpKSU9%>j%O!Oakh7YFwSD!AX|a{>l*c%D{ROIa((*+4AC-Y-!?6e6i%buv2ct-S3Qb zTgUAYj>*UAq@>d{cXU50_WTOfgM{YnR`(Vm6Rvv#y72XYgq>Z#??4YSfG$n%n0n1+ zeXpw8t4G)X&r^m3}(;cy@Wtrib@J0n=*t4H^*4Bt~ybof8O;Q_`N+v*?I5}E@Yq(INxXQX23LR6g^`U*&0^wqcs^!;lqCm&Y$kk2e(8Na>lzrUja z35c+OO903Qlms?EXOC8A)a<=DJ(v*gs>D5s;PIMe(^?zdQBM;}RD#>*7ZrHPQ2DD% zfsQC_^CUNyK4zvK{cUE;FKZH*H|P5B&m7rkkMRcb#FwB?5!ineriq2CxCMetXe%## zY=PaiU`15#CSo&Lbin(SRzJ88#2-%03T3??d;LQ-iYE8r(aR`FN1#s~n&@*?X?r1F zzP?nagU*9Nt=y9m1YNk5eQ0Lv>6*VRVSG2DIupbintwR2vJOgtTPuuH-~qTX$8heM zrS7leD;;A?|9O&-%ZPqCZ1KX*_Azi~Swb^L5c1CcAy`n{Ers-PVpvYzvRA}mAyb!$ zp!9vgQJypv3hFwyncY(WV@e0apE-`)3vy=6vCiPa-5TZ|tkB=}nd4d>$nL{w0C9}o zo>t)#cr#&iyoRMKmcSR-&1E&$#DK&~Gi z$i4zC+F&6ZXye*Oeo#F8+h*)b8oV^9=3sL_K&{=K&1GGXQ=ysio*u=X{is_$>0n{a zdv1ATE>bcgc&@&XT-$6XqQRHcXRy7?1KfwuSuj(6H;-}scE{bJlaFK|6Mk?$zsqlO zZ_<6^4t+5&BBA@93AgpEBY6C3T_YBnvOXV{t%#NjZX!n#PubAt<%Ka&5Yiqd)2;Xt z4;l{cnY&Rw-u2fP)bbl|U;49y$K8oT0lIw;^4@!pZx$_pc$sS z9cLv9A^HC{FBT^KdikvK2+-3jpoNdb=b3#I5~HeSHI0EV#cD2B+}T z-EFKf865)HHY3c>Hvl=o-(a`&$Dy?Ad5cn2xwH3OtIQR}>t~%NC1aB%UjNNB1MVyC zI#7lRwdNMLf5sQM7RZff+z${)Tl3$47jciV(y#QI$-}s|1!1`Wvs6ogN<5ga+q@;7_JW`Df65J!b+#l`HGGMp#TFUBj=c5eV zJzV3JW39n@;cuf1Om`NgUcMy$!kE}9JdioSeyCXFZ9dKntq0ndMGdS`wQH!xe#G1B z`s<;%!^5*imf~u?59*=9!oArjBR`J3<1oafKa(bycgqbCY&9ByN&8d385sh{#~ct} zo7FkbJOP6En*)eP*pJU8Xr&Gg?@Lsb96iSy-Th1SDc{HS@c6pL0Q#lnfL{K6&nqp{ zR16Z*bV%1Bl;i8fyT$aUhXZ#=mI`MWQvrBJN5idgQQs!b)cB|_A4j=g*gMGxT8QwR z)BhKMelE(ZQ^ec*I*=;z4zS~JHZS0hWM%7D7r4RQ+fW!}=H60zsPk&^4zom<&t#%d ztJ@EOn6a%QAL8ET+@?QG#`QK&ELkWz0;pa)J+PqZ2Rz0VpPE|TN#9xIMS5wDu#W1@ zBx3LdSta&)_pp5LT6#z)(sw7AEv^3L=$BGWr*Y1q#GXq8q2GGIQ2EJ_Uss<5T;)+%gLD%EyUkk?X^`x ze3-oI-EL&F@}1o^81vg;#?^Q5gn}UPM{#?gVY1O{YxkwkWyXFzZ6(A4_wf>QKBJKagVI^-d(kqp2tC$yd z{BEsOVbP1%c)LrW<;)EM#u0FKjDue&sb3t_tr#KcT5426Nv+LyA?3BBrD)a&e~%rH zmLudaN7(6^8+qktx$TeRBpDfAP~$dE4Y(8HW3KjN&wxj-$<@egvfshxA_9?xHlefn tW9B}n${oSlN5w|Q#Q1;aqa%In-#*T=mUx!W4|JWO`>LAAlH2Bi{{u*|QH}rr literal 0 HcmV?d00001 diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 4c10710..a6bab93 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -4,20 +4,31 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:sudoku/api/api.dart'; import 'package:sudoku/home/home.dart'; import 'package:sudoku/l10n/l10n.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/theme/theme.dart'; class App extends StatelessWidget { const App({ required SudokuAPI apiClient, + required PuzzleRepository puzzleRepository, super.key, - }) : _apiClient = apiClient; + }) : _apiClient = apiClient, + _puzzleRepository = puzzleRepository; final SudokuAPI _apiClient; + final PuzzleRepository _puzzleRepository; @override Widget build(BuildContext context) { - return RepositoryProvider.value( - value: _apiClient, + return MultiRepositoryProvider( + providers: [ + RepositoryProvider.value( + value: _apiClient, + ), + RepositoryProvider.value( + value: _puzzleRepository, + ), + ], child: const AppView(), ); } diff --git a/lib/assets/assets.dart b/lib/assets/assets.dart index 4f883b6..6dda0ab 100644 --- a/lib/assets/assets.dart +++ b/lib/assets/assets.dart @@ -20,4 +20,7 @@ abstract class Assets { /// Gemini icon. static const geminiIcon = 'assets/icons/gemini.png'; + + /// Heart icon. + static const heartIcon = 'assets/icons/heart.png'; } diff --git a/lib/home/bloc/home_bloc.dart b/lib/home/bloc/home_bloc.dart index 8f590a7..3096017 100644 --- a/lib/home/bloc/home_bloc.dart +++ b/lib/home/bloc/home_bloc.dart @@ -4,6 +4,7 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:sudoku/api/api.dart'; import 'package:sudoku/models/models.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; part 'home_event.dart'; part 'home_state.dart'; @@ -11,12 +12,15 @@ part 'home_state.dart'; class HomeBloc extends Bloc { HomeBloc({ required SudokuAPI apiClient, + required PuzzleRepository puzzleRepository, }) : _apiClient = apiClient, + _puzzleRepository = puzzleRepository, super(const HomeState()) { on(_onSudokuCreationRequested); } final SudokuAPI _apiClient; + final PuzzleRepository _puzzleRepository; FutureOr _onSudokuCreationRequested( SudokuCreationRequested event, @@ -24,7 +28,6 @@ class HomeBloc extends Bloc { ) async { emit( state.copyWith( - sudoku: () => null, difficulty: () => event.difficulty, sudokuCreationStatus: () => SudokuCreationStatus.inProgress, sudokuCreationError: () => null, @@ -35,9 +38,11 @@ class HomeBloc extends Bloc { final sudoku = await _apiClient.createSudoku( difficulty: event.difficulty, ); + _puzzleRepository.storePuzzle( + puzzle: Puzzle(sudoku: sudoku, difficulty: event.difficulty), + ); emit( state.copyWith( - sudoku: () => sudoku, sudokuCreationStatus: () => SudokuCreationStatus.completed, ), ); diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index 287af7e..6d8c224 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -7,33 +7,28 @@ enum SudokuCreationErrorType { unexpected, invalidRawData, apiClient } class HomeState extends Equatable { const HomeState({ - this.sudoku, this.difficulty, this.sudokuCreationStatus = SudokuCreationStatus.initial, this.sudokuCreationError, }); - final Sudoku? sudoku; final Difficulty? difficulty; final SudokuCreationStatus sudokuCreationStatus; final SudokuCreationErrorType? sudokuCreationError; @override List get props => [ - sudoku, difficulty, sudokuCreationStatus, sudokuCreationError, ]; HomeState copyWith({ - Sudoku? Function()? sudoku, Difficulty? Function()? difficulty, SudokuCreationStatus Function()? sudokuCreationStatus, SudokuCreationErrorType? Function()? sudokuCreationError, }) { return HomeState( - sudoku: sudoku != null ? sudoku() : this.sudoku, difficulty: difficulty != null ? difficulty() : this.difficulty, sudokuCreationStatus: sudokuCreationStatus != null ? sudokuCreationStatus() diff --git a/lib/home/view/home_page.dart b/lib/home/view/home_page.dart index f2e9ab2..adeebcf 100644 --- a/lib/home/view/home_page.dart +++ b/lib/home/view/home_page.dart @@ -9,7 +9,7 @@ import 'package:sudoku/home/home.dart'; import 'package:sudoku/l10n/l10n.dart'; import 'package:sudoku/layout/layout.dart'; import 'package:sudoku/models/models.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/typography/typography.dart'; import 'package:sudoku/widgets/widgets.dart'; @@ -25,6 +25,7 @@ class HomePage extends StatelessWidget { return BlocProvider( create: (context) => HomeBloc( apiClient: context.read(), + puzzleRepository: context.read(), ), child: const HomeView(), ); @@ -54,13 +55,12 @@ class HomeView extends StatelessWidget { ); } - if (state.sudoku != null && - state.sudokuCreationStatus == SudokuCreationStatus.completed) { + if (state.sudokuCreationStatus == SudokuCreationStatus.completed) { Navigator.pop(context); Navigator.push( context, MaterialPageRoute( - builder: (context) => SudokuPage(sudoku: state.sudoku!), + builder: (context) => const PuzzlePage(), ), ); } diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 4b7a3bb..2b2415a 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -8,10 +8,6 @@ "@sudokuGameSubtitle": { "description": "Text shown in Home Page for Medium and Large screen as the subtitle" }, - "sudokuAppBarTitle": "New Sudoku Game", - "@sudokuAppBarTitle": { - "description": "Text shown in the AppBar of the Sudoku Page" - }, "resumeTimerButtonText": "Resume the puzzle", "@resumeTimerButtonText": { "description": "Text shown in the FloatingActionButton of the Sudoku Board" @@ -96,5 +92,26 @@ "errorClientDialogSubtitle": "There has been an error while communicating to the backend service. Please try again. If this issue persists, please try after some time.", "@errorClientDialogSubtitle": { "description": "Text shown as subtitle in the error due to client dialog in Home Page" + }, + "puzzleAppBarDifficulty": "{difficulty, select, easy{Easy} medium{Medium} difficult{Difficult} expert{Expert} other{New}}", + "@puzzleAppBarDifficulty": { + "description": "Text shown in app bar of the Puzzle Page", + "placeholders": { + "difficulty": { + "type": "String" + } + } + }, + "puzzleAppBarSudoku": "Sudoku", + "@puzzleAppBarSudoku": { + "description": "Text shown in app bar of the Puzzle Page" + }, + "eraseInputButtonText": "Erase number", + "@eraseInputButtonText": { + "description": "Text shown in the input erase button in Puzzle Page" + }, + "sudokuLoadingText": "Building your sudoku", + "@sudokuLoadingText": { + "description": "Text shown when sudoku is getting initialized in Puzzle Page" } } \ No newline at end of file diff --git a/lib/main_development.dart b/lib/main_development.dart index 4869477..94a9ce5 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -1,11 +1,20 @@ import 'package:sudoku/api/api.dart'; import 'package:sudoku/app/app.dart'; import 'package:sudoku/bootstrap.dart'; +import 'package:sudoku/cache/cache.dart'; import 'package:sudoku/env/env.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; void main() { bootstrap(() { final apiClient = SudokuDioClient(baseUrl: Env.apiBaseUrl); - return App(apiClient: apiClient); + + final cacheClient = CacheClient(); + final puzzleRepository = PuzzleRepository(cacheClient: cacheClient); + + return App( + apiClient: apiClient, + puzzleRepository: puzzleRepository, + ); }); } diff --git a/lib/main_production.dart b/lib/main_production.dart index 4869477..94a9ce5 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -1,11 +1,20 @@ import 'package:sudoku/api/api.dart'; import 'package:sudoku/app/app.dart'; import 'package:sudoku/bootstrap.dart'; +import 'package:sudoku/cache/cache.dart'; import 'package:sudoku/env/env.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; void main() { bootstrap(() { final apiClient = SudokuDioClient(baseUrl: Env.apiBaseUrl); - return App(apiClient: apiClient); + + final cacheClient = CacheClient(); + final puzzleRepository = PuzzleRepository(cacheClient: cacheClient); + + return App( + apiClient: apiClient, + puzzleRepository: puzzleRepository, + ); }); } diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 4869477..94a9ce5 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -1,11 +1,20 @@ import 'package:sudoku/api/api.dart'; import 'package:sudoku/app/app.dart'; import 'package:sudoku/bootstrap.dart'; +import 'package:sudoku/cache/cache.dart'; import 'package:sudoku/env/env.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; void main() { bootstrap(() { final apiClient = SudokuDioClient(baseUrl: Env.apiBaseUrl); - return App(apiClient: apiClient); + + final cacheClient = CacheClient(); + final puzzleRepository = PuzzleRepository(cacheClient: cacheClient); + + return App( + apiClient: apiClient, + puzzleRepository: puzzleRepository, + ); }); } diff --git a/lib/puzzle/puzzle.dart b/lib/puzzle/puzzle.dart index 64ad6f3..3b17992 100644 --- a/lib/puzzle/puzzle.dart +++ b/lib/puzzle/puzzle.dart @@ -1,3 +1,5 @@ export 'bloc/puzzle_bloc.dart'; export 'models/models.dart'; export 'repository/puzzle_repository.dart'; +export 'view/view.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/puzzle/view/puzzle_page.dart b/lib/puzzle/view/puzzle_page.dart new file mode 100644 index 0000000..5714c34 --- /dev/null +++ b/lib/puzzle/view/puzzle_page.dart @@ -0,0 +1,338 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/colors/colors.dart'; +import 'package:sudoku/l10n/l10n.dart'; +import 'package:sudoku/layout/layout.dart'; +import 'package:sudoku/models/models.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; +import 'package:sudoku/timer/timer.dart'; +import 'package:sudoku/typography/typography.dart'; +import 'package:sudoku/widgets/widgets.dart'; + +class PuzzlePage extends StatelessWidget { + const PuzzlePage({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => PuzzleBloc( + puzzleRepository: context.read(), + )..add(const PuzzleInitialized()), + ), + BlocProvider( + create: (context) => TimerBloc( + ticker: const Ticker(), + )..add(const TimerStarted()), + ), + ], + child: const PuzzleView(), + ); + } +} + +@visibleForTesting +class PuzzleView extends StatelessWidget { + const PuzzleView({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + systemOverlayStyle: theme.brightness == Brightness.light + ? SystemUiOverlayStyle.dark + : SystemUiOverlayStyle.light, + ), + body: const SudokuBackground( + child: PuzzleViewLayout(), + ), + ); + } +} + +@visibleForTesting +class PuzzleViewLayout extends StatelessWidget { + const PuzzleViewLayout({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final dimension = context.select( + (PuzzleBloc bloc) => bloc.state.puzzle.sudoku.getDimesion(), + ); + + if (dimension <= 0) { + return Center( + child: Text( + l10n.sudokuLoadingText, + style: SudokuTextStyle.caption, + ), + ); + } + + return ResponsiveLayoutBuilder( + small: (_, child) => child!, + medium: (_, child) => child!, + large: (_, child) => Align( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height, + maxWidth: SudokuBreakpoint.large, + ), + child: const Align( + child: SingleChildScrollView( + child: Column( + children: [ + PageHeader(), + ResponsiveGap(large: 96), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + SudokuBoardView(), + SizedBox(width: 56), + Column( + children: [ + SizedBox( + width: SudokuInputSize.large * 3, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MistakesCountView(), + SudokuTimer(), + ], + ), + ), + ResponsiveGap(large: 32), + SudokuInputView(), + SizedBox(height: 32), + InputEraseViewForLargeLayout(), + ], + ), + ], + ), + ], + ), + ), + ), + ), + ), + child: (layoutSize) { + final maxWidth = switch (layoutSize) { + ResponsiveLayoutSize.small => SudokuBoardSize.small, + ResponsiveLayoutSize.medium => SudokuBoardSize.medium, + ResponsiveLayoutSize.large => SudokuInputSize.large * dimension, + }; + + return Align( + alignment: Alignment.topCenter, + child: SingleChildScrollView( + child: Column( + children: [ + const ResponsiveGap( + small: 72, + medium: 24, + large: 32, + ), + const PageHeader(), + const ResponsiveGap( + small: 24, + medium: 32, + large: 48, + ), + SizedBox( + width: maxWidth, + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MistakesCountView(), + SudokuTimer(), + ], + ), + ), + const ResponsiveGap( + small: 16, + medium: 24, + large: 32, + ), + const SudokuBoardView(), + const ResponsiveGap( + small: 16, + medium: 24, + large: 32, + ), + const SudokuInputView(), + ], + ), + ), + ); + }, + ); + } +} + +class InputEraseViewForLargeLayout extends StatelessWidget { + const InputEraseViewForLargeLayout({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return GestureDetector( + onTap: () => context.read().add(const SudokuInputErased()), + child: SizedBox( + width: SudokuInputSize.large * 3, + height: 56, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + gradient: const LinearGradient( + colors: [ + SudokuColors.darkPurple, + SudokuColors.darkPink, + ], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ), + ), + child: Center( + child: Text( + l10n.eraseInputButtonText, + textAlign: TextAlign.center, + style: SudokuTextStyle.button.copyWith( + color: Colors.white, + ), + ), + ), + ), + ), + ); + } +} + +@visibleForTesting +class SudokuInputView extends StatelessWidget { + const SudokuInputView({super.key}); + + @override + Widget build(BuildContext context) { + final sudoku = context.select( + (PuzzleBloc bloc) => bloc.state.puzzle.sudoku, + ); + + return SudokuInput( + sudokuDimension: sudoku.getDimesion(), + ); + } +} + +@visibleForTesting +class PageHeader extends StatelessWidget { + const PageHeader({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + const gradient = LinearGradient( + colors: [ + SudokuColors.darkPurple, + SudokuColors.darkPink, + ], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ); + + final difficulty = context.select( + (PuzzleBloc bloc) => bloc.state.puzzle.difficulty, + ); + + return ResponsiveLayoutBuilder( + small: (_, child) => child!, + medium: (_, child) => child!, + large: (_, child) => child!, + child: (layoutSize) { + final titleTextStyle = switch (layoutSize) { + ResponsiveLayoutSize.small => SudokuTextStyle.headline6, + ResponsiveLayoutSize.medium => SudokuTextStyle.headline5, + ResponsiveLayoutSize.large => SudokuTextStyle.headline1, + }; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (bounds) => gradient.createShader( + Rect.fromLTWH(0, 0, bounds.width, bounds.height), + ), + child: Text( + l10n.puzzleAppBarDifficulty(difficulty.name), + style: titleTextStyle, + ), + ), + Text( + ' ${l10n.puzzleAppBarSudoku}', + style: titleTextStyle, + ), + ], + ); + }, + ); + } +} + +@visibleForTesting +class SudokuBoardView extends StatelessWidget { + const SudokuBoardView({super.key}); + + @override + Widget build(BuildContext context) { + final dimension = context.select( + (PuzzleBloc bloc) => bloc.state.puzzle.sudoku.getDimesion(), + ); + + return ResponsiveLayoutBuilder( + small: (_, child) => child!, + medium: (_, child) => child!, + large: (_, child) => child!, + child: (layoutSize) { + final blockSize = switch (layoutSize) { + ResponsiveLayoutSize.small => SudokuBoardSize.small / dimension, + ResponsiveLayoutSize.medium => SudokuBoardSize.medium / dimension, + ResponsiveLayoutSize.large => SudokuBoardSize.large / dimension, + }; + + const key = 'sudoku_board_view'; + + return BlocBuilder( + buildWhen: (p, c) => p.puzzle.sudoku != c.puzzle.sudoku, + builder: (context, state) { + return SudokuBoard( + blocks: [ + for (final block in state.puzzle.sudoku.blocks) + Positioned( + key: Key('${key}_${block.position.x}_${block.position.y}'), + top: block.position.x * blockSize, + left: block.position.y * blockSize, + child: SudokuBlock( + block: block, + state: state, + ), + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/puzzle/view/view.dart b/lib/puzzle/view/view.dart new file mode 100644 index 0000000..f4f5aa1 --- /dev/null +++ b/lib/puzzle/view/view.dart @@ -0,0 +1 @@ +export 'puzzle_page.dart'; diff --git a/lib/puzzle/widgets/mistakes_count_view.dart b/lib/puzzle/widgets/mistakes_count_view.dart new file mode 100644 index 0000000..d852189 --- /dev/null +++ b/lib/puzzle/widgets/mistakes_count_view.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/assets/assets.dart'; +import 'package:sudoku/layout/layout.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; +import 'package:sudoku/typography/typography.dart'; + +/// {@template mistakes_count_view} +/// Displays how many mistakes are remaining for the current puzzle. +/// {@endtemplate} +class MistakesCountView extends StatelessWidget { + /// {@macro mistakes_count_view} + const MistakesCountView({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final mistakesCount = context.select( + (PuzzleBloc bloc) => bloc.state.puzzle.remainingMistakes, + ); + + return ResponsiveLayoutBuilder( + small: (_, child) => child!, + medium: (_, child) => child!, + large: (_, child) => child!, + child: (layoutSize) { + final timerTextStyle = switch (layoutSize) { + ResponsiveLayoutSize.large => SudokuTextStyle.bodyText1, + _ => SudokuTextStyle.bodyText1, + }; + + return DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: theme.dividerColor, + width: 1.4, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + Assets.heartIcon, + height: timerTextStyle.fontSize, + width: timerTextStyle.fontSize, + ), + Text( + ' x $mistakesCount', + style: timerTextStyle, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/puzzle/widgets/widgets.dart b/lib/puzzle/widgets/widgets.dart new file mode 100644 index 0000000..734dc5a --- /dev/null +++ b/lib/puzzle/widgets/widgets.dart @@ -0,0 +1 @@ +export 'mistakes_count_view.dart'; diff --git a/lib/sudoku/bloc/sudoku_bloc.dart b/lib/sudoku/bloc/sudoku_bloc.dart deleted file mode 100644 index e1794ff..0000000 --- a/lib/sudoku/bloc/sudoku_bloc.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:sudoku/models/models.dart'; - -part 'sudoku_event.dart'; -part 'sudoku_state.dart'; - -class SudokuBloc extends Bloc { - SudokuBloc({required Sudoku sudoku}) : super(SudokuState(sudoku: sudoku)) { - on(_onBlockSelected); - on(_onInputTapped); - } - - void _onBlockSelected(SudokuBlockSelected event, Emitter emit) { - if (event.block.isGenerated) { - emit( - state.copyWith( - blockSelectionStatus: () => BlockSelectionStatus.cannotBeSelected, - currentSelectedBlock: () => null, - highlightedBlocks: () => {}, - ), - ); - } else { - final subGridBlocks = state.sudoku.getSubGridBlocks(event.block); - final rowBlocks = state.sudoku.getRowBlocks(event.block); - final columnBlocks = state.sudoku.getColumnBlocks(event.block); - - final highlightedBlocks = [ - ...subGridBlocks, - ...rowBlocks, - ...columnBlocks, - ]; - - emit( - state.copyWith( - blockSelectionStatus: () => BlockSelectionStatus.selected, - currentSelectedBlock: () => event.block, - highlightedBlocks: highlightedBlocks.toSet, - ), - ); - } - } - - void _onInputTapped(SudokuInputTapped event, Emitter emit) { - if (state.currentSelectedBlock == null) return; - - final mutableSudoku = Sudoku(blocks: [...state.sudoku.blocks]); - final blockToUpdate = state.currentSelectedBlock!; - - final sudoku = mutableSudoku.updateBlock(blockToUpdate, event.input); - - if (sudoku.isComplete()) { - emit( - state.copyWith( - puzzleStatus: () => SudokuPuzzleStatus.complete, - sudoku: () => sudoku, - ), - ); - } else { - emit( - state.copyWith(sudoku: () => sudoku), - ); - } - } -} diff --git a/lib/sudoku/bloc/sudoku_event.dart b/lib/sudoku/bloc/sudoku_event.dart deleted file mode 100644 index 132a2bb..0000000 --- a/lib/sudoku/bloc/sudoku_event.dart +++ /dev/null @@ -1,21 +0,0 @@ -part of 'sudoku_bloc.dart'; - -sealed class SudokuEvent extends Equatable { - const SudokuEvent(); -} - -final class SudokuBlockSelected extends SudokuEvent { - const SudokuBlockSelected(this.block); - final Block block; - - @override - List get props => [block]; -} - -final class SudokuInputTapped extends SudokuEvent { - const SudokuInputTapped(this.input); - final int input; - - @override - List get props => [input]; -} diff --git a/lib/sudoku/bloc/sudoku_state.dart b/lib/sudoku/bloc/sudoku_state.dart deleted file mode 100644 index 385b4fb..0000000 --- a/lib/sudoku/bloc/sudoku_state.dart +++ /dev/null @@ -1,53 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -part of 'sudoku_bloc.dart'; - -enum SudokuPuzzleStatus { incomplete, complete } - -enum BlockSelectionStatus { nothingSelected, cannotBeSelected, selected } - -class SudokuState extends Equatable { - const SudokuState({ - required this.sudoku, - this.puzzleStatus = SudokuPuzzleStatus.incomplete, - this.blockSelectionStatus = BlockSelectionStatus.nothingSelected, - this.highlightedBlocks = const {}, - this.currentSelectedBlock, - }); - - final Sudoku sudoku; - final SudokuPuzzleStatus puzzleStatus; - final BlockSelectionStatus blockSelectionStatus; - final Set highlightedBlocks; - final Block? currentSelectedBlock; - - @override - List get props => [ - sudoku, - puzzleStatus, - blockSelectionStatus, - highlightedBlocks, - currentSelectedBlock, - ]; - - SudokuState copyWith({ - Sudoku Function()? sudoku, - SudokuPuzzleStatus Function()? puzzleStatus, - BlockSelectionStatus Function()? blockSelectionStatus, - Set Function()? highlightedBlocks, - Block? Function()? currentSelectedBlock, - }) { - return SudokuState( - sudoku: sudoku != null ? sudoku() : this.sudoku, - puzzleStatus: puzzleStatus != null ? puzzleStatus() : this.puzzleStatus, - blockSelectionStatus: blockSelectionStatus != null - ? blockSelectionStatus() - : this.blockSelectionStatus, - highlightedBlocks: highlightedBlocks != null - ? highlightedBlocks() - : this.highlightedBlocks, - currentSelectedBlock: currentSelectedBlock != null - ? currentSelectedBlock() - : this.currentSelectedBlock, - ); - } -} diff --git a/lib/sudoku/sudoku.dart b/lib/sudoku/sudoku.dart index 8c02f88..ec0822d 100644 --- a/lib/sudoku/sudoku.dart +++ b/lib/sudoku/sudoku.dart @@ -1,4 +1,2 @@ -export 'bloc/sudoku_bloc.dart'; export 'models/models.dart'; -export 'view/view.dart'; export 'widgets/widgets.dart'; diff --git a/lib/sudoku/view/sudoku_page.dart b/lib/sudoku/view/sudoku_page.dart deleted file mode 100644 index cf30ba9..0000000 --- a/lib/sudoku/view/sudoku_page.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:sudoku/l10n/l10n.dart'; -import 'package:sudoku/layout/layout.dart'; -import 'package:sudoku/models/models.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; -import 'package:sudoku/timer/timer.dart'; -import 'package:sudoku/typography/typography.dart'; - -/// {@template sudoku_page} -/// The root page of the Sudoku UI. -/// {@endtemplate} -class SudokuPage extends StatelessWidget { - /// {@macro sudoku_page} - const SudokuPage({required this.sudoku, super.key}); - final Sudoku sudoku; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => SudokuBloc( - sudoku: sudoku, - ), - ), - BlocProvider( - create: (context) => TimerBloc( - ticker: const Ticker(), - )..add(const TimerStarted()), - ), - ], - child: const SudokuView(), - ); - } -} - -/// {@template sudoku_view} -/// Displays the content for the [SudokuPage]. -/// {@endtemplate} -class SudokuView extends StatelessWidget { - /// {@macro sudoku_view} - const SudokuView({super.key}); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - final sudoku = context.select( - (SudokuBloc bloc) => bloc.state.sudoku, - ); - - return ResponsiveLayoutBuilder( - small: (_, child) => child!, - medium: (_, child) => child!, - large: (_, __) => Scaffold( - body: SingleChildScrollView( - child: Column( - children: [ - const ResponsiveGap(large: 246), - const Center( - child: SudokuTimer(), - ), - const ResponsiveGap(large: 96), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 250, - child: Text( - l10n.sudokuAppBarTitle, - style: SudokuTextStyle.headline1.copyWith( - fontWeight: SudokuFontWeight.black, - ), - ), - ), - const SizedBox(width: 60), - const SudokuBoardView( - layoutSize: ResponsiveLayoutSize.large, - ), - const SizedBox(width: 96), - SudokuInput( - sudokuDimension: sudoku.getDimesion(), - ), - ], - ), - const ResponsiveGap(large: 246), - ], - ), - ), - ), - child: (layoutSize) { - return Scaffold( - appBar: AppBar( - title: Text(l10n.sudokuAppBarTitle), - ), - body: SingleChildScrollView( - child: SizedBox( - width: double.maxFinite, - child: Column( - children: [ - const ResponsiveGap(small: 16, medium: 24), - const SudokuTimer(), - const ResponsiveGap(small: 16, medium: 24), - SudokuBoardView(layoutSize: layoutSize), - const ResponsiveGap(small: 32, medium: 56), - SudokuInput( - sudokuDimension: sudoku.getDimesion(), - ), - ], - ), - ), - ), - ); - }, - ); - } -} - -@visibleForTesting -class SudokuBoardView extends StatelessWidget { - const SudokuBoardView({ - required this.layoutSize, - super.key, - }); - final ResponsiveLayoutSize layoutSize; - - @override - Widget build(BuildContext context) { - final dimension = context.select( - (SudokuBloc bloc) => bloc.state.sudoku.getDimesion(), - ); - - final blockSize = switch (layoutSize) { - ResponsiveLayoutSize.small => SudokuBoardSize.small / dimension, - ResponsiveLayoutSize.medium => SudokuBoardSize.medium / dimension, - ResponsiveLayoutSize.large => SudokuBoardSize.large / dimension, - }; - - return BlocBuilder( - buildWhen: (previous, current) => previous.sudoku != current.sudoku, - builder: (context, state) { - return SudokuBoard( - blocks: [ - for (final block in state.sudoku.blocks) - Positioned( - key: Key( - 'sudoku_board_view_${block.position.x}_${block.position.y}', - ), - top: block.position.x * blockSize, - left: block.position.y * blockSize, - child: SudokuBlock( - block: block, - state: state, - ), - ), - ], - ); - }, - ); - } -} diff --git a/lib/sudoku/view/view.dart b/lib/sudoku/view/view.dart deleted file mode 100644 index c3b3560..0000000 --- a/lib/sudoku/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'sudoku_page.dart'; diff --git a/lib/sudoku/widgets/sudoku_block.dart b/lib/sudoku/widgets/sudoku_block.dart index 8ae64db..b76b70a 100644 --- a/lib/sudoku/widgets/sudoku_block.dart +++ b/lib/sudoku/widgets/sudoku_block.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/colors/colors.dart'; import 'package:sudoku/layout/layout.dart'; import 'package:sudoku/models/models.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/sudoku/sudoku.dart'; +// import 'package:sudoku/sudoku/sudoku.dart'; import 'package:sudoku/typography/typography.dart'; /// {@template sudoku_block} -/// Displays the Sudoku [block] based upon the current [state]. +/// Displays the Sudoku [block] based upon the current puzzle [state]. /// {@endtemplate} class SudokuBlock extends StatelessWidget { /// {@macro sudoku_block} @@ -20,18 +23,18 @@ class SudokuBlock extends StatelessWidget { final Block block; /// The state of the sudoku. - final SudokuState state; + final PuzzleState state; @override Widget build(BuildContext context) { final theme = Theme.of(context); - final dimension = state.sudoku.getDimesion(); + final dimension = state.puzzle.sudoku.getDimesion(); final selectedBlock = context.select( - (SudokuBloc bloc) => bloc.state.currentSelectedBlock, + (PuzzleBloc bloc) => bloc.state.selectedBlock, ); final highlightedBlocks = context.select( - (SudokuBloc bloc) => bloc.state.highlightedBlocks, + (PuzzleBloc bloc) => bloc.state.highlightedBlocks, ); // Comparing with the current block's position, otherwise @@ -64,7 +67,7 @@ class SudokuBlock extends StatelessWidget { child: (_) { return GestureDetector( onTap: () { - context.read().add(SudokuBlockSelected(block)); + context.read().add(SudokuBlockSelected(block)); }, child: DecoratedBox( decoration: BoxDecoration( @@ -81,7 +84,11 @@ class SudokuBlock extends StatelessWidget { child: Text( block.currentValue != -1 ? '${block.currentValue}' : '', style: SudokuTextStyle.bodyText1.copyWith( - color: block.isGenerated ? null : theme.colorScheme.secondary, + color: block.correctValue != block.currentValue + ? theme.colorScheme.error + : block.isGenerated + ? null + : SudokuColors.darkPurple, ), ), ), diff --git a/lib/sudoku/widgets/sudoku_board.dart b/lib/sudoku/widgets/sudoku_board.dart index 52d164b..b0f38e9 100644 --- a/lib/sudoku/widgets/sudoku_board.dart +++ b/lib/sudoku/widgets/sudoku_board.dart @@ -2,10 +2,12 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/colors/colors.dart'; import 'package:sudoku/l10n/l10n.dart'; import 'package:sudoku/layout/layout.dart'; import 'package:sudoku/sudoku/sudoku.dart'; import 'package:sudoku/timer/timer.dart'; +import 'package:sudoku/typography/typography.dart'; /// {@template sudoku_board} /// Displays the Sudoku board in a [Stack] containing [blocks]. @@ -28,6 +30,15 @@ class SudokuBoard extends StatelessWidget { (TimerBloc bloc) => !bloc.state.isRunning, ); + const gradient = LinearGradient( + colors: [ + SudokuColors.darkPurple, + SudokuColors.darkPink, + ], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ); + return ResponsiveLayoutBuilder( small: (_, child) => SizedBox.square( key: const Key('sudoku_board_small'), @@ -78,12 +89,22 @@ class SudokuBoard extends StatelessWidget { ), if (isTimerPaused) Center( - child: FloatingActionButton.extended( - onPressed: () => context.read().add( - const TimerResumed(), - ), - label: Text(l10n.resumeTimerButtonText), - icon: const Icon(Icons.play_arrow), + child: DecoratedBox( + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(16), + ), + child: FloatingActionButton.extended( + backgroundColor: Colors.transparent, + onPressed: () => context.read().add( + const TimerResumed(), + ), + label: Text( + l10n.resumeTimerButtonText, + style: SudokuTextStyle.button, + ), + icon: const Icon(Icons.play_arrow), + ), ), ), ], diff --git a/lib/sudoku/widgets/sudoku_input.dart b/lib/sudoku/widgets/sudoku_input.dart index 9006b85..a45534f 100644 --- a/lib/sudoku/widgets/sudoku_input.dart +++ b/lib/sudoku/widgets/sudoku_input.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/colors/colors.dart'; import 'package:sudoku/layout/layout.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/sudoku/sudoku.dart'; import 'package:sudoku/typography/typography.dart'; @@ -54,6 +56,7 @@ class SudokuInput extends StatelessWidget { sudokuDimension: sudokuDimension, inputsPerRow: (sudokuDimension / 3).ceil(), inputSize: SudokuInputSize.large, + showEraserInSameLine: false, ), ), ); @@ -65,46 +68,71 @@ class _SudokuInputView extends StatelessWidget { required this.sudokuDimension, required this.inputsPerRow, required this.inputSize, + this.showEraserInSameLine = true, }); final int sudokuDimension; final int inputsPerRow; final double inputSize; + final bool showEraserInSameLine; @override Widget build(BuildContext context) { final theme = Theme.of(context); + final keySize = switch (inputSize) { SudokuInputSize.small => 'small', SudokuInputSize.medium => 'medium', SudokuInputSize.large => 'large', _ => 'other', }; + + final elementsCount = + showEraserInSameLine ? sudokuDimension + 1 : sudokuDimension; + return Stack( children: [ - for (var i = 0; i < sudokuDimension; i++) + for (var i = 0; i < elementsCount; i++) Positioned( top: (i ~/ inputsPerRow) * inputSize, left: (i % inputsPerRow) * inputSize, child: GestureDetector( - onTap: () => context.read().add( - SudokuInputTapped(i + 1), - ), + onTap: () => showEraserInSameLine && i == elementsCount - 1 + ? context.read().add(const SudokuInputErased()) + : context.read().add(SudokuInputEntered(i + 1)), child: Container( key: Key('sudoku_input_${keySize}_block_${i + 1}'), alignment: Alignment.center, - height: inputSize, - width: inputSize, + margin: const EdgeInsets.all(8), + height: inputSize - 16, + width: inputSize - 16, decoration: BoxDecoration( border: Border.all( - color: theme.dividerColor, - width: 0.8, + color: showEraserInSameLine && i == elementsCount - 1 + ? Colors.transparent + : theme.dividerColor, + width: 1.4, ), + gradient: showEraserInSameLine && i == elementsCount - 1 + ? const LinearGradient( + colors: [ + SudokuColors.darkPurple, + SudokuColors.darkPink, + ], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ) + : null, + borderRadius: BorderRadius.circular(14), ), child: Text( - '${i + 1}', + showEraserInSameLine && i == elementsCount - 1 + ? 'X' + : '${i + 1}', style: SudokuTextStyle.headline6.copyWith( - color: theme.colorScheme.secondary, + color: showEraserInSameLine && i == elementsCount - 1 + ? Colors.white + : theme.colorScheme.secondary, ), ), ), diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 3cf9f26..371d798 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -2,19 +2,27 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sudoku/api/api.dart'; import 'package:sudoku/app/app.dart'; import 'package:sudoku/home/home.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import '../../helpers/helpers.dart'; void main() { group('App', () { late SudokuAPI apiClient; + late PuzzleRepository puzzleRepository; setUp(() { apiClient = MockSudokuAPI(); + puzzleRepository = MockPuzzleRepository(); }); testWidgets('renders HomePage', (tester) async { - await tester.pumpWidget(App(apiClient: apiClient)); + await tester.pumpWidget( + App( + apiClient: apiClient, + puzzleRepository: puzzleRepository, + ), + ); expect(find.byType(HomePage), findsOneWidget); }); }); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index 0f1a6b6..a2b07f8 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -6,16 +6,10 @@ import 'package:sudoku/cache/cache.dart'; import 'package:sudoku/home/home.dart'; import 'package:sudoku/models/models.dart'; import 'package:sudoku/puzzle/puzzle.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; import 'package:sudoku/timer/timer.dart'; class MockSudoku extends Mock implements Sudoku {} -class MockSudokuBloc extends MockBloc - implements SudokuBloc {} - -class MockSudokuState extends Mock implements SudokuState {} - class MockBlock extends Mock implements Block {} class MockPosition extends Mock implements Position {} @@ -36,3 +30,8 @@ class MockCacheClient extends Mock implements CacheClient {} class MockPuzzle extends Mock implements Puzzle {} class MockPuzzleRepository extends Mock implements PuzzleRepository {} + +class MockPuzzleBloc extends MockBloc + implements PuzzleBloc {} + +class MockPuzzleState extends Mock implements PuzzleState {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index cc3f8c3..2ad2969 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:sudoku/api/api.dart'; import 'package:sudoku/home/home.dart'; import 'package:sudoku/l10n/l10n.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/timer/timer.dart'; import 'helpers.dart'; @@ -13,9 +13,10 @@ extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { SudokuAPI? apiClient, + PuzzleRepository? puzzleRepository, HomeBloc? homeBloc, - SudokuBloc? sudokuBloc, TimerBloc? timerBloc, + PuzzleBloc? puzzleBloc, }) { return pumpWidget( MultiRepositoryProvider( @@ -23,18 +24,21 @@ extension PumpApp on WidgetTester { RepositoryProvider.value( value: apiClient ?? MockSudokuAPI(), ), + RepositoryProvider.value( + value: puzzleRepository ?? MockPuzzleRepository(), + ), ], child: MultiBlocProvider( providers: [ BlocProvider.value( value: homeBloc ?? MockHomeBloc(), ), - BlocProvider.value( - value: sudokuBloc ?? MockSudokuBloc(), - ), BlocProvider.value( value: timerBloc ?? MockTimerBloc(), ), + BlocProvider.value( + value: puzzleBloc ?? MockPuzzleBloc(), + ), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/test/helpers/sudoku_helpers.dart b/test/helpers/sudoku_helpers.dart index 8c262a3..205734a 100644 --- a/test/helpers/sudoku_helpers.dart +++ b/test/helpers/sudoku_helpers.dart @@ -133,3 +133,436 @@ final sudoku2x2WithOneRemaining = sudoku2x2 .updateBlock(sudoku2x2Block12, 3) .updateBlock(sudoku2x2Block13, 2) .updateBlock(sudoku2x2Block15, 4); + +const sudoku3x3 = Sudoku( + blocks: [ + Block( + position: Position(x: 0, y: 0), + correctValue: 4, + currentValue: -1, + ), + Block( + position: Position(x: 0, y: 1), + correctValue: 7, + currentValue: -1, + ), + Block( + position: Position(x: 0, y: 2), + correctValue: 5, + currentValue: 5, + isGenerated: true, + ), + Block( + position: Position(x: 0, y: 3), + correctValue: 3, + currentValue: 3, + isGenerated: true, + ), + Block( + position: Position(x: 0, y: 4), + correctValue: 6, + currentValue: -1, + ), + Block( + position: Position(x: 0, y: 5), + correctValue: 9, + currentValue: -1, + ), + Block( + position: Position(x: 0, y: 6), + correctValue: 2, + currentValue: -1, + ), + Block( + position: Position(x: 0, y: 7), + correctValue: 1, + currentValue: -1, + ), + Block( + position: Position(x: 0, y: 8), + correctValue: 8, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 0), + correctValue: 8, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 1), + correctValue: 2, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 2), + correctValue: 9, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 3), + correctValue: 1, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 4), + correctValue: 7, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 5), + correctValue: 5, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 6), + correctValue: 3, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 7), + correctValue: 6, + currentValue: -1, + ), + Block( + position: Position(x: 1, y: 8), + correctValue: 6, + currentValue: 6, + isGenerated: true, + ), + Block( + position: Position(x: 2, y: 0), + correctValue: 3, + currentValue: -1, + ), + Block( + position: Position(x: 2, y: 1), + correctValue: 8, + currentValue: 8, + isGenerated: true, + ), + Block( + position: Position(x: 2, y: 2), + correctValue: 6, + currentValue: -1, + ), + Block( + position: Position(x: 2, y: 3), + correctValue: 7, + currentValue: -1, + ), + Block( + position: Position(x: 2, y: 4), + correctValue: 2, + currentValue: -1, + ), + Block( + position: Position(x: 2, y: 5), + correctValue: 1, + currentValue: 1, + isGenerated: true, + ), + Block( + position: Position(x: 2, y: 6), + correctValue: 5, + currentValue: -1, + ), + Block( + position: Position(x: 2, y: 7), + correctValue: 4, + currentValue: 4, + isGenerated: true, + ), + Block( + position: Position(x: 2, y: 8), + correctValue: 9, + currentValue: -1, + ), + Block( + position: Position(x: 3, y: 0), + correctValue: 1, + currentValue: 1, + isGenerated: true, + ), + Block( + position: Position(x: 3, y: 1), + correctValue: 5, + currentValue: -1, + ), + Block( + position: Position(x: 3, y: 2), + correctValue: 7, + currentValue: -1, + ), + Block( + position: Position(x: 3, y: 3), + correctValue: 2, + currentValue: 2, + isGenerated: true, + ), + Block( + position: Position(x: 3, y: 4), + correctValue: 8, + currentValue: -1, + ), + Block( + position: Position(x: 3, y: 5), + correctValue: 3, + currentValue: 3, + isGenerated: true, + ), + Block( + position: Position(x: 3, y: 6), + correctValue: 4, + currentValue: -1, + ), + Block( + position: Position(x: 3, y: 7), + correctValue: 9, + currentValue: -1, + ), + Block( + position: Position(x: 3, y: 8), + correctValue: 6, + currentValue: -1, + ), + Block( + position: Position(x: 4, y: 0), + correctValue: 9, + currentValue: 9, + isGenerated: true, + ), + Block( + position: Position(x: 4, y: 1), + correctValue: 6, + currentValue: -1, + ), + Block( + position: Position(x: 4, y: 2), + correctValue: 2, + currentValue: -1, + ), + Block( + position: Position(x: 4, y: 3), + correctValue: 5, + currentValue: -1, + ), + Block( + position: Position(x: 4, y: 4), + correctValue: 4, + currentValue: -1, + ), + Block( + position: Position(x: 4, y: 5), + correctValue: 7, + currentValue: -1, + ), + Block( + position: Position(x: 4, y: 6), + correctValue: 1, + currentValue: -1, + ), + Block( + position: Position(x: 4, y: 7), + correctValue: 8, + currentValue: 8, + isGenerated: true, + ), + Block( + position: Position(x: 4, y: 8), + correctValue: 3, + currentValue: -1, + ), + Block( + position: Position(x: 5, y: 0), + correctValue: 3, + currentValue: 3, + isGenerated: true, + ), + Block( + position: Position(x: 5, y: 1), + correctValue: 4, + currentValue: -1, + ), + Block( + position: Position(x: 5, y: 2), + correctValue: 1, + currentValue: -1, + ), + Block( + position: Position(x: 5, y: 3), + correctValue: 8, + currentValue: -1, + ), + Block( + position: Position(x: 5, y: 4), + correctValue: 9, + currentValue: -1, + ), + Block( + position: Position(x: 5, y: 5), + correctValue: 6, + currentValue: -1, + ), + Block( + position: Position(x: 5, y: 6), + correctValue: 7, + currentValue: -1, + ), + Block( + position: Position(x: 5, y: 7), + correctValue: 2, + currentValue: 2, + isGenerated: true, + ), + Block( + position: Position(x: 5, y: 8), + correctValue: 5, + currentValue: 5, + isGenerated: true, + ), + Block( + position: Position(x: 6, y: 0), + correctValue: 7, + currentValue: 7, + isGenerated: true, + ), + Block( + position: Position(x: 6, y: 1), + correctValue: 1, + currentValue: -1, + ), + Block( + position: Position(x: 6, y: 2), + correctValue: 8, + currentValue: -1, + ), + Block( + position: Position(x: 6, y: 3), + correctValue: 4, + currentValue: 4, + isGenerated: true, + ), + Block( + position: Position(x: 6, y: 4), + correctValue: 6, + currentValue: -1, + ), + Block( + position: Position(x: 6, y: 5), + correctValue: 2, + currentValue: -1, + ), + Block( + position: Position(x: 6, y: 6), + correctValue: 5, + currentValue: -1, + ), + Block( + position: Position(x: 6, y: 7), + correctValue: 3, + currentValue: -1, + ), + Block( + position: Position(x: 6, y: 8), + correctValue: 3, + currentValue: 3, + isGenerated: true, + ), + Block( + position: Position(x: 7, y: 0), + correctValue: 2, + currentValue: 2, + isGenerated: true, + ), + Block( + position: Position(x: 7, y: 1), + correctValue: 9, + currentValue: -1, + ), + Block( + position: Position(x: 7, y: 2), + correctValue: 4, + currentValue: -1, + ), + Block( + position: Position(x: 7, y: 3), + correctValue: 6, + currentValue: -1, + ), + Block( + position: Position(x: 7, y: 4), + correctValue: 5, + currentValue: 5, + isGenerated: true, + ), + Block( + position: Position(x: 7, y: 5), + correctValue: 8, + currentValue: -1, + ), + Block( + position: Position(x: 7, y: 6), + correctValue: 3, + currentValue: -1, + ), + Block( + position: Position(x: 7, y: 7), + correctValue: 1, + currentValue: 1, + isGenerated: true, + ), + Block( + position: Position(x: 7, y: 8), + correctValue: 7, + currentValue: -1, + ), + Block( + position: Position(x: 8, y: 0), + correctValue: 6, + currentValue: 6, + isGenerated: true, + ), + Block( + position: Position(x: 8, y: 1), + correctValue: 3, + currentValue: -1, + ), + Block( + position: Position(x: 8, y: 2), + correctValue: 3, + currentValue: 3, + isGenerated: true, + ), + Block( + position: Position(x: 8, y: 3), + correctValue: 9, + currentValue: -1, + ), + Block( + position: Position(x: 8, y: 4), + correctValue: 1, + currentValue: -1, + ), + Block( + position: Position(x: 8, y: 5), + correctValue: 4, + currentValue: -1, + ), + Block( + position: Position(x: 8, y: 6), + correctValue: 9, + currentValue: 9, + isGenerated: true, + ), + Block( + position: Position(x: 8, y: 7), + correctValue: 7, + currentValue: -1, + ), + Block( + position: Position(x: 8, y: 8), + correctValue: 2, + currentValue: -1, + ), + ], +); diff --git a/test/home/bloc/home_bloc_test.dart b/test/home/bloc/home_bloc_test.dart index 70e20bf..7f5abb4 100644 --- a/test/home/bloc/home_bloc_test.dart +++ b/test/home/bloc/home_bloc_test.dart @@ -6,12 +6,14 @@ import 'package:mocktail/mocktail.dart'; import 'package:sudoku/api/api.dart'; import 'package:sudoku/home/home.dart'; import 'package:sudoku/models/models.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import '../../helpers/helpers.dart'; void main() { group('HomeBloc', () { late SudokuAPI apiClient; + late PuzzleRepository puzzleRepository; const sudoku = Sudoku( blocks: [ @@ -25,6 +27,7 @@ void main() { setUp(() { apiClient = MockSudokuAPI(); + puzzleRepository = MockPuzzleRepository(); when(() => apiClient.createSudoku(difficulty: any(named: 'difficulty'))) .thenAnswer((_) => Future.value(sudoku)); }); @@ -34,7 +37,10 @@ void main() { }); HomeBloc buildBloc() { - return HomeBloc(apiClient: apiClient); + return HomeBloc( + apiClient: apiClient, + puzzleRepository: puzzleRepository, + ); } test('constructor works correctly', () { @@ -43,26 +49,31 @@ void main() { blocTest( 'emits state with in progress and completed [SudokuCreationStatus] ' - 'along with created sudoku with defined difficulty', + 'along with defined difficulty', build: buildBloc, act: (bloc) => bloc.add(SudokuCreationRequested(Difficulty.medium)), expect: () => [ HomeState( - sudoku: null, difficulty: Difficulty.medium, sudokuCreationStatus: SudokuCreationStatus.inProgress, sudokuCreationError: null, ), HomeState( - sudoku: sudoku, difficulty: Difficulty.medium, sudokuCreationStatus: SudokuCreationStatus.completed, sudokuCreationError: null, ), ], - verify: (_) => verify( - () => apiClient.createSudoku(difficulty: Difficulty.medium), - ).called(1), + verify: (_) { + verify( + () => apiClient.createSudoku(difficulty: Difficulty.medium), + ).called(1); + verify( + () => puzzleRepository.storePuzzle( + puzzle: Puzzle(sudoku: sudoku, difficulty: Difficulty.medium), + ), + ).called(1); + }, ); blocTest( @@ -75,13 +86,11 @@ void main() { act: (bloc) => bloc.add(SudokuCreationRequested(Difficulty.medium)), expect: () => [ HomeState( - sudoku: null, difficulty: Difficulty.medium, sudokuCreationStatus: SudokuCreationStatus.inProgress, sudokuCreationError: null, ), HomeState( - sudoku: null, difficulty: Difficulty.medium, sudokuCreationStatus: SudokuCreationStatus.failed, sudokuCreationError: SudokuCreationErrorType.apiClient, @@ -99,13 +108,11 @@ void main() { act: (bloc) => bloc.add(SudokuCreationRequested(Difficulty.medium)), expect: () => [ HomeState( - sudoku: null, difficulty: Difficulty.medium, sudokuCreationStatus: SudokuCreationStatus.inProgress, sudokuCreationError: null, ), HomeState( - sudoku: null, difficulty: Difficulty.medium, sudokuCreationStatus: SudokuCreationStatus.failed, sudokuCreationError: SudokuCreationErrorType.invalidRawData, @@ -123,13 +130,11 @@ void main() { act: (bloc) => bloc.add(SudokuCreationRequested(Difficulty.medium)), expect: () => [ HomeState( - sudoku: null, difficulty: Difficulty.medium, sudokuCreationStatus: SudokuCreationStatus.inProgress, sudokuCreationError: null, ), HomeState( - sudoku: null, difficulty: Difficulty.medium, sudokuCreationStatus: SudokuCreationStatus.failed, sudokuCreationError: SudokuCreationErrorType.unexpected, diff --git a/test/home/bloc/home_state_test.dart b/test/home/bloc/home_state_test.dart index a30b435..85d8f9f 100644 --- a/test/home/bloc/home_state_test.dart +++ b/test/home/bloc/home_state_test.dart @@ -7,13 +7,11 @@ import 'package:sudoku/models/models.dart'; void main() { group('HomeState', () { HomeState createSubject({ - Sudoku? sudoku, Difficulty? difficulty, SudokuCreationStatus? sudokuCreationStatus, SudokuCreationErrorType? sudokuCreationError, }) { return HomeState( - sudoku: sudoku, difficulty: difficulty, sudokuCreationStatus: sudokuCreationStatus ?? SudokuCreationStatus.initial, @@ -30,7 +28,6 @@ void main() { createSubject().props, equals( [ - null, null, SudokuCreationStatus.initial, null, @@ -47,7 +44,6 @@ void main() { test('returns the old value for each parameter if null is provided', () { expect( createSubject().copyWith( - sudoku: null, difficulty: null, sudokuCreationStatus: null, sudokuCreationError: null, @@ -59,14 +55,12 @@ void main() { test('returns the updated copy of this for every non-null parameter', () { expect( createSubject().copyWith( - sudoku: () => Sudoku(blocks: const []), difficulty: () => Difficulty.expert, sudokuCreationStatus: () => SudokuCreationStatus.inProgress, sudokuCreationError: () => SudokuCreationErrorType.unexpected, ), equals( createSubject( - sudoku: Sudoku(blocks: const []), difficulty: Difficulty.expert, sudokuCreationStatus: SudokuCreationStatus.inProgress, sudokuCreationError: SudokuCreationErrorType.unexpected, @@ -79,13 +73,11 @@ void main() { test('can copyWith null parameters', () { expect( createSubject().copyWith( - sudoku: () => null, difficulty: () => null, sudokuCreationError: () => null, ), equals( createSubject( - sudoku: null, difficulty: null, sudokuCreationError: null, ), diff --git a/test/home/home_page_test.dart b/test/home/view/home_page_test.dart similarity index 94% rename from test/home/home_page_test.dart rename to test/home/view/home_page_test.dart index 8ee5a79..860c504 100644 --- a/test/home/home_page_test.dart +++ b/test/home/view/home_page_test.dart @@ -6,10 +6,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:sudoku/home/home.dart'; import 'package:sudoku/models/models.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/widgets/widgets.dart'; -import '../helpers/helpers.dart'; +import '../../helpers/helpers.dart'; void main() { group('HomePage', () { @@ -120,8 +120,16 @@ void main() { ); testWidgets( - 'routes to [SudokuPage] when [SudokuCreationStatus] is completed', + 'routes to [PuzzlePage] when [SudokuCreationStatus] is completed', (tester) async { + final puzzle = MockPuzzle(); + when(() => puzzle.sudoku).thenReturn(sudoku3x3); + when(() => puzzle.difficulty).thenReturn(Difficulty.medium); + when(() => puzzle.remainingMistakes).thenReturn(3); + + final puzzleRepository = MockPuzzleRepository(); + when(puzzleRepository.getPuzzle).thenReturn(puzzle); + whenListen( homeBloc, Stream.fromIterable( @@ -131,7 +139,6 @@ void main() { sudokuCreationStatus: SudokuCreationStatus.inProgress, ), HomeState( - sudoku: Sudoku(blocks: const []), sudokuCreationStatus: SudokuCreationStatus.completed, ), ], @@ -139,10 +146,14 @@ void main() { initialState: HomeState(), ); - await tester.pumpApp(HomeView(), homeBloc: homeBloc); + await tester.pumpApp( + HomeView(), + homeBloc: homeBloc, + puzzleRepository: puzzleRepository, + ); await tester.pumpAndSettle(); - expect(find.byType(SudokuPage), findsOneWidget); + expect(find.byType(PuzzlePage), findsOneWidget); }, ); diff --git a/test/puzzle/view/puzzle_page_test.dart b/test/puzzle/view/puzzle_page_test.dart new file mode 100644 index 0000000..9c0a577 --- /dev/null +++ b/test/puzzle/view/puzzle_page_test.dart @@ -0,0 +1,314 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sudoku/models/models.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; +import 'package:sudoku/sudoku/sudoku.dart'; +import 'package:sudoku/timer/timer.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('PuzzlePage', () { + late Puzzle puzzle; + late PuzzleRepository puzzleRepository; + + setUp(() { + puzzle = MockPuzzle(); + puzzleRepository = MockPuzzleRepository(); + + when(() => puzzle.sudoku).thenReturn(sudoku3x3); + when(() => puzzle.difficulty).thenReturn(Difficulty.medium); + + when(() => puzzleRepository.getPuzzle()).thenReturn(puzzle); + }); + + testWidgets('renders PuzzleView on a large display', (tester) async { + tester.setLargeDisplaySize(); + + await tester.pumpApp( + PuzzlePage(), + puzzleRepository: puzzleRepository, + ); + expect(find.byType(PuzzleView), findsOneWidget); + }); + + testWidgets('renders PuzzleView on a medium display', (tester) async { + tester.setMediumDisplaySize(); + + await tester.pumpApp( + PuzzlePage(), + puzzleRepository: puzzleRepository, + ); + expect(find.byType(PuzzleView), findsOneWidget); + }); + + testWidgets('renders PuzzleView on a small display', (tester) async { + tester.setSmallDisplaySize(); + + await tester.pumpApp( + PuzzlePage(), + puzzleRepository: puzzleRepository, + ); + expect(find.byType(PuzzleView), findsOneWidget); + }); + + group('PageHeader', () { + late Puzzle puzzle; + late PuzzleBloc puzzleBloc; + late PuzzleState puzzleState; + + setUp(() { + puzzle = MockPuzzle(); + puzzleBloc = MockPuzzleBloc(); + puzzleState = MockPuzzleState(); + + when(() => puzzle.difficulty).thenReturn(Difficulty.medium); + when(() => puzzleState.puzzle).thenReturn(puzzle); + when(() => puzzleBloc.state).thenReturn(puzzleState); + }); + + testWidgets('renders difficulty on a large layout', (tester) async { + tester.setLargeDisplaySize(); + await tester.pumpApp(const PageHeader(), puzzleBloc: puzzleBloc); + expect(find.text('Medium'), findsOneWidget); + }); + + testWidgets('renders difficulty on a medium layout', (tester) async { + tester.setMediumDisplaySize(); + await tester.pumpApp(const PageHeader(), puzzleBloc: puzzleBloc); + expect(find.text('Medium'), findsOneWidget); + }); + + testWidgets('renders difficulty on a small layout', (tester) async { + tester.setSmallDisplaySize(); + await tester.pumpApp(const PageHeader(), puzzleBloc: puzzleBloc); + expect(find.text('Medium'), findsOneWidget); + }); + }); + + group('PuzzleViewLayout', () { + late Sudoku sudoku; + late Puzzle puzzle; + late PuzzleState puzzleState; + late PuzzleBloc puzzleBloc; + late TimerBloc timerBloc; + + setUp(() { + sudoku = MockSudoku(); + puzzle = MockPuzzle(); + puzzleState = MockPuzzleState(); + + puzzleBloc = MockPuzzleBloc(); + timerBloc = MockTimerBloc(); + + when(() => sudoku.blocks).thenReturn([]); + when(() => sudoku.getDimesion()).thenReturn(3); + + when(() => puzzle.sudoku).thenReturn(sudoku); + when(() => puzzle.difficulty).thenReturn(Difficulty.medium); + when(() => puzzle.remainingMistakes).thenReturn(3); + + when(() => puzzleState.puzzle).thenReturn(puzzle); + when(() => puzzleBloc.state).thenReturn(puzzleState); + + when(() => timerBloc.state).thenReturn( + TimerState(secondsElapsed: 1, isRunning: true), + ); + }); + + testWidgets( + 'renders [PageHeader], [SudokuBoardView], [MistakesCountView], ' + '[SudokuTimer], [SudokuInputView], [InputEraseViewForLargeLayout] ' + 'on a large display', + (tester) async { + tester.setLargeDisplaySize(); + + await tester.pumpApp( + PuzzleViewLayout(), + puzzleBloc: puzzleBloc, + timerBloc: timerBloc, + ); + + expect(find.byType(PageHeader), findsOneWidget); + expect(find.byType(SudokuBoardView), findsOneWidget); + expect(find.byType(MistakesCountView), findsOneWidget); + + expect(find.byType(SudokuTimer), findsOneWidget); + expect(find.byType(SudokuInputView), findsOneWidget); + expect(find.byType(InputEraseViewForLargeLayout), findsOneWidget); + }, + ); + + testWidgets( + 'renders [PageHeader], [SudokuBoardView], [MistakesCountView], ' + '[SudokuTimer], and [SudokuInputView] on a medium display', + (tester) async { + tester.setMediumDisplaySize(); + + await tester.pumpApp( + PuzzleViewLayout(), + puzzleBloc: puzzleBloc, + timerBloc: timerBloc, + ); + + expect(find.byType(PageHeader), findsOneWidget); + expect(find.byType(SudokuBoardView), findsOneWidget); + expect(find.byType(MistakesCountView), findsOneWidget); + + expect(find.byType(SudokuTimer), findsOneWidget); + expect(find.byType(SudokuInputView), findsOneWidget); + expect(find.byType(InputEraseViewForLargeLayout), findsNothing); + }, + ); + + testWidgets( + 'renders [PageHeader], [SudokuBoardView], [MistakesCountView], ' + '[SudokuTimer], and [SudokuInputView] on a small display', + (tester) async { + tester.setSmallDisplaySize(); + + await tester.pumpApp( + PuzzleViewLayout(), + puzzleBloc: puzzleBloc, + timerBloc: timerBloc, + ); + + expect(find.byType(PageHeader), findsOneWidget); + expect(find.byType(SudokuBoardView), findsOneWidget); + expect(find.byType(MistakesCountView), findsOneWidget); + + expect(find.byType(SudokuTimer), findsOneWidget); + expect(find.byType(SudokuInputView), findsOneWidget); + expect(find.byType(InputEraseViewForLargeLayout), findsNothing); + }, + ); + }); + + group('InputEraseViewForLargeLayout', () { + late PuzzleBloc puzzleBloc; + + setUp(() { + puzzleBloc = MockPuzzleBloc(); + }); + + testWidgets('adds [SudokuInputErased] when tapped', (tester) async { + await tester.pumpApp( + InputEraseViewForLargeLayout(), + puzzleBloc: puzzleBloc, + ); + await tester.tap(find.byType(GestureDetector)); + await tester.pumpAndSettle(); + + verify(() => puzzleBloc.add(SudokuInputErased())).called(1); + }); + }); + + group('SudokuBoardView', () { + late PuzzleBloc puzzleBloc; + late TimerBloc timerBloc; + late Puzzle puzzle; + late Sudoku sudoku; + + setUp(() { + puzzleBloc = MockPuzzleBloc(); + timerBloc = MockTimerBloc(); + puzzle = MockPuzzle(); + sudoku = MockSudoku(); + + when(() => sudoku.getDimesion()).thenReturn(3); + when(() => sudoku.blocks).thenReturn([]); + when(() => puzzle.sudoku).thenReturn(sudoku); + + when(() => puzzleBloc.state).thenReturn( + PuzzleState(puzzle: puzzle), + ); + + when(() => timerBloc.state).thenReturn( + TimerState(secondsElapsed: 1, isRunning: true), + ); + }); + + testWidgets('renders on a large layout', (tester) async { + tester.setLargeDisplaySize(); + + await tester.pumpApp( + const SudokuBoardView(), + puzzleBloc: puzzleBloc, + timerBloc: timerBloc, + ); + + expect(find.byType(SudokuBoard), findsOneWidget); + }); + + testWidgets('renders on a medium layout', (tester) async { + tester.setMediumDisplaySize(); + + await tester.pumpApp( + const SudokuBoardView(), + puzzleBloc: puzzleBloc, + timerBloc: timerBloc, + ); + + expect(find.byType(SudokuBoard), findsOneWidget); + }); + + testWidgets('renders on a small layout', (tester) async { + tester.setSmallDisplaySize(); + + await tester.pumpApp( + const SudokuBoardView(), + puzzleBloc: puzzleBloc, + timerBloc: timerBloc, + ); + + expect(find.byType(SudokuBoard), findsOneWidget); + }); + + testWidgets( + 're-renders only when sudoku from [PuzzleState] changes', + (tester) async { + final block1 = Block( + position: Position(x: 0, y: 0), + correctValue: 1, + currentValue: 1, + ); + final block2 = Block( + position: Position(x: 0, y: 1), + correctValue: 2, + currentValue: 2, + ); + + when(() => puzzleBloc.stream).thenAnswer( + (_) => Stream.fromIterable([ + PuzzleState( + puzzle: Puzzle( + sudoku: Sudoku(blocks: const []), + difficulty: Difficulty.medium, + ), + ), + PuzzleState( + puzzle: Puzzle( + sudoku: Sudoku(blocks: [block1, block2]), + difficulty: Difficulty.medium, + ), + ), + ]), + ); + + await tester.pumpApp( + const SudokuBoardView(), + puzzleBloc: puzzleBloc, + timerBloc: timerBloc, + ); + + await tester.pumpAndSettle(); + expect(find.byKey(Key('sudoku_board_view_0_0')), findsOneWidget); + expect(find.byKey(Key('sudoku_board_view_0_1')), findsOneWidget); + }, + ); + }); + }); +} diff --git a/test/sudoku/bloc/sudoku_bloc_test.dart b/test/sudoku/bloc/sudoku_bloc_test.dart deleted file mode 100644 index d6c30de..0000000 --- a/test/sudoku/bloc/sudoku_bloc_test.dart +++ /dev/null @@ -1,261 +0,0 @@ -// ignore_for_file: prefer_const_constructors, avoid_redundant_argument_values - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:sudoku/models/models.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; - -void main() { - const sudoku2x2Block0 = Block( - position: Position(x: 0, y: 0), - correctValue: 4, - currentValue: -1, - ); - - const sudoku2x2Block1 = Block( - position: Position(x: 0, y: 1), - correctValue: 1, - currentValue: 1, - isGenerated: true, - ); - - const sudoku2x2Block2 = Block( - position: Position(x: 0, y: 2), - correctValue: 2, - currentValue: -1, - ); - - const sudoku2x2Block3 = Block( - position: Position(x: 0, y: 3), - correctValue: 3, - currentValue: -1, - ); - - const sudoku2x2Block4 = Block( - position: Position(x: 1, y: 0), - correctValue: 2, - currentValue: 2, - isGenerated: true, - ); - - const sudoku2x2Block5 = Block( - position: Position(x: 1, y: 1), - correctValue: 3, - currentValue: 3, - isGenerated: true, - ); - - const sudoku2x2Block6 = Block( - position: Position(x: 1, y: 2), - correctValue: 4, - currentValue: -1, - ); - - const sudoku2x2Block7 = Block( - position: Position(x: 1, y: 3), - correctValue: 1, - currentValue: -1, - ); - - const sudoku2x2Block8 = Block( - position: Position(x: 2, y: 0), - correctValue: 1, - currentValue: -1, - ); - - const sudoku2x2Block9 = Block( - position: Position(x: 2, y: 1), - correctValue: 4, - currentValue: -1, - ); - - const sudoku2x2Block10 = Block( - position: Position(x: 2, y: 2), - correctValue: 3, - currentValue: 3, - isGenerated: true, - ); - - const sudoku2x2Block11 = Block( - position: Position(x: 2, y: 3), - correctValue: 2, - currentValue: 2, - isGenerated: true, - ); - - const sudoku2x2Block12 = Block( - position: Position(x: 3, y: 0), - correctValue: 3, - currentValue: -1, - ); - - const sudoku2x2Block13 = Block( - position: Position(x: 3, y: 1), - correctValue: 2, - currentValue: -1, - ); - - const sudoku2x2Block14 = Block( - position: Position(x: 3, y: 2), - correctValue: 1, - currentValue: 1, - isGenerated: true, - ); - - const sudoku2x2Block15 = Block( - position: Position(x: 3, y: 3), - correctValue: 4, - currentValue: -1, - ); - - const sudoku = Sudoku( - blocks: [ - sudoku2x2Block0, - sudoku2x2Block1, - sudoku2x2Block2, - sudoku2x2Block3, - sudoku2x2Block4, - sudoku2x2Block5, - sudoku2x2Block6, - sudoku2x2Block7, - sudoku2x2Block8, - sudoku2x2Block9, - sudoku2x2Block10, - sudoku2x2Block11, - sudoku2x2Block12, - sudoku2x2Block13, - sudoku2x2Block14, - sudoku2x2Block15, - ], - ); - - const lastLeftSudokuGenerated = [ - [-1, 1, 2, 3], - [2, 3, 4, 1], - [1, 4, 3, 2], - [3, 2, 1, 4], - ]; - - const lastLeftSudokuAnswer = [ - [4, 1, 2, 3], - [2, 3, 4, 1], - [1, 4, 3, 2], - [3, 2, 1, 4], - ]; - - final lastLeftSudoku = Sudoku.fromRawData( - lastLeftSudokuGenerated, - lastLeftSudokuAnswer, - ); - - group('SudokuBlock', () { - SudokuBloc buildBloc() { - return SudokuBloc(sudoku: sudoku); - } - - group('constructor', () { - test('works correctly', () { - expect(buildBloc, returnsNormally); - }); - - test('has an initial state', () { - expect( - buildBloc().state, - equals(SudokuState(sudoku: sudoku)), - ); - }); - }); - - group('SudokuBlockSelected', () { - blocTest( - 'emits state with [cannotBeSelected] [blockSelectionStatus], ' - 'and null [selectedBlock] when block [isGenerated] is true', - build: buildBloc, - act: (bloc) => bloc.add(SudokuBlockSelected(sudoku2x2Block1)), - expect: () => [ - SudokuState( - sudoku: sudoku, - blockSelectionStatus: BlockSelectionStatus.cannotBeSelected, - highlightedBlocks: const {}, - currentSelectedBlock: null, - ), - ], - ); - - blocTest( - 'emits state with selected [blockSelectionStatus], ' - 'and correct [selectedBlock] when block [isGenerated] is false', - build: buildBloc, - act: (bloc) => bloc.add(SudokuBlockSelected(sudoku2x2Block0)), - expect: () => [ - SudokuState( - sudoku: sudoku, - blockSelectionStatus: BlockSelectionStatus.selected, - highlightedBlocks: { - sudoku2x2Block0, - sudoku2x2Block1, - sudoku2x2Block2, - sudoku2x2Block3, - sudoku2x2Block4, - sudoku2x2Block5, - sudoku2x2Block8, - sudoku2x2Block12, - }, - currentSelectedBlock: sudoku2x2Block0, - ), - ], - ); - }); - - group('SudokuInputTapped', () { - final newSudoku = sudoku.updateBlock(sudoku2x2Block0, 7); - final newLastLeftSudoku = lastLeftSudoku.updateBlock(sudoku2x2Block0, 4); - - blocTest( - 'does not emit state if state [currentSelectedBlock] is emoty', - build: buildBloc, - seed: () => SudokuState(sudoku: sudoku, currentSelectedBlock: null), - act: (bloc) => bloc.add(SudokuInputTapped(7)), - expect: () => [], - ); - - blocTest( - 'emits updated sudoku when [isComplete] is false', - build: buildBloc, - seed: () => SudokuState( - sudoku: sudoku, - blockSelectionStatus: BlockSelectionStatus.selected, - currentSelectedBlock: sudoku2x2Block0, - ), - act: (bloc) => bloc.add(SudokuInputTapped(7)), - expect: () => [ - SudokuState( - sudoku: newSudoku, - blockSelectionStatus: BlockSelectionStatus.selected, - currentSelectedBlock: sudoku2x2Block0, - ), - ], - ); - - blocTest( - 'emits updated sudoku, and puzzle status when [isComplete] ' - 'returns true', - build: buildBloc, - seed: () => SudokuState( - sudoku: lastLeftSudoku, - blockSelectionStatus: BlockSelectionStatus.selected, - currentSelectedBlock: sudoku2x2Block0, - ), - act: (bloc) => bloc.add(SudokuInputTapped(4)), - expect: () => [ - SudokuState( - sudoku: newLastLeftSudoku, - puzzleStatus: SudokuPuzzleStatus.complete, - blockSelectionStatus: BlockSelectionStatus.selected, - currentSelectedBlock: sudoku2x2Block0, - ), - ], - ); - }); - }); -} diff --git a/test/sudoku/bloc/sudoku_event_test.dart b/test/sudoku/bloc/sudoku_event_test.dart deleted file mode 100644 index 486b619..0000000 --- a/test/sudoku/bloc/sudoku_event_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter_test/flutter_test.dart'; -import 'package:sudoku/models/models.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; - -void main() { - const mockBlock = Block( - position: Position(x: 0, y: 2), - correctValue: 3, - currentValue: -1, - ); - - group('SudokuEvent', () { - group('SudokuBlockSelected', () { - test('supports value equality', () { - expect( - SudokuBlockSelected(mockBlock), - equals(SudokuBlockSelected(mockBlock)), - ); - }); - - test('props are correct', () { - expect( - SudokuBlockSelected(mockBlock).props, - equals([mockBlock]), - ); - }); - }); - - group('SudokuInputTapped', () { - test('supports value equality', () { - expect( - SudokuInputTapped(2), - equals(SudokuInputTapped(2)), - ); - }); - - test('props are correct', () { - expect( - SudokuInputTapped(5).props, - equals([5]), - ); - }); - }); - }); -} diff --git a/test/sudoku/bloc/sudoku_state_test.dart b/test/sudoku/bloc/sudoku_state_test.dart deleted file mode 100644 index f4c74fb..0000000 --- a/test/sudoku/bloc/sudoku_state_test.dart +++ /dev/null @@ -1,118 +0,0 @@ -// ignore_for_file: avoid_redundant_argument_values - -import 'package:flutter_test/flutter_test.dart'; -import 'package:sudoku/models/models.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; - -void main() { - const rawData = [ - [4, 1, 2, 3], - [2, 3, 4, 1], - [1, 4, 3, 2], - [3, 2, 1, 4], - ]; - - const dummyBlock1 = Block( - position: Position(x: 1, y: 2), - correctValue: 1, - currentValue: 1, - ); - - const dummyBlock2 = Block( - position: Position(x: 2, y: 1), - correctValue: 7, - currentValue: 7, - ); - - group('SudokuState', () { - SudokuState createSubject({ - Sudoku? sudoku, - SudokuPuzzleStatus? puzzleStatus, - BlockSelectionStatus? blockSelectionStatus, - Set? highlightedBlocks, - Block? currentSelectedBlock, - }) { - return SudokuState( - sudoku: sudoku ?? const Sudoku(blocks: []), - puzzleStatus: puzzleStatus ?? SudokuPuzzleStatus.incomplete, - blockSelectionStatus: - blockSelectionStatus ?? BlockSelectionStatus.nothingSelected, - highlightedBlocks: highlightedBlocks ?? {}, - currentSelectedBlock: currentSelectedBlock, - ); - } - - test('constructor works correctly', () { - expect(createSubject, returnsNormally); - }); - - test('supports value equality', () { - expect(createSubject(), equals(createSubject())); - }); - - test('props are correct', () { - expect( - createSubject().props, - equals([ - const Sudoku(blocks: []), // sudoku - SudokuPuzzleStatus.incomplete, // puzzleStatus - BlockSelectionStatus.nothingSelected, // blockSelectionStatus - {}, //highlightedBlocks - null, // currentSelectedBlock - ]), - ); - }); - - group('copyWith', () { - test('returns same object if no argument is passed', () { - expect( - createSubject().copyWith(), - equals(createSubject()), - ); - }); - - test('returns the old value for each parameter if null is provided', () { - expect( - createSubject().copyWith( - sudoku: null, - puzzleStatus: null, - blockSelectionStatus: null, - highlightedBlocks: null, - currentSelectedBlock: null, - ), - equals(createSubject()), - ); - }); - - test('returns the updated copy of this for every non-null parameter', () { - expect( - createSubject().copyWith( - sudoku: () => Sudoku.fromRawData(rawData, rawData), - puzzleStatus: () => SudokuPuzzleStatus.complete, - blockSelectionStatus: () => BlockSelectionStatus.selected, - highlightedBlocks: () => {dummyBlock1, dummyBlock2}, - currentSelectedBlock: () => dummyBlock1, - ), - equals( - createSubject( - sudoku: Sudoku.fromRawData(rawData, rawData), - puzzleStatus: SudokuPuzzleStatus.complete, - blockSelectionStatus: BlockSelectionStatus.selected, - highlightedBlocks: {dummyBlock1, dummyBlock2}, - currentSelectedBlock: dummyBlock1, - ), - ), - ); - }); - }); - - test('can copyWith null currentSelectedBlock', () { - expect( - createSubject().copyWith( - currentSelectedBlock: () => null, - ), - equals(createSubject(currentSelectedBlock: null)), - ); - }); - }); -} diff --git a/test/sudoku/view/sudoku_page_test.dart b/test/sudoku/view/sudoku_page_test.dart deleted file mode 100644 index 88eb226..0000000 --- a/test/sudoku/view/sudoku_page_test.dart +++ /dev/null @@ -1,141 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:sudoku/layout/responsive_layout_builder.dart'; -import 'package:sudoku/models/models.dart'; -import 'package:sudoku/sudoku/sudoku.dart'; -import 'package:sudoku/timer/timer.dart'; - -import '../../helpers/helpers.dart'; - -void main() { - group('SudokuPage', () { - late Sudoku sudoku; - - const block = Block( - position: Position(x: 0, y: 0), - correctValue: 7, - currentValue: -1, - ); - - setUp(() { - sudoku = MockSudoku(); - when(() => sudoku.blocks).thenReturn([block]); - when(() => sudoku.getDimesion()).thenReturn(3); - }); - - testWidgets('renders SudokuView', (tester) async { - await tester.pumpApp(SudokuPage(sudoku: sudoku)); - expect(find.byType(SudokuView), findsOneWidget); - }); - }); - - group('SudokuView', () { - late SudokuBloc sudokuBloc; - late TimerBloc timerBloc; - late Sudoku sudoku; - - setUp(() { - sudokuBloc = MockSudokuBloc(); - timerBloc = MockTimerBloc(); - sudoku = MockSudoku(); - - when(() => sudoku.getDimesion()).thenReturn(3); - when(() => sudoku.blocks).thenReturn([]); - when(() => sudokuBloc.state).thenReturn( - SudokuState(sudoku: sudoku), - ); - - when(() => timerBloc.state).thenReturn(TimerState()); - }); - - testWidgets('renders appbar in small layout', (tester) async { - tester.setSmallDisplaySize(); - await tester.pumpApp( - const SudokuView(), - sudokuBloc: sudokuBloc, - timerBloc: timerBloc, - ); - expect(find.byType(AppBar), findsOneWidget); - }); - - testWidgets('renders appbar in medium layout', (tester) async { - tester.setMediumDisplaySize(); - await tester.pumpApp( - const SudokuView(), - sudokuBloc: sudokuBloc, - timerBloc: timerBloc, - ); - expect(find.byType(AppBar), findsOneWidget); - }); - - testWidgets('does not render appbar in large layout', (tester) async { - tester.setLargeDisplaySize(); - await tester.pumpApp( - const SudokuView(), - sudokuBloc: sudokuBloc, - timerBloc: timerBloc, - ); - expect(find.byType(AppBar), findsNothing); - }); - }); - - group('SudokuBoardView', () { - late SudokuBloc sudokuBloc; - late TimerBloc timerBloc; - late Sudoku sudoku; - - setUp(() { - sudokuBloc = MockSudokuBloc(); - timerBloc = MockTimerBloc(); - sudoku = MockSudoku(); - - when(() => sudoku.getDimesion()).thenReturn(3); - when(() => sudoku.blocks).thenReturn([]); - when(() => sudokuBloc.state).thenReturn( - SudokuState(sudoku: sudoku), - ); - - when(() => timerBloc.state).thenReturn( - TimerState(secondsElapsed: 1, isRunning: true), - ); - }); - - testWidgets( - 're-renders only when sudoku from [SudokuState] changes', - (tester) async { - final block1 = Block( - position: Position(x: 0, y: 0), - correctValue: 1, - currentValue: 1, - ); - final block2 = Block( - position: Position(x: 0, y: 1), - correctValue: 2, - currentValue: 2, - ); - - when(() => sudokuBloc.stream).thenAnswer( - (_) => Stream.fromIterable([ - SudokuState(sudoku: Sudoku(blocks: const [])), - SudokuState(sudoku: Sudoku(blocks: [block1, block2])), - ]), - ); - - await tester.pumpApp( - const SudokuBoardView( - layoutSize: ResponsiveLayoutSize.large, - ), - sudokuBloc: sudokuBloc, - timerBloc: timerBloc, - ); - - await tester.pumpAndSettle(); - expect(find.byKey(Key('sudoku_board_view_0_0')), findsOneWidget); - expect(find.byKey(Key('sudoku_board_view_0_1')), findsOneWidget); - }, - ); - }); -} diff --git a/test/sudoku/widgets/sudoku_block_test.dart b/test/sudoku/widgets/sudoku_block_test.dart index 7da9f81..ee6c956 100644 --- a/test/sudoku/widgets/sudoku_block_test.dart +++ b/test/sudoku/widgets/sudoku_block_test.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:sudoku/models/models.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/sudoku/sudoku.dart'; import '../../helpers/helpers.dart'; @@ -20,20 +21,24 @@ void main() { const mediumBlockKey = 'sudoku_block_medium_0_0'; const largeBlockKey = 'sudoku_block_large_0_0'; + late Puzzle puzzle; late Sudoku sudoku; - late SudokuBloc bloc; - late SudokuState state; + late PuzzleBloc bloc; + late PuzzleState state; setUp(() { sudoku = MockSudoku(); when(() => sudoku.getDimesion()).thenReturn(3); - state = MockSudokuState(); - when(() => state.sudoku).thenReturn(sudoku); - when(() => state.highlightedBlocks).thenReturn({}); - when(() => state.currentSelectedBlock).thenReturn(block); + puzzle = MockPuzzle(); + when(() => puzzle.sudoku).thenReturn(sudoku); - bloc = MockSudokuBloc(); + state = MockPuzzleState(); + when(() => state.puzzle).thenReturn(puzzle); + when(() => state.selectedBlock).thenReturn(block); + when(() => state.highlightedBlocks).thenReturn([]); + + bloc = MockPuzzleBloc(); when(() => bloc.state).thenReturn(state); }); @@ -42,7 +47,7 @@ void main() { (tester) async { await tester.pumpApp( SudokuBlock(block: block, state: state), - sudokuBloc: bloc, + puzzleBloc: bloc, ); await tester.tap(find.byType(GestureDetector)); @@ -57,7 +62,7 @@ void main() { await tester.pumpApp( SudokuBlock(block: block, state: state), - sudokuBloc: bloc, + puzzleBloc: bloc, ); expect(find.byKey(Key(largeBlockKey)), findsOneWidget); @@ -68,7 +73,7 @@ void main() { await tester.pumpApp( SudokuBlock(block: block, state: state), - sudokuBloc: bloc, + puzzleBloc: bloc, ); expect(find.byKey(Key(mediumBlockKey)), findsOneWidget); @@ -79,7 +84,7 @@ void main() { await tester.pumpApp( SudokuBlock(block: block, state: state), - sudokuBloc: bloc, + puzzleBloc: bloc, ); expect(find.byKey(Key(smallBlockKey)), findsOneWidget); @@ -91,15 +96,15 @@ void main() { final otherBlock = MockBlock(); when(() => otherBlock.position).thenReturn(Position(x: 0, y: 1)); - when(() => state.highlightedBlocks).thenReturn({block}); - when(() => state.currentSelectedBlock).thenReturn(otherBlock); + when(() => state.highlightedBlocks).thenReturn([block]); + when(() => state.selectedBlock).thenReturn(otherBlock); await tester.pumpApp( SudokuBlock( block: block, state: state, ), - sudokuBloc: bloc, + puzzleBloc: bloc, ); expect( diff --git a/test/sudoku/widgets/sudoku_input_test.dart b/test/sudoku/widgets/sudoku_input_test.dart index 6c5e486..ad07817 100644 --- a/test/sudoku/widgets/sudoku_input_test.dart +++ b/test/sudoku/widgets/sudoku_input_test.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/sudoku/sudoku.dart'; import '../../helpers/helpers.dart'; @@ -13,24 +14,39 @@ void main() { const mediumInputKey = 'sudoku_input_medium'; const smallInputKey = 'sudoku_input_small'; - late SudokuBloc sudokuBloc; + late PuzzleBloc puzzleBloc; setUp(() { - sudokuBloc = MockSudokuBloc(); + puzzleBloc = MockPuzzleBloc(); }); testWidgets( - 'adds [SudokuInputTapped] when tapped on an input block', + 'adds [SudokuInputEntered] when tapped on an input block', (tester) async { await tester.pumpApp( SudokuInput(sudokuDimension: 1), - sudokuBloc: sudokuBloc, + puzzleBloc: puzzleBloc, ); - await tester.tap(find.byType(GestureDetector)); + await tester.tap(find.byType(GestureDetector).first); await tester.pumpAndSettle(); - verify(() => sudokuBloc.add(SudokuInputTapped(1))).called(1); + verify(() => puzzleBloc.add(SudokuInputEntered(1))).called(1); + }, + ); + + testWidgets( + 'adds [SudokuInputErased] when tapped on the last input block', + (tester) async { + await tester.pumpApp( + SudokuInput(sudokuDimension: 1), + puzzleBloc: puzzleBloc, + ); + + await tester.tap(find.byType(GestureDetector).last); + await tester.pumpAndSettle(); + + verify(() => puzzleBloc.add(SudokuInputErased())).called(1); }, );