The Basics of Unit Testing in StencilJS | Elite Ionic

文章推薦指數: 80 %
投票人數:10人

A unit test is a chunk of code that is written and then executed to test that a single "unit" of your code behaves as expected. A unit test ... JoshMoronyJuly30,201911minreadTestingstenciljestunite2etddOriginallypublishedJuly30,2019Automatedtestingsuchasunittestsandend-to-endtestsare,inmyview,oneofthebiggest"levelup"mechanicsavailabletodeveloperswhoarelookingtoimprovetheirskillset.Itisdifficultandmightrequirealotoftimetolearntogettothepointwhereyoufeelcompetentinwritingtestsforyourcode,andevenonceyouhavethebasicsdownthereisalwaysroomforimprovementtocreatebettertests-justasyoucancontinuetolearntowritebetterandbettercode.Thefunnythingaboutautomatedtestsisthattheyaresovaluable,butatthesametimenotatallrequired(andarethereforeoftenskipped).Youdon'tneedtestsassociatedwithyourcodetomakethemwork,andwritingtestswon'tdirectlyimpactthefunctionalityofyourapplicationatall.However,eventhoughtheteststhemselveswon'tchangeyourcodedirectlyorthefunctionalityofyourapplication,theywillfacilitatepositivechangesinotherways.Someofthekeybenefitsofinvestingtimeinautomatedtestsinclude:Documentation-unittestsaresetoutinsuchawaythattheyaccuratelydescribetheintendedfunctionalityoftheapplicationVerification-youcanhavegreaterconfidencethatyourapplication'scodeisbehavingthewayyouintendedRegressionTesting-whenyoumakechangestoyourapplicationyoucanbemoreconfidentthatyouhaven'tbrokenanythingelse,andifyouhavethereisagreaterchancethatyouwillfindoutaboutit,asyoucanrunthesametestsagainstthenewcodeCodeQuality-itisdifficulttowriteeffectivetestsforpoorlydesigned/organisedapplications,whereasitismucheasiertowritetestsforwell-designedcode.Naturally,writingautomatedtestsforyourapplicationswillforceyouintowritinggoodcodeSleep-you'llbelessofanervouswreckwhendeployingyourapplicationtotheappstorebecauseyouaregoingtohaveagreaterdegreeofconfidencethatyourcodeworks(andthesamegoesforwhenyouareupdatingyourapplication)Inthistutorial,wearegoingtofocusonthebasicsofwritingunittestsforStencilJScomponents.ThistutorialwillhaveanemphasisonusingIonicwithStencilJS,butthesamebasicconceptswillapplyregardlessofwhetheryouareusingIonicornot.IfyouwouldlikeaquickpreviewofwhatcreatingandrunningunittestsinaStencilJSapplicationlookslike,youcancheckoutthevideoIrecordedtoaccompanythistutorial:CreatingUnitTestsforStencilJSComponents.Mostofthedetailsareinthiswrittentutorial,butthevideoshouldhelptogiveyouabitofcontext.OutlineSourcecodeKeyPrinciplesExecutingUnitTestsinanIonic/StencilJSApplicationWritingOurOwnUnitTestsSummaryKeyPrinciplesBeforewegetintoworkingwithsomeunittests,weneedtocoverafewbasicideas.StencilJSusesJestforunittests,whichisagenericJavaScripttestingframework.IfyouarealreadyfamiliarwithJasminethenyouwillalreadyunderstandmostofwhatisrequiredforwritingtestswithJest,becausetheAPIisextremelysimilar.IhavealreadywrittenmanyarticlesoncreatingunittestswithJasmine,soifyouneedabitofanintroductiontothebasicideasIwouldrecommendreadingthisarticlefirst:HowtoUnitTestanIonic/AngularApplication-ofcourse,weareusingJestnotJasmine(asanIonic/Angularapplicationuses)butthebasicconceptsarealmostidentical.Althoughthearticleabovegoesintomoredetail,I'llalsobreakdownthebasicconceptshere.Thebasicstructureforaunittestmightlooksomethinglikethis:describe('MyService',()=>{ it('shouldcorrectlyaddnumbers',()=>{ expect(1+1).toBe(2); }); });Jestprovidesthedescribe,it,andexpectmethodsthatweareusingabovetocreatethetest.Inthiscase,wearecreatingatestthatchecksthatnumbersareaddedtogethercorrectly.Thisisjustanexample,andsincewearejustdirectlyexecuting1+1(ratherthantestingsomeofouractualcodethathasthetaskofdoingthat)wearen'treallyachievinganythingbecausethisisalwaysgoingtowork.describe()definesatestsuite(e.g.a"collection")oftests(or"specs")it()definesaspecifictestor"spec",anditlivesinsideofasuite(describe()).Thisiswhatdefinestheexpectedbehaviourofthecodeyouaretesting,e.g."itshoulddothis","itshoulddothat"expect()definestheexpectedresultofatestandlivesinsideofit()Aunittestisachunkofcodethatiswrittenandthenexecutedtotestthatasingle"unit"ofyourcodebehavesasexpected.Aunittestmighttestthatamethodreturnstheexpectedvalueforagiveninput,orperhapsthataparticularmethodiscalledwhenthecomponentisinitialised.Itisimportantthatunittestsaresmallandisolated,theyshouldonlytestoneveryspecificthing.Forexample,weshouldn'tcreateasingle"thecomponentworks"testthatexecutesabunchofdifferentexpectstatementstotestfordifferentthings.Weshouldcreatemanysmallindividualunitteststhateachhavetheresponsibilityoftestingonesmallunitofcode.Therearemanyconceptsandprinciplestolearnwhenitcomestocreatingautomatedtests,andyoucouldliterallyreadentirebooksjustonthecoreconceptsoftestingingeneral(withoutgettingintospecificsofaparticularframeworkortestrunner).However,Ithinkthatoneofthemostimportantconceptstokeepinmindis:Arrange,Act,AssertorAAA.Allofyourtestswillfollowthesamebasicstructureof:Arrange-gettheenvironment/dependencies/componentssetupinthestaterequiredforthetestAct-runcodeinthetestthatwillexecutethebehaviouryouaretestingAssert-makeastatementofwhattheexpectedresultwas(whichwilldeterminewhetherornotthetestpassed)Wewilllookatexamplesofthisthroughoutthetutorial(especiallywhenwegetintowritingourownunittests).ExecutingUnitTestsinanIonic/StencilJSApplicationLet'sstartwithrunningsomeunittestsandtakingalookattheoutput.Bydefault,anewlygeneratedIonic/StencilJSapplicationwillhaveafewunittestsandend-to-endtestsalreadycreatedforthedefaultfunctionalityprovided.Thisisgreatbecausewecantakealookatthoseteststogetageneralideaofwhattheylooklike,andalsoexecutetheteststoseewhathappens.First,youshouldgenerateanewionic-pwaapplicationwiththefollowingcommand:npminitstencilOnceyouhavetheapplicationgenerated,andyouhavemadeityourcurrentworkingdirectory,youshouldcreateaninitialbuildofyourapplicationwith:npmrunbuildThentoexecutetheteststhatalreadyexistintheapplicationyouwilljustneedtorun:npmtestThefirsttimeyourunthis,itwillinstallalloftherequireddependenciestorunthetests.Thismighttakeawhile,butitwon'ttakethislongeverytimeyourunthiscommand.Thedefaulttestoutputforanuntouchedblankionic-pwaapplicationwilllooklikethis(Ihavetrimmedthisdownalittleforspace):PASSsrc/components/app-profile/app-profile.spec.ts PASSsrc/components/app-root/app-root.spec.ts PASSsrc/components/app-home/app-home.e2e.ts PASSsrc/components/app-profile/app-profile.e2e.ts Console PASSsrc/components/app-root/app-root.e2e.ts Console PASSsrc/components/app-home/app-home.spec.ts TestSuites:6passed,6total Tests:13passed,13total Snapshots:0total Time:3.208s Ranalltestsuites.Wecanseethatthereare13testsintotalacrossthethreedefaultcomponents,andallofthemhaveexecutedsuccessfully.Let'stakeacloserlookatwhatthesetestsareactuallydoingbyopeningthesrc/components/app-home/app-home.spec.tsfile:import{AppHome}from'./app-home'; describe('app',()=>{ it('builds',()=>{ expect(newAppHome()).toBeTruthy(); }); });Thislookssimilartotheexampletestthatwelookedatbefore,exceptnowweareactuallytestingrealcode.WeimporttheAppHomecomponentintothetestfile,andthenwehaveatestthatcreatesanewinstanceofAppHomeandchecksthatitis"truthy".Thisdoesn'tmeanthattheresultneedstobetruebutthattheresulthasa"true"valuesuchthatitisanobjectorastringoranumber,ratherthanbeingnullorundefinedwhichwouldbe"falsy".ThistestwillensurethatAppHomecanbesuccessfullyinstantiated.Thisisabasictestthatyoucouldaddtoanyofyourcomponents.Nowlet'shaveabitoffunbymakingitfailbychangingtoBeTruthytotoBeFalsy:import{AppHome}from'./app-home'; describe('app',()=>{ it('builds',()=>{ expect(newAppHome()).toBeFalsy(); }); });Ifweexecutethetestsnow,wewillgetafailure:FAILsrc/components/app-home/app-home.spec.ts ●app›builds expect(received).toBeFalsy() Received:{} 3|describe("app",()=>{ 4|it("builds",()=>{ >5|expect(newAppHome()).toBeFalsy(); |^ 6|}); 7|}); 8| atObject.it(src/components/app-home/app-home.spec.ts:5:27) TestSuites:1failed,5passed,6total Tests:1failed,12passed,13total Snapshots:0total Time:3.56s Ranalltestsuites.Wecanseethatweareexpectingthereceivedvaluetobe"falsy",butitisa"truthy"valueandsothetestfails.Thisshouldhighlightsomeoftheusefulnessofunittests.Whenourcodeisn'tdoingwhatweexpectitto,wecanseeexactlywhereitisfailingandwhy(assumingthetestsaredefinedwell).Thisisoneofthereasonstodesignsmall/isolatedunittests,aswhenafailureoccursitwillbemoreobviouswhatcausedthefailure.NOTE:RemembertochangethetestbacktotoBeTruthysothatitdoesn'tcontinuetofailNowlet'stakealookattheunittestsfortheprofilepagedefinedinsrc/components/app-profile/app-profile.spec.tswhicharealittlemoreinteresting:import{AppProfile}from'./app-profile'; describe('app-profile',()=>{ it('builds',()=>{ expect(newAppProfile()).toBeTruthy(); }); describe('normalization',()=>{ it('returnsablankstringifthenameisundefined',()=>{ constcomponent=newAppProfile(); expect(component.formattedName()).toEqual(''); }); it('capitalizesthefirstletter',()=>{ constcomponent=newAppProfile(); component.name='quincy'; expect(component.formattedName()).toEqual('Quincy'); }); it('lower-casesthefollowingletters',()=>{ constcomponent=newAppProfile(); component.name='JOSEPH'; expect(component.formattedName()).toEqual('Joseph'); }); it('handlessingleletternames',()=>{ constcomponent=newAppProfile(); component.name='q'; expect(component.formattedName()).toEqual('Q'); }); }); });Westillhavethebasic"builds"test,butwealsohavesomespecifictestsrelatedtothefunctionalityofthisparticularcomponent.Theprofilepagehassomefunctionalitybuiltintothathandlesformattingtheuser'sname.Thesetestscoverthatfunctionality.Noticethatwedon'tjusthaveasingle"itformatsthenamecorrectly"test.Theprocessforformattingthenameinvolvesseveraldifferentthings,sothesearegoodunittestsinthattheyareindividuallytestingforeach"unit"offunctionalityofthenameformatting.Generallyspeaking,themoreunittestsyouhaveandthemoreisolatedtheyarethebetter,butespeciallyasabeginnertrynottoobsesstoomuchovergettingthings"perfect".Havingabloatedunittestisstillmuchbetterthannottestingatall.Theonethingyoureallydohavetowatchoutforisthatyourtestsareactuallytestingwhatyouthinktheyare.Youmightdesignatestinawaywhereitwillalwayspass,nomatterwhatcodeyouhaveimplemented.Thisisbad,becauseitwillmakeyouthinkyourcodeisworkingasexpectedwhenitisnot.TheTestDrivenDevelopmentapproach,whichwewillbrieflydiscussinamoment,canhelpalleviatethis.WritingOurOwnUnitTestsTofinishthingsoff,let'sactuallybuildoutsometestsofourowninanewcomponent.Itissimpleenoughtolookatsomepre-definedtestandhaveabasicunderstandingofwhatisbeingtested,butwhenyoucometowriteyourowntestsitcanbereallydifficult.Wewilllookintocreatinganadditionalapp-detailcomponent,whichwillhavethepurposeofexceptinganidasapropfromthepreviouspage,andthenusingthatidtofetchanitemfromaservice.WearegoingtofollowalooseTestDrivenDevelopment(TDD)approachhere.Thisisawholenewtopicinitself.Itisastructuredandstrictprocessforwritingtests,butIthinkitcanactuallyhelpbeginnersgetintotesting.Sincethereisastrictprocesstofollow,itbecomeseasiertodeterminewhatkindoftestsyoushouldbewritingandwhenyoushouldwritethem.Iwon'tgetintoabigspielaboutwhatTDDisinthistutorial,butforsomecontext,Iwouldrecommendreadingoneofmypreviousarticlesonthetopic:TestDrivenDevelopmentinIonic:AnIntroductiontoTDD.ThebasicideabehindTestDrivenDevelopmentisthatyouwritethetestsfirst.Thismeansthatyouwillbeattemptingtotestcodethatdoesnotevenexistyet.Youwillfollowthisbasicprocess:WriteatestthatteststhefunctionalityyouwanttoimplementRunthetest(itwillfail)WritethefunctionalitytosatisfythetestRunthetest(itwillhopefullypass-ifnot,fixthefunctionalityorthetestifrequired)Thekeybenefittobeginnerswiththisprocessisthatyouknowwhattocreatetestsfor,andyoucanbereasonablyconfidentthetestiscorrectifitfailsinitiallybutafterimplementingthefunctionalitythetestworks.Itisanimportantsteptomakesurethetestfailsfirst.Let'sgetstartedbydefiningthefilesnecessaryforournewcomponent.Createthefollowingfiles:src/components/app-detail/app-detail.tsxsrc/components/app-detail/app-detail.csssrc/components/app-detail/app-detail.spec.tsBeforeweimplementanycodeatallfortheapp-detailcomponent,wearegoingtocreateatestinthespecfile:import{AppDetail}from'./app-detail'; describe('app',()=>{ it('builds',()=>{ expect(newAppDetail()).toBeTruthy(); }); });Ifwetrytorunthis,wearegoingtogetafailure:FAILsrc/components/app-detail/app-detail.spec.ts ●app›builds TypeError:app_detail_1.AppDetailisnotaconstructor 3|describe("app",()=>{ 4|it("builds",()=>{ >5|expect(newAppDetail()).toBeTruthy(); |^ 6|}); 7|}); 8| atObject.it(src/components/app-detail/app-detail.spec.ts:5:12) PASSsrc/components/app-root/app-root.spec.ts TestSuites:1failed,6passed,7total Tests:1failed,13passed,14total Snapshots:0total Time:3.363s Ranalltestsuites.Thismakessense,becausewehaven'tevencreatedtheAppDetailcomponentyet.Nowlet'sdefinethecomponenttosatisfythetest:import{Component,h}from'@stencil/core'; @Component({ tag:'app-home', styleUrl:'app-home.css', }) exportclassAppHome{ render(){ return[ Detail , , ]; } }Ifyourunthetestsagain,youshouldseethatallofthempass.We'restillinprettyboringterritoryhere,becausewe'veseenallofthisinthedefaulttests.Let'screatetestsforthekeyfunctionalityofourdetailpage.Wewanttwokeythingstohappeninthiscomponent:AnitemIdpropshouldbeavailabletopassinanitemsidAcalltothegetItem()methodoftheItemServiceshouldbecalledwiththeitemIdfromthepropWeareveryspecificallyandintentionallyjustcheckingthatacallismadetothegetItem()method,notthatthecorrectitemisreturnedfromtheservice.Ourunittestsshouldonlybeconcernedwithwhatishappeninginisolation,itisnottheroleofthisunittesttocheckthattheItemServiceisalsoworkingasintended(thiswouldbetheroleoftheservicesownunittests,orof"integration"or"end-to-end"testswhichtakeintoconsiderationtheapplicationasawholeratherthanindividualunitsofcode).Let'stakealookathowwemightimplementthesetests,andthenwewilltrytosatisfythembyimplementingsomecode.WewillstartwithtestingtheitemIdprop.import{AppDetail}from'./app-detail'; import{newSpecPage}from'@stencil/core/testing'; describe('app',()=>{ it('builds',()=>{ expect(newAppDetail()).toBeTruthy(); }); describe('itemdetailfetching',()=>{ it('hasanitemIdprop',async()=>{ //Arrange constpage=awaitnewSpecPage({ components:[AppDetail], html:`

`, }); letcomponent=page.doc.createElement('app-detail'); //Act (componentasany).itemId=3; page.root.appendChild(component); awaitpage.waitForChanges(); //Assert expect(page.rootInstance.itemId).toBe(3); }); it('callsthegetItemmethodofItemServicewiththesuppliedid',()=>{}); }); });Thefirstthingtonoticehereisthatour"hasanitemIdprop"testisasync-thisisnecessarywhereyouwanttomakeuseofasynchronouscodeinatest.ThatbringsustotheasynchronouscodethatweareusingawaitwithandthatisnewSpecPage.ThisisaStencilJSspecificconcept.Sometimes,wewillbeabletojustinstantiateourcomponentwithnewAppDetail()andtestwhatweneed,butsometimesweneedthecomponenttoactuallyberenderedasStencilJSwouldbuildthecomponentanddisplayitinthebrowser.Thismeansthatwecandothis:constpage=awaitnewSpecPage({ components:[AppDetail], html:``, });Totestrenderingourapp-detailcomponent.Wecansupplywhatevercomponentsweneedavailable,andthendefinewhatevertemplatewewantusingthosecomponents.Wearen'tactuallydoingthathere,though.Instead,wearerenderingoutasimple
andthenwearemanuallycreatingtheapp-detailelementandaddingittothepagethatwecreated.Thereasonwearedoingthisissothatwecansupplywhatevertypeofpropvaluewewanttothecomponent,whichwearedoinghere:(componentasany).itemId=3;NOTE:Wearealsousingasanytoignoretypewarnings.Thisisafantasticconcept(usingnewSpecPagetocreatea
andthenappendingtheactualcomponent)thatIcameacrossfromTallyBarakinthisblogpost:UnitTestingStencilOne.Thatpostalsohasabunchofothergreattipsyoucancheckout.Withthismethod,weassignwhateverkindofpropweneedtothecomponent,andthenweuseappendChildtoaddittothetemplate.Wethenwaitforanychangestohappen,andthenwecheckthattheitemIdhasbeensetto3.ThistestisagoodexampleofhowtestscanbesplitupintoArrange,Act,Assert.Ifwerunthisnow,thetestshouldfail:FAILsrc/components/app-detail/app-detail.spec.ts ●app›itemdetailfetching›hasanitemIdprop expect(received).toBe(expected)//Object.isequality Expected:3 Received:undefined 23| 24|//Assert >25|expect(page.rootInstance.itemId).toBe(3); |^ 26|}); 27| 28|it("callsthegetItemmethodofItemServicewiththesuppliedid",()=>{ atObject.it(src/components/app-detail/app-detail.spec.ts:25:40)Nowlet'strytosatisfythattestwithsomecode:import{Component,Prop,h}from'@stencil/core'; @Component({ tag:'app-detail', styleUrl:'app-detail.css', }) exportclassAppDetail{ @Prop()itemId:number; render(){ return[ Detail , , ]; } }Ifyourunthetestsagain,unlikeBalrogsinthedepthsofMoria,theyshouldnowpass.Nowlet'simplementourtestforinterfacingwiththeItemService:import{AppDetail}from"./app-detail"; import{ItemService}from"../../services/items"; import{newSpecPage}from"@stencil/core/testing"; describe("app",()=>{ it("builds",()=>{ expect(newAppDetail()).toBeTruthy(); }); describe("itemdetailfetching",()=>{ it("hasanitemIdprop",async()=>{ //Arrange constpage=awaitnewSpecPage({ components:[AppDetail], html:`
` }); letcomponent=page.doc.createElement("app-detail"); //Act (componentasany).itemId=3; page.root.appendChild(component); awaitpage.waitForChanges(); //Assert expect(page.rootInstance.itemId).toBe(3); }); it("callsthegetItemmethodofItemServicewiththesuppliedid",async()=>{ //Arrange ItemService.getItem=jest.fn(); constpage=awaitnewSpecPage({ components:[AppDetail], html:`
` }); letcomponent=page.doc.createElement("app-detail"); //Act (componentasany).itemId=5; page.root.appendChild(component); awaitpage.waitForChanges(); //Assert expect(ItemService.getItem).toHaveBeenCalledWith(5); }); }); });ThistestismakinguseofanItemServicewhichwehaven'ttalkedaboutorcreated.Typically,youwouldcreatethatserviceinthesamewaybydefiningsometestsfirstandthenimplementingthefunctionality,butforthistutorial,wearejustgoingtocreatetheserviceinsrc/services/items.ts:classItemServiceController{ constructor(){} asyncgetItem(id:number){ return{id:id}; } } exportconstItemService=newItemServiceController();Thetestthatwehavecreatedisverysimilartothefirstone,exceptnowwearesettingupa"mock/spy"onourItemService.Iwouldrecommendreadingalittleaboutmockfunctionshere:MockFunctions.ThebasicideaisthatratherthanusingourrealItemServiceweinsteadswapitoutwitha"fake"or"mocked"service,whichwecanthen"spy"ontocheckabunchofthingslikewhetherornotitsmethodswerecalledinthetest.Inthistest,wejustsupplyourpropagain,butthistimewecheckthatattheendofthetesttheItemService.getItemmethodshouldhavebeencalled.Thisisbecausethecomponentshouldautomaticallytakewhateverpropissupplied,andthenusethatvaluetofetchtheitemfromtheserviceusinggetItem.Ifwerunthattestnow,itshouldfail:FAILsrc/components/app-detail/app-detail.spec.ts ●app›itemdetailfetching›callsthegetItemmethodofItemServicewiththesuppliedid expect(jest.fn()).toHaveBeenCalledWith(expected) Expectedmockfunctiontohavebeencalledwith: [5] Butitwasnotcalled. 44| 45|//Assert >46|expect(ItemService.getItem).toHaveBeenCalledWith(5); |^ 47|}); 48|}); 49|}); atObject.it(src/components/app-detail/app-detail.spec.ts:46:35)Nowlet'simplementthefunctionality:import{Component,Prop,h}from'@stencil/core'; import{ItemService}from'../../services/items'; @Component({ tag:'app-detail', styleUrl:'app-detail.css', }) exportclassAppDetail{ @Prop()itemId:number; asynccomponentDidLoad(){ console.log(awaitItemService.getItem(this.itemId)); } render(){ return[ Detail , , ]; } }andweshouldseethefollowingresult:TestSuites:7passed,7total Tests:16passed,16total Snapshots:0total Time:3.248s Ranalltestsuites.Allofourtestsarenowpassing!SummaryThishasbeenasomewhatlongandin-depthtutorial,butitstillbarelyscratchesthesurfaceoftestingconcepts.Thereisstillmuchmoretolearn,anditwilltaketimetobecomecompetentincreatingautomatedtests.Myadviceisalwaystojuststartwritingtests,anddon'tworrytoomuchaboutmakingthemperfectorfollowingbest-practices(justdon'tassumethatyourtestsareactuallyverifyingthefunctionalityofyourapplicationinthebeginning).Ifyouworrytoomuchaboutthis,youmightbetoointimidatedtoevenstart.IfyouarealreadycompetentinwritingtestswithJasmine,Jest,orsomethingsimilar,thencreatingtestsforStencilJScomponentslikelywon'tbetoodifficult.ThebiggestdifferencewilllikelybetheuseofnewSpecPagetotestcomponentswhererequired.BecomeaPROmemberKeyPrinciplesExecutingUnitTestsinanIonic/StencilJSApplicationWritingOurOwnUnitTestsSummarySourcecodeJointhenewsletter.I'llgiveyouexactdetailsinthedoubleopt-inemailconfirmation.Follow@joshuamoronyIfyouenjoyedthisarticle,feelfreetoshareitwithothers!TweetDiscussionNeedsomehelpwiththistutorial?Spottedanerror?Gotsomehelpfuladviceforothers?JointhediscussiononTwitterIftherearenoactivediscussions,startonebyincludingtheURLofthispostandtagme(@joshuamorony)inanewtweet.I'lltrytohelpoutdirectlywheneverIhavethetime,butyoumightalsowanttoincludeotherrelevanttagstoattractattentionfromotherswhomightalsobeabletohelp.Tomakeitsupereasyforotherstohelpyouout,youmightconsidersettingupanexampleonStackBlitzsootherscanjumprightintoyourcode.PullrequestsIfyoufindanerrororsomeoutdatedcodethatyouwouldliketohelpfix,feelfreetosendmeapullrequestonGitHub


請為這篇文章評分?