[{"data":1,"prerenderedAt":29406},["ShallowReactive",2],{"content-query-ScRmaAbYjz":3,"latestBlog":335,"upcoming-workshops-sidebar":29405},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":7,"description":8,"date":9,"published":10,"slug":11,"tags":12,"image":16,"cypressVersion":17,"playwrightVersion":17,"vitestVersion":17,"readingTime":18,"body":23,"_type":329,"_id":330,"_source":331,"_file":332,"_stem":333,"_extension":334},"/maslows-hammer-and-three-lies-qa-tells-itself","",false,"Maslow's Hammer and Three Lies QA Tells Itself","A short post on what needs to change for QA to thrive in the age of AI.","2026-03-31",true,"maslows-hammer-and-three-lies-qa-tells-itself",[13,14,15],"testing","QA","AI","hammer_e1zcs5.png",null,{"text":19,"minutes":20,"time":21,"words":22},"9 min read",8.51,510600,1702,{"type":24,"children":25,"toc":319},"root",[26,34,39,44,51,56,62,67,73,86,99,104,150,155,160,166,180,185,190,195,200,205,211,216,221,226,231,237,242,257,262,267,273,278,283,288,293,298,309,314],{"type":27,"tag":28,"props":29,"children":30},"element","p",{},[31],{"type":32,"value":33},"text","It seems to me like there's a lot of anxiety on testing conferences these days.",{"type":27,"tag":28,"props":35,"children":36},{},[37],{"type":32,"value":38},"Code velocity is ramping up while QA teams getting downsized. Those who haven't been affected are worried that they'll be facing the same scenario. It's clear that AI has a massive impact, but there hasn't been too much concrete example on what that impact actually is.",{"type":27,"tag":28,"props":40,"children":41},{},[42],{"type":32,"value":43},"Speakers at these conferences mention AI, but they seem to be kinda vague when talking about the parts that fuel the mentioned anxiety. Each Q&A section has the same question being repeated: \"What do we do with all this?\" I keep hearing three common answers.",{"type":27,"tag":45,"props":46,"children":48},"h2",{"id":47},"the-things-will-get-better-answer",[49],{"type":32,"value":50},"The \"things will get better\" answer",{"type":27,"tag":28,"props":52,"children":53},{},[54],{"type":32,"value":55},"The most common one is: \"The golden age of QA is coming\". The logic is AI makes mistakes -> Everyone is using AI to write code -> Code needs to be checked -> We'll need more testers to do the job (mostly manual QA).",{"type":27,"tag":45,"props":57,"children":59},{"id":58},"the-things-may-get-worse-answer",[60],{"type":32,"value":61},"The \"things may get worse\" answer",{"type":27,"tag":28,"props":63,"children":64},{},[65],{"type":32,"value":66},"Another one I hear is: \"The SDLC will crumble down without testing.\"  Main arguments for this one point to major incidents that tend to happen if the software development moves too fast at the cost of proper validation.",{"type":27,"tag":45,"props":68,"children":70},{"id":69},"the-things-will-stay-the-same-answer",[71],{"type":32,"value":72},"The \"things will stay the same\" answer",{"type":27,"tag":28,"props":74,"children":75},{},[76,78,84],{"type":32,"value":77},"The third common answer is that everything will eventually calm down and companies will start \"doing things right\". This encourages testers to keep doing what the're doing, because all of the changes happening out there have ultimately no ",{"type":27,"tag":79,"props":80,"children":81},"strong",{},[82],{"type":32,"value":83},"real",{"type":32,"value":85}," impact on their work.",{"type":27,"tag":28,"props":87,"children":88},{},[89,91],{"type":32,"value":90},"My body goes into restless mode whenever I hear these answers, because I feel like all of these answers are a lie. Not necessarily delibrate lie. Sometimes it's just a lie that the testers themselves believe in. It's as the old saying says:  ",{"type":27,"tag":79,"props":92,"children":93},{},[94],{"type":27,"tag":79,"props":95,"children":96},{},[97],{"type":32,"value":98},"\"If all you have is a hammer, everything looks like a nail.\"",{"type":27,"tag":28,"props":100,"children":101},{},[102],{"type":32,"value":103},"This common saying is attributed to Abraham Maslow (you probably know his pyramid) which he used as a critique for how scientists and academics think. Maslow observed that:",{"type":27,"tag":105,"props":106,"children":107},"ul",{},[108,122,135],{"type":27,"tag":109,"props":110,"children":111},"li",{},[112,114],{"type":32,"value":113},"scientists tend to become ",{"type":27,"tag":79,"props":115,"children":116},{},[117],{"type":27,"tag":79,"props":118,"children":119},{},[120],{"type":32,"value":121},"over-specialized",{"type":27,"tag":109,"props":123,"children":124},{},[125,127],{"type":32,"value":126},"they rely heavily on the ",{"type":27,"tag":79,"props":128,"children":129},{},[130],{"type":27,"tag":79,"props":131,"children":132},{},[133],{"type":32,"value":134},"methods they already know",{"type":27,"tag":109,"props":136,"children":137},{},[138,140,148],{"type":32,"value":139},"they ",{"type":27,"tag":79,"props":141,"children":142},{},[143],{"type":27,"tag":79,"props":144,"children":145},{},[146],{"type":32,"value":147},"force-fit problems",{"type":32,"value":149}," into their preferred frameworks",{"type":27,"tag":28,"props":151,"children":152},{},[153],{"type":32,"value":154},"What leaves me restless is that I sometimes also see this pattern at testing conferences. It's something I've observed for a long time, but while 3 years ago I would just be slightly annoyed, these days I'm nervous and worried.",{"type":27,"tag":28,"props":156,"children":157},{},[158],{"type":32,"value":159},"I'd like to spend some time on these answers for a moment and discuss about why I think they're wrong.",{"type":27,"tag":45,"props":161,"children":163},{"id":162},"golden-age-is-coming",[164],{"type":32,"value":165},"Golden age is coming",{"type":27,"tag":28,"props":167,"children":168},{},[169,171,178],{"type":32,"value":170},"This is an idea that is nice to hear and nice to say. I should know, ",{"type":27,"tag":172,"props":173,"children":175},"a",{"href":174},"testing-will-become-more-important-not-less",[176],{"type":32,"value":177},"I've said it myself",{"type":32,"value":179},". But it's narrow minded.",{"type":27,"tag":28,"props":181,"children":182},{},[183],{"type":32,"value":184},"It's easy to fall for this idea, when you notice the incredible speed that the usage of AI has brought. Speed is something that's being discussed on social media, and at conference talks. Speed is something we oftentimes see in demos. It's cool to demonstrate, and measure.",{"type":27,"tag":28,"props":186,"children":187},{},[188],{"type":32,"value":189},"AI has brought speed to the development. Because of this, it's easy to make a prediction on that increasing the speed of development will create higher demands on testing. Even teams that claim to downsize development teams due to AI efficiency seem to expect higher code output, which some expect will result in more work for testing.",{"type":27,"tag":28,"props":191,"children":192},{},[193],{"type":32,"value":194},"But as I mentioned, this line of thinking lacks dimension. This change in speed is not just about making teams code faster. It has a transformative property. With AI, we'll be looking at changes in how development teams look and what are the roles and responsibilities.",{"type":27,"tag":28,"props":196,"children":197},{},[198],{"type":32,"value":199},"This is what drives many downsizing decisions in companies. I'm not claiming that these decisions are always the right ones. Sometimes, AI is the scapegoat for massive layoffs. But no matter what the real reason is, it's obvious that many roles are going through a re-definition period.",{"type":27,"tag":28,"props":201,"children":202},{},[203],{"type":32,"value":204},"It seems unrealistic and downright delusional to think that the redefinition will skip over QA, but affect vritually every part of development. The expectation that QA will be in higher demand because we can generate code faster is unfortunately ignoring this transformation. It's achored in a modus operandi that is disappearing. It's built on the idea that AI code generation is only affecting the output velocity, without affecting anything else.",{"type":27,"tag":45,"props":206,"children":208},{"id":207},"sdlc-is-crumbling-down",[209],{"type":32,"value":210},"SDLC is crumbling down",{"type":27,"tag":28,"props":212,"children":213},{},[214],{"type":32,"value":215},"Many will point to anecdotes where poor AI coding output caused incidents, outages, spike in number of bugs, reliability or performance issues. Combined with layoffs that affect many QAs, the picture painted looks like a recipe for a disaster.",{"type":27,"tag":28,"props":217,"children":218},{},[219],{"type":32,"value":220},"Stories like these serve as a good argument for keeping the QA role as we know it today. It opens up a path that guides us to advocating for testers. These stories suggest that if companies want to keep quality, they need to keep their QA teams intact. This sounds reasonable on the surface. But it's built on a flawed assumption - that quality is primarily a result of having testers on the team.",{"type":27,"tag":28,"props":222,"children":223},{},[224],{"type":32,"value":225},"In reality, keeping quality is not just about having testers. Many of the most effective quality gates are systematic. Linters and checks in type-based languages catch typos and syntax errors. Unit tests prevent regressions at the source. Code reviews catch issues before they ever reach a test environment. Monitoring and observability tools catch production issues in real time, often faster than any manual tester could. More and more systems and tooling is being built around AI to help improve the quality.",{"type":27,"tag":28,"props":227,"children":228},{},[229],{"type":32,"value":230},"When we frame the conversation as \"hire more testers or quality will suffer\", we're ignoring all the other mechanisms that contribute to quality. And worse - we're positioning QA as the only thing standing between a company and disaster, which is a fragile place to be. If the only argument for your role is that things will fall apart without you, eventually someone will test that theory.",{"type":27,"tag":45,"props":232,"children":234},{"id":233},"things-will-not-change",[235],{"type":32,"value":236},"Things will not change",{"type":27,"tag":28,"props":238,"children":239},{},[240],{"type":32,"value":241},"This answer is rooted in AI skepticism and a firm belief in one's own expertise. \"I've been doing this for 15 years, I know what good testing looks like, and no AI agent is going to change that.\" There's a quiet confidence to it that can feel reassuring.",{"type":27,"tag":28,"props":243,"children":244},{},[245,247,255],{"type":32,"value":246},"And look - expertise and self-worth are important. I'm not dismissing that. I’ve said repeatedly on stage and off-stage that the need for expertise is not going anywhere. But I’m also seeing a weird paradox when talking to \"things will not change\" folks. If you ask any tester what the most important quality of a QA professional is, most will say ",{"type":27,"tag":79,"props":248,"children":249},{},[250],{"type":27,"tag":79,"props":251,"children":252},{},[253],{"type":32,"value":254},"critical thinking",{"type":32,"value":256},".",{"type":27,"tag":28,"props":258,"children":259},{},[260],{"type":32,"value":261},"I'd agree with that. But critical thinking is not simply just something you have. It's something you constantly pursue. It requires learning, growing your knowledge, and expanding your competency. It means being willing to challenge your own assumptions - not just the assumptions in the software you test.",{"type":27,"tag":28,"props":263,"children":264},{},[265],{"type":32,"value":266},"The biggest problem with the \"things will stay the same\" mindset is its anti-intellectualism. It shuts down curiosity. You’ll hear things like \"They said test automation is going to replace us and look where we are.\" or \"The bubble will soon pop.\". But more importantly, it says \"I already know enough.\" And that's paradoxical coming from people who pride themselves on questioning everything. If your critical thinking stops at the boundary of your own role and career, it's not really critical thinking. It's self-preservation dressed up as a professional skill.",{"type":27,"tag":45,"props":268,"children":270},{"id":269},"so-what-do-we-do-with-all-this",[271],{"type":32,"value":272},"So - what do we do with all this?",{"type":27,"tag":28,"props":274,"children":275},{},[276],{"type":32,"value":277},"I’m also being asked this question by peers. I have lengthy discussions and arguments with friends and members of QA community that I’m part of. I don't have a crystal ball. I can't tell you which roles will disappear or which ones will emerge. But I can share how I think about the direction this is heading.",{"type":27,"tag":28,"props":279,"children":280},{},[281],{"type":32,"value":282},"The QA role is about to change. Not overnight, and probably not in a single dramatic shift. But the trajectory is giving some hints.",{"type":27,"tag":28,"props":284,"children":285},{},[286],{"type":32,"value":287},"The way we advocate for quality needs to change. \"We need more time and people for testing\" is not going to stand as an argument. Quality needs to scale alongside code velocity. If code output doubles but your testing capacity stays the same, the answer isn't to slow down development. The answer is to find ways to make quality keep up. This calls for changes in how we work. Let me give you an example.",{"type":27,"tag":28,"props":289,"children":290},{},[291],{"type":32,"value":292},"When a QA catches a bug today, there's really not too much preventing the same bug from happening again tomorrow. A human found it, a human fixed it, and the system that produced it remains unchanged. This is something that needs to change. QA will have to become more of an engineering role - building, improving and contributing to these systems, creating agents, using quality engineering to build a harness for quality that scales beyond what a single person can check manually.",{"type":27,"tag":28,"props":294,"children":295},{},[296],{"type":32,"value":297},"QA will become a highly collaborative role. Not a gatekeeper that blocks releases, but an enabler that unlocks the team. This has been a reality in many teams I’ve had the chance to work with, but it’s not a rule unfortunately. A gatekeeper says \"you can't ship until I say so.\" An enabler says \"here's how we can ship faster and with confidence.\" QAs are part of post-mortems, collaborate on building solutions, co-create standards and rules for projects define and re-define quality gates and open up important team disucssions.",{"type":27,"tag":28,"props":299,"children":300},{},[301,307],{"type":27,"tag":302,"props":303,"children":304},"em",{},[305],{"type":32,"value":306},"AI has made code cheap to generate. The scarce resource that the market needs now is trust.",{"type":32,"value":308}," - Itamar Friedmann, Cofounder & CEO at Qodo",{"type":27,"tag":28,"props":310,"children":311},{},[312],{"type":32,"value":313},"Quality engineers help build that trust. The role is highly equipped for this task, but that doesn’t mean there’s nothing new to learn. Which brings me back to Maslow's hammer. The answer to the over-specialization problem is to become multidisciplinary. QA needs to become a multidisciplinary role. Not just writing test cases. Not just automating scripts. But understanding systems, building tooling, working with data, and yes - using AI to amplify the work.",{"type":27,"tag":28,"props":315,"children":316},{},[317],{"type":32,"value":318},"We should absolutely be advocates for quality. But it matters what we advocate for. Advocating for bigger teams that move slowly, block releases, and demand more time and resources is going to be an uphill battle. This battle fuels the anxiety, and as we can see - it's a battle we seem losing sticking to our old guns. Instead, we should advocate for smarter systems, better feedback loops, and quality that's built into the process rather than bolted on at the end.",{"title":5,"searchDepth":320,"depth":320,"links":321},2,[322,323,324,325,326,327,328],{"id":47,"depth":320,"text":50},{"id":58,"depth":320,"text":61},{"id":69,"depth":320,"text":72},{"id":162,"depth":320,"text":165},{"id":207,"depth":320,"text":210},{"id":233,"depth":320,"text":236},{"id":269,"depth":320,"text":272},"markdown","content:maslows-hammer-and-three-lies-qa-tells-itself:index.md","content","maslows-hammer-and-three-lies-qa-tells-itself/index.md","maslows-hammer-and-three-lies-qa-tells-itself/index","md",[336,572,916,1622,1921,2424,2988,3217,3447,4022,4476,4757,5043,5272,5452,5792,5948,6475,7717,8226,9839,10362,10985,11446,11782,12195,12953,13381,13998,14599,15137,15562,16010,16510,16910,17255,17552,17802,18349,18661,18993,19565,19890,20215,20743,21086,21550,21840,21982,22762,23077,23674,24476,24911,25424,25690,25929,26226,26495,27201,27611,27769,27970,28344,28497,28709,28924],{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":7,"description":8,"date":9,"published":10,"slug":11,"tags":337,"image":16,"cypressVersion":17,"playwrightVersion":17,"vitestVersion":17,"readingTime":338,"body":339,"_type":329,"_id":330,"_source":331,"_file":332,"_stem":333,"_extension":334},[13,14,15],{"text":19,"minutes":20,"time":21,"words":22},{"type":24,"children":340,"toc":563},[341,345,349,353,357,361,365,369,373,382,393,397,434,438,442,446,455,459,463,467,471,475,479,483,487,491,495,499,503,515,519,523,527,531,535,539,543,547,555,559],{"type":27,"tag":28,"props":342,"children":343},{},[344],{"type":32,"value":33},{"type":27,"tag":28,"props":346,"children":347},{},[348],{"type":32,"value":38},{"type":27,"tag":28,"props":350,"children":351},{},[352],{"type":32,"value":43},{"type":27,"tag":45,"props":354,"children":355},{"id":47},[356],{"type":32,"value":50},{"type":27,"tag":28,"props":358,"children":359},{},[360],{"type":32,"value":55},{"type":27,"tag":45,"props":362,"children":363},{"id":58},[364],{"type":32,"value":61},{"type":27,"tag":28,"props":366,"children":367},{},[368],{"type":32,"value":66},{"type":27,"tag":45,"props":370,"children":371},{"id":69},[372],{"type":32,"value":72},{"type":27,"tag":28,"props":374,"children":375},{},[376,377,381],{"type":32,"value":77},{"type":27,"tag":79,"props":378,"children":379},{},[380],{"type":32,"value":83},{"type":32,"value":85},{"type":27,"tag":28,"props":383,"children":384},{},[385,386],{"type":32,"value":90},{"type":27,"tag":79,"props":387,"children":388},{},[389],{"type":27,"tag":79,"props":390,"children":391},{},[392],{"type":32,"value":98},{"type":27,"tag":28,"props":394,"children":395},{},[396],{"type":32,"value":103},{"type":27,"tag":105,"props":398,"children":399},{},[400,411,422],{"type":27,"tag":109,"props":401,"children":402},{},[403,404],{"type":32,"value":113},{"type":27,"tag":79,"props":405,"children":406},{},[407],{"type":27,"tag":79,"props":408,"children":409},{},[410],{"type":32,"value":121},{"type":27,"tag":109,"props":412,"children":413},{},[414,415],{"type":32,"value":126},{"type":27,"tag":79,"props":416,"children":417},{},[418],{"type":27,"tag":79,"props":419,"children":420},{},[421],{"type":32,"value":134},{"type":27,"tag":109,"props":423,"children":424},{},[425,426,433],{"type":32,"value":139},{"type":27,"tag":79,"props":427,"children":428},{},[429],{"type":27,"tag":79,"props":430,"children":431},{},[432],{"type":32,"value":147},{"type":32,"value":149},{"type":27,"tag":28,"props":435,"children":436},{},[437],{"type":32,"value":154},{"type":27,"tag":28,"props":439,"children":440},{},[441],{"type":32,"value":159},{"type":27,"tag":45,"props":443,"children":444},{"id":162},[445],{"type":32,"value":165},{"type":27,"tag":28,"props":447,"children":448},{},[449,450,454],{"type":32,"value":170},{"type":27,"tag":172,"props":451,"children":452},{"href":174},[453],{"type":32,"value":177},{"type":32,"value":179},{"type":27,"tag":28,"props":456,"children":457},{},[458],{"type":32,"value":184},{"type":27,"tag":28,"props":460,"children":461},{},[462],{"type":32,"value":189},{"type":27,"tag":28,"props":464,"children":465},{},[466],{"type":32,"value":194},{"type":27,"tag":28,"props":468,"children":469},{},[470],{"type":32,"value":199},{"type":27,"tag":28,"props":472,"children":473},{},[474],{"type":32,"value":204},{"type":27,"tag":45,"props":476,"children":477},{"id":207},[478],{"type":32,"value":210},{"type":27,"tag":28,"props":480,"children":481},{},[482],{"type":32,"value":215},{"type":27,"tag":28,"props":484,"children":485},{},[486],{"type":32,"value":220},{"type":27,"tag":28,"props":488,"children":489},{},[490],{"type":32,"value":225},{"type":27,"tag":28,"props":492,"children":493},{},[494],{"type":32,"value":230},{"type":27,"tag":45,"props":496,"children":497},{"id":233},[498],{"type":32,"value":236},{"type":27,"tag":28,"props":500,"children":501},{},[502],{"type":32,"value":241},{"type":27,"tag":28,"props":504,"children":505},{},[506,507,514],{"type":32,"value":246},{"type":27,"tag":79,"props":508,"children":509},{},[510],{"type":27,"tag":79,"props":511,"children":512},{},[513],{"type":32,"value":254},{"type":32,"value":256},{"type":27,"tag":28,"props":516,"children":517},{},[518],{"type":32,"value":261},{"type":27,"tag":28,"props":520,"children":521},{},[522],{"type":32,"value":266},{"type":27,"tag":45,"props":524,"children":525},{"id":269},[526],{"type":32,"value":272},{"type":27,"tag":28,"props":528,"children":529},{},[530],{"type":32,"value":277},{"type":27,"tag":28,"props":532,"children":533},{},[534],{"type":32,"value":282},{"type":27,"tag":28,"props":536,"children":537},{},[538],{"type":32,"value":287},{"type":27,"tag":28,"props":540,"children":541},{},[542],{"type":32,"value":292},{"type":27,"tag":28,"props":544,"children":545},{},[546],{"type":32,"value":297},{"type":27,"tag":28,"props":548,"children":549},{},[550,554],{"type":27,"tag":302,"props":551,"children":552},{},[553],{"type":32,"value":306},{"type":32,"value":308},{"type":27,"tag":28,"props":556,"children":557},{},[558],{"type":32,"value":313},{"type":27,"tag":28,"props":560,"children":561},{},[562],{"type":32,"value":318},{"title":5,"searchDepth":320,"depth":320,"links":564},[565,566,567,568,569,570,571],{"id":47,"depth":320,"text":50},{"id":58,"depth":320,"text":61},{"id":69,"depth":320,"text":72},{"id":162,"depth":320,"text":165},{"id":207,"depth":320,"text":210},{"id":233,"depth":320,"text":236},{"id":269,"depth":320,"text":272},{"_path":573,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":574,"description":575,"date":576,"published":10,"slug":577,"tags":578,"image":583,"cypressVersion":17,"playwrightVersion":17,"vitestVersion":17,"readingTime":584,"body":589,"_type":329,"_id":913,"_source":331,"_file":914,"_stem":915,"_extension":334},"/agents-md-files-are-not-misleading-your-ai-agent","AGENTS.md Files Are Not Misleading Your AI Agent","A recent study sparked headlines claiming AGENTS.md files hurt AI agent performance. But the full picture tells a different story - human-written context files actually improve results by 4%.","2026-03-02","agents-md-files-are-not-misleading-your-ai-agent",[579,580,581,582],"A.I.","developer tools","Claude Code","Cursor","agents_s6v5xq",{"text":585,"minutes":586,"time":587,"words":588},"4 min read",3.76,225600,752,{"type":24,"children":590,"toc":906},[591,596,602,607,612,617,623,635,640,661,681,687,709,737,742,775,787,792,804,809,815,834,839,845,850,901],{"type":27,"tag":28,"props":592,"children":593},{},[594],{"type":32,"value":595},"There's a hot discussion on this topic on the internet right now. People are resharing the headlines, but I think few actually take the time to read the actual study that sparked the conversation. So we're going to break this down, but let's first talk about what these files actually are.",{"type":27,"tag":45,"props":597,"children":599},{"id":598},"what-are-agentsmd-and-claudemd-files",[600],{"type":32,"value":601},"What are AGENTS.md and CLAUDE.md files",{"type":27,"tag":28,"props":603,"children":604},{},[605],{"type":32,"value":606},"Some people explain them as sort of README files for your AI agent. They give a general overview of the project that you are working with. I would say it's a good enough explanation, but there's a little bit more to it.",{"type":27,"tag":28,"props":608,"children":609},{},[610],{"type":32,"value":611},"These files are sent along with your prompt when you work with Cursor, Claude Code, Codex, or other AI coding agents. They get injected into the context window before your agent starts working on a task. Over the last couple of months, they have become sort of a standard and many people chose to add them to their projects.",{"type":27,"tag":28,"props":613,"children":614},{},[615],{"type":32,"value":616},"However, it matters a lot how you choose to create these files.",{"type":27,"tag":45,"props":618,"children":620},{"id":619},"what-the-study-actually-says",[621],{"type":32,"value":622},"What the study actually says",{"type":27,"tag":28,"props":624,"children":625},{},[626,628,633],{"type":32,"value":627},"So let's go back to the study. The study shows a ",{"type":27,"tag":79,"props":629,"children":630},{},[631],{"type":32,"value":632},"3% performance decrease",{"type":32,"value":634}," in tasks done by AI agents on repositories that contain these files over those that don't. This is what's making the headlines today. And it is actually a big deal.",{"type":27,"tag":28,"props":636,"children":637},{},[638],{"type":32,"value":639},"But there's an important caveat.",{"type":27,"tag":28,"props":641,"children":642},{},[643,645,650,652,659],{"type":32,"value":644},"On average, a ",{"type":27,"tag":79,"props":646,"children":647},{},[648],{"type":32,"value":649},"4% performance improvement",{"type":32,"value":651}," was measured in those projects where ",{"type":27,"tag":653,"props":654,"children":656},"code",{"className":655},[],[657],{"type":32,"value":658},"AGENTS.md",{"type":32,"value":660}," files were actually created by humans. The difference between the two numbers comes down to one thing - who wrote the file.",{"type":27,"tag":28,"props":662,"children":663},{},[664,666,671,673,679],{"type":32,"value":665},"It matters a lot whether you create your ",{"type":27,"tag":653,"props":667,"children":669},{"className":668},[],[670],{"type":32,"value":658},{"type":32,"value":672}," file using a ",{"type":27,"tag":653,"props":674,"children":676},{"className":675},[],[677],{"type":32,"value":678},"/init",{"type":32,"value":680}," command, essentially relying on your AI agent to scan your repo, or whether you create it on your own and update it as you go.",{"type":27,"tag":45,"props":682,"children":684},{"id":683},"why-auto-generated-files-can-hurt",[685],{"type":32,"value":686},"Why auto-generated files can hurt",{"type":27,"tag":28,"props":688,"children":689},{},[690,692,700,702,707],{"type":32,"value":691},"There is a great video by ",{"type":27,"tag":172,"props":693,"children":697},{"href":694,"rel":695},"https://www.youtube.com/watch?v=N9z-MsIqcMk",[696],"nofollow",[698],{"type":32,"value":699},"Matt Pocock",{"type":32,"value":701}," that goes into explanation on why using the ",{"type":27,"tag":653,"props":703,"children":705},{"className":704},[],[706],{"type":32,"value":678},{"type":32,"value":708}," command is not a very good idea.",{"type":27,"tag":28,"props":710,"children":711},{},[712,714,719,721,727,729,735],{"type":32,"value":713},"Your ",{"type":27,"tag":653,"props":715,"children":717},{"className":716},[],[718],{"type":32,"value":658},{"type":32,"value":720}," file should not contain things like ",{"type":27,"tag":653,"props":722,"children":724},{"className":723},[],[725],{"type":32,"value":726},"npm run dev",{"type":32,"value":728}," or other scripts that an average AI agent can already read from your ",{"type":27,"tag":653,"props":730,"children":732},{"className":731},[],[733],{"type":32,"value":734},"package.json",{"type":32,"value":736},". That kind of information just adds noise. And a noisy context window means worse output.",{"type":27,"tag":28,"props":738,"children":739},{},[740],{"type":32,"value":741},"Instead, it should be a place for things like:",{"type":27,"tag":105,"props":743,"children":744},{},[745,755,765],{"type":27,"tag":109,"props":746,"children":747},{},[748,753],{"type":27,"tag":79,"props":749,"children":750},{},[751],{"type":32,"value":752},"Custom project conventions",{"type":32,"value":754}," - naming patterns, file organization rules, or coding standards your team follows",{"type":27,"tag":109,"props":756,"children":757},{},[758,763],{"type":27,"tag":79,"props":759,"children":760},{},[761],{"type":32,"value":762},"Non-obvious architectural decisions",{"type":32,"value":764}," - why you chose a specific approach over a more common one",{"type":27,"tag":109,"props":766,"children":767},{},[768,773],{"type":27,"tag":79,"props":769,"children":770},{},[771],{"type":32,"value":772},"Business logic quirks",{"type":32,"value":774}," - edge cases, special rules, or domain-specific knowledge that isn't obvious from the code",{"type":27,"tag":28,"props":776,"children":777},{},[778,780,785],{"type":32,"value":779},"Think about all of the things that confuse your new colleagues. Things that you only learn after months of working on a project. That's what belongs into your ",{"type":27,"tag":653,"props":781,"children":783},{"className":782},[],[784],{"type":32,"value":658},{"type":32,"value":786}," file.",{"type":27,"tag":28,"props":788,"children":789},{},[790],{"type":32,"value":791},"Here's an example of what a useful entry might look like:",{"type":27,"tag":793,"props":794,"children":799},"pre",{"className":795,"code":797,"filename":658,"language":798,"meta":5},[796],"language-plain","## Authentication\n\nWe use a custom token refresh flow instead of the standard OAuth refresh.\nThis is because our auth provider has a 5-second delay on token refresh\nthat caused timeouts in production. The workaround is in `lib/auth/refresh.ts`.\nDo NOT refactor this to use the standard refresh flow.\n\n## Testing\n\nTests in the `e2e/` folder run against a staging database that resets\nevery night. Don't try to fix stale data by seeding - just wait for the\nnext reset or run `npm run seed:staging` manually.\n","plain",[800],{"type":27,"tag":653,"props":801,"children":802},{"__ignoreMap":5},[803],{"type":32,"value":797},{"type":27,"tag":28,"props":805,"children":806},{},[807],{"type":32,"value":808},"This is the kind of context that helps an AI agent make better decisions. It's information that can't be easily inferred from scanning the codebase.",{"type":27,"tag":45,"props":810,"children":812},{"id":811},"the-concept-is-not-perfect",[813],{"type":32,"value":814},"The concept is not perfect",{"type":27,"tag":28,"props":816,"children":817},{},[818,820,825,827,832],{"type":32,"value":819},"I feel it is important to say that the ",{"type":27,"tag":653,"props":821,"children":823},{"className":822},[],[824],{"type":32,"value":658},{"type":32,"value":826}," concept is not perfect. It needs constant updates because it rots like documentation. If you reference a file and that file moves, or you reference a folder and you actually move that folder in your project, then having that stale reference in your ",{"type":27,"tag":653,"props":828,"children":830},{"className":829},[],[831],{"type":32,"value":658},{"type":32,"value":833}," file is going to confuse the hell out of your agent.",{"type":27,"tag":28,"props":835,"children":836},{},[837],{"type":32,"value":838},"There's also the fact that some tools make their own choices on whether to include these files. They can potentially introduce noise to the context window, which is probably the main reason for that 3% performance decrease in the study.",{"type":27,"tag":45,"props":840,"children":842},{"id":841},"what-i-take-from-this",[843],{"type":32,"value":844},"What I take from this",{"type":27,"tag":28,"props":846,"children":847},{},[848],{"type":32,"value":849},"Overall, I think the study opened up a good discussion about AI workflows, context windows, and what actually matters when you interact with an AI agent. The key takeaways for me are:",{"type":27,"tag":851,"props":852,"children":853},"ol",{},[854,871,881,891],{"type":27,"tag":109,"props":855,"children":856},{},[857,862,864,869],{"type":27,"tag":79,"props":858,"children":859},{},[860],{"type":32,"value":861},"Don't blindly auto-generate your context files",{"type":32,"value":863}," - a ",{"type":27,"tag":653,"props":865,"children":867},{"className":866},[],[868],{"type":32,"value":678},{"type":32,"value":870}," command will fill your file with information your agent already has access to",{"type":27,"tag":109,"props":872,"children":873},{},[874,879],{"type":27,"tag":79,"props":875,"children":876},{},[877],{"type":32,"value":878},"Write them by hand",{"type":32,"value":880}," - human-written files showed a 4% improvement",{"type":27,"tag":109,"props":882,"children":883},{},[884,889],{"type":27,"tag":79,"props":885,"children":886},{},[887],{"type":32,"value":888},"Focus on non-obvious knowledge",{"type":32,"value":890}," - document what's hard to discover, not what's easy to find",{"type":27,"tag":109,"props":892,"children":893},{},[894,899],{"type":27,"tag":79,"props":895,"children":896},{},[897],{"type":32,"value":898},"Maintain your files",{"type":32,"value":900}," - treat them like documentation that needs regular updates",{"type":27,"tag":28,"props":902,"children":903},{},[904],{"type":32,"value":905},"But let me know what you think in the comments. As always, happy coding!",{"title":5,"searchDepth":320,"depth":320,"links":907},[908,909,910,911,912],{"id":598,"depth":320,"text":601},{"id":619,"depth":320,"text":622},{"id":683,"depth":320,"text":686},{"id":811,"depth":320,"text":814},{"id":841,"depth":320,"text":844},"content:agents-md-files-are-not-misleading-your-ai-agent:index.md","agents-md-files-are-not-misleading-your-ai-agent/index.md","agents-md-files-are-not-misleading-your-ai-agent/index",{"_path":917,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":918,"description":919,"date":920,"published":10,"slug":921,"tags":922,"image":925,"cypressVersion":17,"playwrightVersion":17,"vitestVersion":17,"readingTime":926,"body":931,"_type":329,"_id":1619,"_source":331,"_file":1620,"_stem":1621,"_extension":334},"/dont-let-ai-read-your-env-files","Don’t let A.I. read your .env files","Stop storing API keys and database passwords in plain text .env files. Learn how to use 1Password CLI to inject secrets at runtime for better security.","2025-12-11","dont-let-ai-read-your-env-files",[923,580,924,581],"security","1Password","env_files_uegzbl.png",{"text":927,"minutes":928,"time":929,"words":930},"5 min read",4.81,288600,962,{"type":24,"children":932,"toc":1601},[933,955,970,999,1010,1016,1028,1049,1061,1067,1079,1090,1095,1104,1115,1128,1137,1142,1148,1153,1228,1234,1239,1245,1259,1269,1275,1280,1285,1315,1321,1326,1335,1340,1346,1358,1367,1372,1384,1393,1398,1406,1411,1417,1429,1441,1450,1462,1475,1484,1496,1502,1507,1519,1524,1533,1546,1555,1561,1573,1578],{"type":27,"tag":28,"props":934,"children":935},{},[936,938,944,946,953],{"type":32,"value":937},"AI coding assistants like Claude Code, Cursor, and GitHub Copilot are becoming part of our daily workflow. They read our files, understand our codebase, and help us write code faster. But there's a problem - they can also read your ",{"type":27,"tag":653,"props":939,"children":941},{"className":940},[],[942],{"type":32,"value":943},".env",{"type":32,"value":945}," files. A story has been recently ",{"type":27,"tag":172,"props":947,"children":950},{"href":948,"rel":949},"https://www.linkedin.com/posts/abhishekray00_i-dont-know-if-this-screenshot-is-real-but-activity-7404195631377883136-MzfR",[696],[951],{"type":32,"value":952},"circulating on social media",{"type":32,"value":954},", but I also experienced this firsthand:",{"type":27,"tag":28,"props":956,"children":957},{},[958],{"type":27,"tag":959,"props":960,"children":969},"img",{"alt":961,"className":962,"src":968},"Claude Code reading my .env file",[963,964,965,966,967],"shadow-block-mint","mx-auto","block","border-2","border-black","env_read_yokdxk.png",[],{"type":27,"tag":28,"props":971,"children":972},{},[973,975,980,982,989,991,997],{"type":32,"value":974},"Some AI tools such as Cursor don’t read yout ",{"type":27,"tag":653,"props":976,"children":978},{"className":977},[],[979],{"type":32,"value":943},{"type":32,"value":981}," file ",{"type":27,"tag":172,"props":983,"children":986},{"href":984,"rel":985},"https://cursor.com/docs/context/ignore-files#global-ignore-files",[696],[987],{"type":32,"value":988},"by default",{"type":32,"value":990},", but on the other hand, Claude code will take a peek unless you tell it not to. A good way to prevent this is tu set up your ",{"type":27,"tag":653,"props":992,"children":994},{"className":993},[],[995],{"type":32,"value":996},"~/.claude/settings.json",{"type":32,"value":998}," file that sets the default rule for all your projects.",{"type":27,"tag":793,"props":1000,"children":1005},{"className":1001,"code":1003,"filename":996,"language":1004,"meta":5},[1002],"language-json","{\n  \"permissions\": {\n    \"read\": {\n      \"deny\": [\n        \"**/.env*\",\n        \"**/*.pem\",\n        \"**/*.key\",\n        \"**/secrets/**\",\n        \"**/credentials/**\",\n        \"**/.aws/**\",\n        \"**/.ssh/**\",\n        \"**/docker-compose*.yml\",\n        \"**/config/database.yml\"\n      ]\n    }\n  }\n}\n","json",[1006],{"type":27,"tag":653,"props":1007,"children":1008},{"__ignoreMap":5},[1009],{"type":32,"value":1003},{"type":27,"tag":45,"props":1011,"children":1013},{"id":1012},"why-is-this-a-problem",[1014],{"type":32,"value":1015},"Why is this a problem",{"type":27,"tag":28,"props":1017,"children":1018},{},[1019,1021,1026],{"type":32,"value":1020},"When an AI assistant reads your ",{"type":27,"tag":653,"props":1022,"children":1024},{"className":1023},[],[1025],{"type":32,"value":943},{"type":32,"value":1027}," file, those secrets get sent to an LLM. Your API keys, database passwords, and other credentials become part of the context window. This is a security risk that sometimes gets overlooked until it's too late.",{"type":27,"tag":1029,"props":1030,"children":1031},"blockquote",{},[1032,1044],{"type":27,"tag":1033,"props":1034,"children":1036},"h3",{"id":1035},"if-this-has-happened-to-you",[1037,1039],{"type":32,"value":1038},"🚨 ",{"type":27,"tag":79,"props":1040,"children":1041},{},[1042],{"type":32,"value":1043},"If this has happened to you:",{"type":27,"tag":28,"props":1045,"children":1046},{},[1047],{"type":32,"value":1048},"Treat any leaked secret as compromised. Accidentally pasting your API key to Google, sending it through Slack, or just commiting it to a repository is a security risk and needs to be addressed immediately. Revoke the API key immediately and generate a new one. If you're working on a team, let them know so they can rotate the key on their end as well. It’s better to admit the mistake than to have it exploited. Happens to the best of us.",{"type":27,"tag":28,"props":1050,"children":1051},{},[1052,1054,1059],{"type":32,"value":1053},"The good news is that there's a solution that can help you keep your secrets safe, while utilizing the simplicity of the ",{"type":27,"tag":653,"props":1055,"children":1057},{"className":1056},[],[1058],{"type":32,"value":943},{"type":32,"value":1060}," file. In this blog I would like to show you how to set this up using 1Password CLI tool. There are other providers such as Bitwarden, Doppler, or even solutions from AWS, Google, and Azure that provide similar functionality.",{"type":27,"tag":45,"props":1062,"children":1064},{"id":1063},"how-it-works",[1065],{"type":32,"value":1066},"How It Works",{"type":27,"tag":28,"props":1068,"children":1069},{},[1070,1072,1077],{"type":32,"value":1071},"The concept is simple. Instead of storing actual secret values in your ",{"type":27,"tag":653,"props":1073,"children":1075},{"className":1074},[],[1076],{"type":32,"value":943},{"type":32,"value":1078}," file like this:",{"type":27,"tag":793,"props":1080,"children":1085},{"className":1081,"code":1083,"language":1084,"meta":5},[1082],"language-bash","# Traditional .env file - secrets in plain text\nDATABASE_URL=postgres://user:password123@localhost/mydb\nAPI_KEY=sk-abc123secret\n","bash",[1086],{"type":27,"tag":653,"props":1087,"children":1088},{"__ignoreMap":5},[1089],{"type":32,"value":1083},{"type":27,"tag":28,"props":1091,"children":1092},{},[1093],{"type":32,"value":1094},"You store references to secrets in your 1Password vault:",{"type":27,"tag":793,"props":1096,"children":1099},{"className":1097,"code":1098,"language":1084,"meta":5},[1082],"# With 1Password references - no actual secrets\nDATABASE_URL=\"op://Development/Database/connection_string\"\nAPI_KEY=\"op://Development/Stripe/api_key\"\n",[1100],{"type":27,"tag":653,"props":1101,"children":1102},{"__ignoreMap":5},[1103],{"type":32,"value":1098},{"type":27,"tag":28,"props":1105,"children":1106},{},[1107,1109],{"type":32,"value":1108},"The format follows this pattern: ",{"type":27,"tag":653,"props":1110,"children":1112},{"className":1111},[],[1113],{"type":32,"value":1114},"op://vault-name/item-name/field-name",{"type":27,"tag":28,"props":1116,"children":1117},{},[1118,1120,1126],{"type":32,"value":1119},"When you run your application, you use the ",{"type":27,"tag":653,"props":1121,"children":1123},{"className":1122},[],[1124],{"type":32,"value":1125},"op run",{"type":32,"value":1127}," command to inject the actual secrets at runtime:",{"type":27,"tag":793,"props":1129,"children":1132},{"className":1130,"code":1131,"language":1084,"meta":5},[1082],"op run --env-file=.env -- npm run dev\n",[1133],{"type":27,"tag":653,"props":1134,"children":1135},{"__ignoreMap":5},[1136],{"type":32,"value":1131},{"type":27,"tag":28,"props":1138,"children":1139},{},[1140],{"type":32,"value":1141},"1Password intercepts the secret references and replaces them with real values from your vault. The secrets only exist in memory during execution - they're never written to disk.",{"type":27,"tag":45,"props":1143,"children":1145},{"id":1144},"why-this-is-better",[1146],{"type":32,"value":1147},"Why This Is Better",{"type":27,"tag":28,"props":1149,"children":1150},{},[1151],{"type":32,"value":1152},"This approach gives you several advantages:",{"type":27,"tag":851,"props":1154,"children":1155},{},[1156,1181,1198,1208,1218],{"type":27,"tag":109,"props":1157,"children":1158},{},[1159,1164,1166,1171,1173,1179],{"type":27,"tag":79,"props":1160,"children":1161},{},[1162],{"type":32,"value":1163},"Safe from AI assistants",{"type":32,"value":1165}," - When Claude Code or Cursor reads your ",{"type":27,"tag":653,"props":1167,"children":1169},{"className":1168},[],[1170],{"type":32,"value":943},{"type":32,"value":1172}," file, they only see references like ",{"type":27,"tag":653,"props":1174,"children":1176},{"className":1175},[],[1177],{"type":32,"value":1178},"op://Work/Stripe/api_key",{"type":32,"value":1180},", not actual secrets",{"type":27,"tag":109,"props":1182,"children":1183},{},[1184,1189,1191,1196],{"type":27,"tag":79,"props":1185,"children":1186},{},[1187],{"type":32,"value":1188},"No secrets in version control",{"type":32,"value":1190}," - You can safely commit your ",{"type":27,"tag":653,"props":1192,"children":1194},{"className":1193},[],[1195],{"type":32,"value":943},{"type":32,"value":1197}," file because it only contains references",{"type":27,"tag":109,"props":1199,"children":1200},{},[1201,1206],{"type":27,"tag":79,"props":1202,"children":1203},{},[1204],{"type":32,"value":1205},"Team sharing",{"type":32,"value":1207}," - Secrets are shared via 1Password vaults, not copied between machines or sent through Slack",{"type":27,"tag":109,"props":1209,"children":1210},{},[1211,1216],{"type":27,"tag":79,"props":1212,"children":1213},{},[1214],{"type":32,"value":1215},"Instant rotation",{"type":32,"value":1217}," - Update a secret in 1Password, and all team members get it immediately",{"type":27,"tag":109,"props":1219,"children":1220},{},[1221,1226],{"type":27,"tag":79,"props":1222,"children":1223},{},[1224],{"type":32,"value":1225},"Audit trail",{"type":32,"value":1227}," - 1Password logs who accessed what secrets and when",{"type":27,"tag":45,"props":1229,"children":1231},{"id":1230},"setting-it-up",[1232],{"type":32,"value":1233},"Setting It Up",{"type":27,"tag":28,"props":1235,"children":1236},{},[1237],{"type":32,"value":1238},"Let me walk you through the setup process. I'm using macOS, but the steps are similar for other platforms.",{"type":27,"tag":1033,"props":1240,"children":1242},{"id":1241},"step-1-install-1password-cli",[1243],{"type":32,"value":1244},"Step 1: Install 1Password CLI",{"type":27,"tag":28,"props":1246,"children":1247},{},[1248,1250,1257],{"type":32,"value":1249},"If you don't have Homebrew installed, you'll need to ",{"type":27,"tag":172,"props":1251,"children":1254},{"href":1252,"rel":1253},"https://brew.sh/",[696],[1255],{"type":32,"value":1256},"install it first",{"type":32,"value":1258},". Then run:",{"type":27,"tag":793,"props":1260,"children":1264},{"className":1261,"code":1262,"filename":1263,"language":1084,"meta":5},[1082],"# macOS\nbrew install --cask 1password-cli\n# Windows\nwinget install 1password-cli\n","macOS, Windows",[1265],{"type":27,"tag":653,"props":1266,"children":1267},{"__ignoreMap":5},[1268],{"type":32,"value":1262},{"type":27,"tag":1033,"props":1270,"children":1272},{"id":1271},"step-2-enable-desktop-app-integration",[1273],{"type":32,"value":1274},"Step 2: Enable Desktop App Integration",{"type":27,"tag":28,"props":1276,"children":1277},{},[1278],{"type":32,"value":1279},"This step is important - it allows the CLI to authenticate through the 1Password desktop app, which means you can use Touch ID instead of typing your master password every time.",{"type":27,"tag":28,"props":1281,"children":1282},{},[1283],{"type":32,"value":1284},"Open your 1Password desktop app and:",{"type":27,"tag":851,"props":1286,"children":1287},{},[1288,1293,1305],{"type":27,"tag":109,"props":1289,"children":1290},{},[1291],{"type":32,"value":1292},"Go to Settings",{"type":27,"tag":109,"props":1294,"children":1295},{},[1296,1298,1303],{"type":32,"value":1297},"Go to the ",{"type":27,"tag":79,"props":1299,"children":1300},{},[1301],{"type":32,"value":1302},"Developer",{"type":32,"value":1304}," section",{"type":27,"tag":109,"props":1306,"children":1307},{},[1308,1310],{"type":32,"value":1309},"Enable ",{"type":27,"tag":79,"props":1311,"children":1312},{},[1313],{"type":32,"value":1314},"\"Integrate with 1Password CLI\"",{"type":27,"tag":1033,"props":1316,"children":1318},{"id":1317},"step-3-verify-the-connection",[1319],{"type":32,"value":1320},"Step 3: Verify the Connection",{"type":27,"tag":28,"props":1322,"children":1323},{},[1324],{"type":32,"value":1325},"Test that everything is connected properly:",{"type":27,"tag":793,"props":1327,"children":1330},{"className":1328,"code":1329,"language":1084,"meta":5},[1082],"op vault list\n",[1331],{"type":27,"tag":653,"props":1332,"children":1333},{"__ignoreMap":5},[1334],{"type":32,"value":1329},{"type":27,"tag":28,"props":1336,"children":1337},{},[1338],{"type":32,"value":1339},"You should see a list of your vaults. If this works, you're all set up.",{"type":27,"tag":45,"props":1341,"children":1343},{"id":1342},"using-it-in-your-projects",[1344],{"type":32,"value":1345},"Using It in Your Projects",{"type":27,"tag":28,"props":1347,"children":1348},{},[1349,1351,1356],{"type":32,"value":1350},"Now let's put this to work. Let’s say you have a project with a ",{"type":27,"tag":653,"props":1352,"children":1354},{"className":1353},[],[1355],{"type":32,"value":943},{"type":32,"value":1357}," file that looks like this:",{"type":27,"tag":793,"props":1359,"children":1362},{"className":1360,"code":1361,"language":1084,"meta":5},[1082],"DATABASE_URL=postgres://user:secretpassword@db.example.com/myapp\nSTRIPE_SECRET_KEY=sk_live_abc123\nOPENAI_API_KEY=sk-openai-xyz789\n",[1363],{"type":27,"tag":653,"props":1364,"children":1365},{"__ignoreMap":5},[1366],{"type":32,"value":1361},{"type":27,"tag":28,"props":1368,"children":1369},{},[1370],{"type":32,"value":1371},"First, create these secrets in 1Password. I recommend creating a dedicated vault for development secrets, or organizing them in your existing Work vault.",{"type":27,"tag":28,"props":1373,"children":1374},{},[1375,1377,1382],{"type":32,"value":1376},"Then update your ",{"type":27,"tag":653,"props":1378,"children":1380},{"className":1379},[],[1381],{"type":32,"value":943},{"type":32,"value":1383}," file to use references:",{"type":27,"tag":793,"props":1385,"children":1388},{"className":1386,"code":1387,"language":1084,"meta":5},[1082],"DATABASE_URL=\"op://Work/Database Production/connection_string\"\nSTRIPE_SECRET_KEY=\"op://Work/Stripe/secret_key\"\nOPENAI_API_KEY=\"op://Work/OpenAI/api_key\"\n",[1389],{"type":27,"tag":653,"props":1390,"children":1391},{"__ignoreMap":5},[1392],{"type":32,"value":1387},{"type":27,"tag":28,"props":1394,"children":1395},{},[1396],{"type":32,"value":1397},"Now run your application with:",{"type":27,"tag":793,"props":1399,"children":1401},{"className":1400,"code":1131,"language":1084,"meta":5},[1082],[1402],{"type":27,"tag":653,"props":1403,"children":1404},{"__ignoreMap":5},[1405],{"type":32,"value":1131},{"type":27,"tag":28,"props":1407,"children":1408},{},[1409],{"type":32,"value":1410},"The CLI will prompt for authentication (or use Touch ID if you have that set up), fetch the secrets from your vault, and inject them as environment variables.",{"type":27,"tag":45,"props":1412,"children":1414},{"id":1413},"making-it-less-verbose",[1415],{"type":32,"value":1416},"Making It Less Verbose",{"type":27,"tag":28,"props":1418,"children":1419},{},[1420,1422,1427],{"type":32,"value":1421},"You might be wondering if you have to type ",{"type":27,"tag":653,"props":1423,"children":1425},{"className":1424},[],[1426],{"type":32,"value":1125},{"type":32,"value":1428}," every time. You have options here.",{"type":27,"tag":28,"props":1430,"children":1431},{},[1432,1434,1439],{"type":32,"value":1433},"You can update your ",{"type":27,"tag":653,"props":1435,"children":1437},{"className":1436},[],[1438],{"type":32,"value":734},{"type":32,"value":1440}," scripts:",{"type":27,"tag":793,"props":1442,"children":1445},{"className":1443,"code":1444,"language":1004,"meta":5},[1002],"{\n  \"scripts\": {\n    \"dev\": \"op run --env-file=.env -- next dev\",\n    \"start\": \"op run --env-file=.env -- node server.js\"\n  }\n}\n",[1446],{"type":27,"tag":653,"props":1447,"children":1448},{"__ignoreMap":5},[1449],{"type":32,"value":1444},{"type":27,"tag":28,"props":1451,"children":1452},{},[1453,1455,1460],{"type":32,"value":1454},"Now ",{"type":27,"tag":653,"props":1456,"children":1458},{"className":1457},[],[1459],{"type":32,"value":726},{"type":32,"value":1461}," automatically uses 1Password.",{"type":27,"tag":28,"props":1463,"children":1464},{},[1465,1467,1473],{"type":32,"value":1466},"Or create a shell alias in your ",{"type":27,"tag":653,"props":1468,"children":1470},{"className":1469},[],[1471],{"type":32,"value":1472},".zshrc",{"type":32,"value":1474},":",{"type":27,"tag":793,"props":1476,"children":1479},{"className":1477,"code":1478,"language":1084,"meta":5},[1082],"alias dev=\"op run --env-file=.env -- npm run dev\"\n",[1480],{"type":27,"tag":653,"props":1481,"children":1482},{"__ignoreMap":5},[1483],{"type":32,"value":1478},{"type":27,"tag":28,"props":1485,"children":1486},{},[1487,1489,1494],{"type":32,"value":1488},"That said, I actually appreciate the small friction of ",{"type":27,"tag":653,"props":1490,"children":1492},{"className":1491},[],[1493],{"type":32,"value":1125},{"type":32,"value":1495},". It reminds me that secrets are being accessed, which keeps security top of mind.",{"type":27,"tag":45,"props":1497,"children":1499},{"id":1498},"debugging",[1500],{"type":32,"value":1501},"Debugging",{"type":27,"tag":28,"props":1503,"children":1504},{},[1505],{"type":32,"value":1506},"With credentials hidden this well, it can someone get confusing when trying to debug a problem. 1Password CLI will not print out your environment variables into terminal. Let’s say you have a script that looks like this:",{"type":27,"tag":793,"props":1508,"children":1514},{"className":1509,"code":1511,"filename":1512,"language":1513,"meta":5},[1510],"language-js","console.log(process.env.DATABASE_URL);\n","index.js","js",[1515],{"type":27,"tag":653,"props":1516,"children":1517},{"__ignoreMap":5},[1518],{"type":32,"value":1511},{"type":27,"tag":28,"props":1520,"children":1521},{},[1522],{"type":32,"value":1523},"Running this script will still print out just the reference to the secret, not the actual value.",{"type":27,"tag":793,"props":1525,"children":1528},{"className":1526,"code":1527,"language":1084,"meta":5},[1082],"op run node index.js\n# output\nop://Work/Database Production/connection_string\n",[1529],{"type":27,"tag":653,"props":1530,"children":1531},{"__ignoreMap":5},[1532],{"type":32,"value":1527},{"type":27,"tag":28,"props":1534,"children":1535},{},[1536,1538,1544],{"type":32,"value":1537},"To reveal the actual value, you can use the ",{"type":27,"tag":653,"props":1539,"children":1541},{"className":1540},[],[1542],{"type":32,"value":1543},"--no-masking",{"type":32,"value":1545}," flag:",{"type":27,"tag":793,"props":1547,"children":1550},{"className":1548,"code":1549,"language":1084,"meta":5},[1082],"op run --no-masking node index.js\n# output\npostgres://user:secretpassword@db.example.com/myapp\n",[1551],{"type":27,"tag":653,"props":1552,"children":1553},{"__ignoreMap":5},[1554],{"type":32,"value":1549},{"type":27,"tag":45,"props":1556,"children":1558},{"id":1557},"wrapping-up",[1559],{"type":32,"value":1560},"Wrapping Up",{"type":27,"tag":28,"props":1562,"children":1563},{},[1564,1566,1571],{"type":32,"value":1565},"For me personally, this change made me more confident that I’m not accidentally leaking my keys to A.I. assistants. It also took away the chore of finding my API keys in ",{"type":27,"tag":653,"props":1567,"children":1569},{"className":1568},[],[1570],{"type":32,"value":943},{"type":32,"value":1572}," files from other projects, when I’m experimenting with multiple small projects at once",{"type":27,"tag":28,"props":1574,"children":1575},{},[1576],{"type":32,"value":1577},"The setup takes about five minutes, and the workflow change is minimal. If you're already using 1Password for personal passwords, extending it to development secrets is a natural next step.",{"type":27,"tag":28,"props":1579,"children":1580},{},[1581,1583,1590,1592,1599],{"type":32,"value":1582},"Hope this helps! If you found this useful, consider sharing it with your team. You can follow me on ",{"type":27,"tag":172,"props":1584,"children":1587},{"href":1585,"rel":1586},"https://twitter.com/filiphric",[696],[1588],{"type":32,"value":1589},"Twitter",{"type":32,"value":1591}," or ",{"type":27,"tag":172,"props":1593,"children":1596},{"href":1594,"rel":1595},"https://www.linkedin.com/in/filiphric/",[696],[1597],{"type":32,"value":1598},"LinkedIn",{"type":32,"value":1600}," for more content like this.",{"title":5,"searchDepth":320,"depth":320,"links":1602},[1603,1608,1609,1610,1615,1616,1617,1618],{"id":1012,"depth":320,"text":1015,"children":1604},[1605],{"id":1035,"depth":1606,"text":1607},3,"🚨 If this has happened to you:",{"id":1063,"depth":320,"text":1066},{"id":1144,"depth":320,"text":1147},{"id":1230,"depth":320,"text":1233,"children":1611},[1612,1613,1614],{"id":1241,"depth":1606,"text":1244},{"id":1271,"depth":1606,"text":1274},{"id":1317,"depth":1606,"text":1320},{"id":1342,"depth":320,"text":1345},{"id":1413,"depth":320,"text":1416},{"id":1498,"depth":320,"text":1501},{"id":1557,"depth":320,"text":1560},"content:dont-let-ai-read-your-env-files:index.md","dont-let-ai-read-your-env-files/index.md","dont-let-ai-read-your-env-files/index",{"_path":1623,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":1624,"description":1625,"date":1626,"published":10,"slug":1627,"tags":1628,"image":1632,"cypressVersion":17,"playwrightVersion":17,"vitestVersion":17,"cursorVersion":17,"readingTime":1633,"body":1637,"_type":329,"_id":1918,"_source":331,"_file":1919,"_stem":1920,"_extension":334},"/how-i-automated-the-annoying-part-of-my-job-with-goose-and-playwright-mcp","How I automated the annoying part of my job with Goose and Playwright MCP","Learn how to automate repetitive issue creation tasks using Goose desktop app and Playwright MCP. Stop context switching and let AI handle the boring stuff.","2025-11-27","how-i-automated-the-annoying-part-of-my-job-with-goose-and-playwright-mcp",[579,1629,1630,1631],"playwright","automation","productivity","goose_yht2yr.png",{"text":927,"minutes":1634,"time":1635,"words":1636},4.66,279600,932,{"type":24,"children":1638,"toc":1906},[1639,1644,1649,1654,1660,1672,1678,1691,1696,1702,1707,1712,1717,1723,1728,1742,1747,1752,1758,1763,1778,1784,1797,1807,1813,1818,1824,1829,1843,1848,1853,1859,1886],{"type":27,"tag":28,"props":1640,"children":1641},{},[1642],{"type":32,"value":1643},"Nobody likes context switching and I'm no different. When I'm deep at work, usually I jot down notes somewhere on the side and continue with my work.",{"type":27,"tag":28,"props":1645,"children":1646},{},[1647],{"type":32,"value":1648},"But after my focus session is done, I'm usually faced with a boring and lengthy task of submitting those notes to a ticketing system so that our team can work on my product feedback and bug reports.",{"type":27,"tag":28,"props":1650,"children":1651},{},[1652],{"type":32,"value":1653},"And because I love automating my problems away, I've decided to solve these issues with a bunch of the tools I've been trying out lately. Let me show you how I have used Goose, Playwright MCP, and their new Playwright MCP Bridge extension for Chrome to automate the boring task of submitting multiple tickets to our internal issue tracking system.",{"type":27,"tag":45,"props":1655,"children":1657},{"id":1656},"what-is-goose",[1658],{"type":32,"value":1659},"What is Goose?",{"type":27,"tag":28,"props":1661,"children":1662},{},[1663,1670],{"type":27,"tag":172,"props":1664,"children":1667},{"href":1665,"rel":1666},"https://block.github.io/goose/",[696],[1668],{"type":32,"value":1669},"Goose",{"type":32,"value":1671}," is an open-source AI agent developed by Block. Think of it as an AI assistant that can actually do things on your computer, not just chat about them. It works through extensions (which is their umbrella term for MCPs and other tooling) that give it capabilities like browsing the web, running code, or interacting with APIs and much more.",{"type":27,"tag":45,"props":1673,"children":1675},{"id":1674},"installing-goose-desktop",[1676],{"type":32,"value":1677},"Installing Goose Desktop",{"type":27,"tag":28,"props":1679,"children":1680},{},[1681,1683,1689],{"type":32,"value":1682},"The easiest way to get started is with the ",{"type":27,"tag":172,"props":1684,"children":1686},{"href":1665,"rel":1685},[696],[1687],{"type":32,"value":1688},"Goose Desktop app",{"type":32,"value":1690},". Once installed, you can configure which AI provider you want to use. This is one of the advantages of Goose. You can use whichever model you want. It's as if ChatGPT allowed you to use Anthropic, Mistral, or even your local models run by Ollama.",{"type":27,"tag":28,"props":1692,"children":1693},{},[1694],{"type":32,"value":1695},"After the initial setup, you'll see a clean chat interface. But we’re not her to just chat. To make it actually browse websites and interact with your issue tracker, we need to add the Playwright extension.",{"type":27,"tag":45,"props":1697,"children":1699},{"id":1698},"adding-playwright-mcp-extension",[1700],{"type":32,"value":1701},"Adding Playwright MCP Extension",{"type":27,"tag":28,"props":1703,"children":1704},{},[1705],{"type":32,"value":1706},"In Goose, MCP servers are called \"extensions.\" To add Playwright, open Extensions panel in Goose. You can add a MCP manually, but it’s easier to just click \"Browse Extensions\", find Playwright MCP in the list and hit \"Install\".",{"type":27,"tag":28,"props":1708,"children":1709},{},[1710],{"type":32,"value":1711},"And that's it. Goose now has the ability to open a browser, navigate to pages, click buttons, fill forms, and basically do anything you could do manually. The cool thing about Playwright MCP is that it's running in a constant feedback loop. This means that when you open a page and you say \"click a button\", Playwright MCP will be able to find that button even if it had moved recently or was renamed.",{"type":27,"tag":28,"props":1713,"children":1714},{},[1715],{"type":32,"value":1716},"But here's where things usually get tricky. By default, Playwright MCP launches a fresh browser instance for each task. This means that you'll need to log in every time you want to use it. You could technically give Goose your credentials and have it log in every time. But that's slow, potentially insecure, and doesn't work well with SSO or 2FA. The solution is to use a browser that's already logged in.",{"type":27,"tag":45,"props":1718,"children":1720},{"id":1719},"playwright-mcp-bridge-extension",[1721],{"type":32,"value":1722},"Playwright MCP Bridge Extension",{"type":27,"tag":28,"props":1724,"children":1725},{},[1726],{"type":32,"value":1727},"This is the second part of the solution. We have Playwright MCP installed in Goose, but new we’ll install a Chrome extension that can bridge the connection between Playwright MCP and your existing browser.",{"type":27,"tag":28,"props":1729,"children":1730},{},[1731,1733,1740],{"type":32,"value":1732},"Debbie O'Brien ",{"type":27,"tag":172,"props":1734,"children":1737},{"href":1735,"rel":1736},"https://dev.to/debs_obrien/testing-in-a-logged-in-state-with-the-playwright-mcp-browser-extension-4cmg",[696],[1738],{"type":32,"value":1739},"wrote a great article",{"type":32,"value":1741}," explaining this approach.",{"type":27,"tag":28,"props":1743,"children":1744},{},[1745],{"type":32,"value":1746},"The idea is simple: instead of Playwright launching a fresh browser instance, it connects to your existing browser where you're already logged in.",{"type":27,"tag":28,"props":1748,"children":1749},{},[1750],{"type":32,"value":1751},"Here's how to set it up:",{"type":27,"tag":1033,"props":1753,"children":1755},{"id":1754},"_1-install-the-browser-extension",[1756],{"type":32,"value":1757},"1. Install the Browser Extension",{"type":27,"tag":28,"props":1759,"children":1760},{},[1761],{"type":32,"value":1762},"First, install the Playwright MCP Bridge extension in your browser. It's available for Chrome and other Chromium-based browsers.",{"type":27,"tag":1029,"props":1764,"children":1765},{},[1766],{"type":27,"tag":28,"props":1767,"children":1768},{},[1769,1771],{"type":32,"value":1770},"Currently, it cannot be found on Google Chrome Web Store. You'll need to download it and import that manually. ",{"type":27,"tag":172,"props":1772,"children":1775},{"href":1773,"rel":1774},"https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md",[696],[1776],{"type":32,"value":1777},"Detailed instructions can be found here.",{"type":27,"tag":1033,"props":1779,"children":1781},{"id":1780},"_2-configure-goose-to-use-the-bridge",[1782],{"type":32,"value":1783},"2. Configure Goose to Use the Bridge",{"type":27,"tag":28,"props":1785,"children":1786},{},[1787,1789,1795],{"type":32,"value":1788},"In your Goose extensions configuration, update the Playwright settings to use the bridge connection. This is done by passing ",{"type":27,"tag":653,"props":1790,"children":1792},{"className":1791},[],[1793],{"type":32,"value":1794},"--extension",{"type":32,"value":1796}," flag to the Playwright MCP settings.",{"type":27,"tag":28,"props":1798,"children":1799},{},[1800],{"type":27,"tag":959,"props":1801,"children":1806},{"alt":1802,"className":1803,"src":1805},"Goose extensions configuration",[1804,967,966],"shadow-block-cheese","goose_playwright_setup.png",[],{"type":27,"tag":1033,"props":1808,"children":1810},{"id":1809},"_3-pass-the-bridge-token-to-goose",[1811],{"type":32,"value":1812},"3. Pass the Bridge Token to Goose",{"type":27,"tag":28,"props":1814,"children":1815},{},[1816],{"type":32,"value":1817},"This step is optional, but if you want to avoid needing to confirm the browser automation every time, you need to set this up. In Chrome, click the Playwright MCP Bridge extension icon. This reveals your unique token, which you can pass to Goose as an environment variable.",{"type":27,"tag":45,"props":1819,"children":1821},{"id":1820},"putting-it-all-together",[1822],{"type":32,"value":1823},"Putting It All Together",{"type":27,"tag":28,"props":1825,"children":1826},{},[1827],{"type":32,"value":1828},"And now we are ready to rock.",{"type":27,"tag":28,"props":1830,"children":1831},{},[1832,1834,1841],{"type":32,"value":1833},"From this point on, it's just a matter of instructing Goose to do the thing that you want. I like to use ",{"type":27,"tag":172,"props":1835,"children":1838},{"href":1836,"rel":1837},"https://superwhisper.com/",[696],[1839],{"type":32,"value":1840},"Superwhisper",{"type":32,"value":1842},", so I basically just read my notes out loud and tell Goose to add issues one by one to our issue tracker.",{"type":27,"tag":28,"props":1844,"children":1845},{},[1846],{"type":32,"value":1847},"A really great thing about using Playwright MCP is that there’s no need to figure out the details of where each element on page is (as we do in test automation). Once the goal is clear, Playwright MCP is able to figure out what it needs to do. For a simple application like an issue tracker, it is obvious where the title should be, what the description should be, and how to submit another issue.",{"type":27,"tag":28,"props":1849,"children":1850},{},[1851],{"type":32,"value":1852},"At this point, I don't even watch the browser execution. I just put it on the side screen and get to my next thing in my to-do list. Once Goose is done, automating the task, it'll send me a notification.",{"type":27,"tag":45,"props":1854,"children":1856},{"id":1855},"some-final-tips",[1857],{"type":32,"value":1858},"Some final tips",{"type":27,"tag":105,"props":1860,"children":1861},{},[1862,1867,1881],{"type":27,"tag":109,"props":1863,"children":1864},{},[1865],{"type":32,"value":1866},"Keep a good balance between a thorough explanation and brevity. You don't want to use long prompts, but also don't want to be too vague",{"type":27,"tag":109,"props":1868,"children":1869},{},[1870,1872,1879],{"type":32,"value":1871},"Once you have a task that you automate often, consider ",{"type":27,"tag":172,"props":1873,"children":1876},{"href":1874,"rel":1875},"https://block.github.io/goose/docs/tutorials/recipes-tutorial/",[696],[1877],{"type":32,"value":1878},"creating a Recipe",{"type":32,"value":1880}," for it. This will allow you to avoid writing instructions every time",{"type":27,"tag":109,"props":1882,"children":1883},{},[1884],{"type":32,"value":1885},"Choose a cheaper model. Automation is not that complicated, so you don’t need to use the latest and greatest. An older and cheaper model will do the job just fine",{"type":27,"tag":28,"props":1887,"children":1888},{},[1889,1891,1897,1898,1904],{"type":32,"value":1890},"Hope this helps! If you set this up or have questions, reach out on ",{"type":27,"tag":172,"props":1892,"children":1895},{"href":1893,"rel":1894},"https://twitter.com/filip_hric",[696],[1896],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":1899,"children":1902},{"href":1900,"rel":1901},"https://www.linkedin.com/in/filip-hric/",[696],[1903],{"type":32,"value":1598},{"type":32,"value":1905},". I'd love to hear how you're using AI agents in your workflow.",{"title":5,"searchDepth":320,"depth":320,"links":1907},[1908,1909,1910,1911,1916,1917],{"id":1656,"depth":320,"text":1659},{"id":1674,"depth":320,"text":1677},{"id":1698,"depth":320,"text":1701},{"id":1719,"depth":320,"text":1722,"children":1912},[1913,1914,1915],{"id":1754,"depth":1606,"text":1757},{"id":1780,"depth":1606,"text":1783},{"id":1809,"depth":1606,"text":1812},{"id":1820,"depth":320,"text":1823},{"id":1855,"depth":320,"text":1858},"content:how-i-automated-the-annoying-part-of-my-job-with-goose-and-playwright-mcp:index.md","how-i-automated-the-annoying-part-of-my-job-with-goose-and-playwright-mcp/index.md","how-i-automated-the-annoying-part-of-my-job-with-goose-and-playwright-mcp/index",{"_path":1922,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":1923,"description":1924,"date":1925,"published":10,"slug":1926,"tags":1927,"image":1929,"cypressVersion":17,"playwrightVersion":1930,"vitestVersion":17,"cursorVersion":1931,"readingTime":1932,"body":1937,"_type":329,"_id":2421,"_source":331,"_file":2422,"_stem":2423,"_extension":334},"/cursor-playwright-tips","10 Tips for Writing Playwright Tests with Cursor","Practical tips gathered from months of writing Playwright tests with Cursor. Learn how to use project rules, workflows, screenshots, and MCP for better test automation.","2025-10-13","cursor-playwright-tips",[1629,579,13,1928],"cursor","playwright_cursor_d3a5mi.png","1.56.0","1.7.0",{"text":1933,"minutes":1934,"time":1935,"words":1936},"7 min read",6.88,412800,1376,{"type":24,"children":1938,"toc":2409},[1939,1944,1949,1955,1960,1965,1971,1976,1981,2019,2024,2037,2048,2061,2067,2072,2077,2082,2088,2093,2098,2119,2128,2134,2146,2151,2172,2178,2183,2195,2213,2218,2224,2229,2240,2245,2256,2261,2266,2272,2277,2291,2314,2319,2325,2330,2335,2349,2354,2360,2372,2399,2404],{"type":27,"tag":28,"props":1940,"children":1941},{},[1942],{"type":32,"value":1943},"If you've been following the A.I. coding assistant space, you've probably noticed how fast things are moving. New models drop every month, and everyone's trying to figure out the \"right way\" to work with these tools. I've spent the last couple of months writing Playwright tests with Cursor, and honestly, there's been a lot of trial and error. Some things worked brilliantly, others... not so much.",{"type":27,"tag":28,"props":1945,"children":1946},{},[1947],{"type":32,"value":1948},"I decided to gather everything I've learned into this post. Let's dive in.",{"type":27,"tag":45,"props":1950,"children":1952},{"id":1951},"_1-stick-with-auto-mode",[1953],{"type":32,"value":1954},"1. Stick with \"Auto\" mode",{"type":27,"tag":28,"props":1956,"children":1957},{},[1958],{"type":32,"value":1959},"This might be a hot take: you don't need to chase every new model release. I know it's tempting to go for the latest and greatest. All the models promise to be better, more efficient, and more powerful than the last. In my experience, switching from model to model made me readjust the \"feel\" for how a model operates.",{"type":27,"tag":28,"props":1961,"children":1962},{},[1963],{"type":32,"value":1964},"I've found that staying in \"auto\" mode and letting Cursor handle the model selection works really well. You're not wasting mental energy on which model to use for which task. Just focus on writing good prompts and let the tool do its thing.",{"type":27,"tag":45,"props":1966,"children":1968},{"id":1967},"_2-use-project-rules-to-describe-your-application",[1969],{"type":32,"value":1970},"2. Use project rules to describe your application",{"type":27,"tag":28,"props":1972,"children":1973},{},[1974],{"type":32,"value":1975},"This is probably the most important tip on this list. Project rules in Cursor are automatically added to context, which means they're always available when the A.I. generates code.",{"type":27,"tag":28,"props":1977,"children":1978},{},[1979],{"type":32,"value":1980},"I use rules to describe:",{"type":27,"tag":105,"props":1982,"children":1983},{},[1984,1989,1994,1999,2004,2009,2014],{"type":27,"tag":109,"props":1985,"children":1986},{},[1987],{"type":32,"value":1988},"Your application architecture",{"type":27,"tag":109,"props":1990,"children":1991},{},[1992],{"type":32,"value":1993},"Your testing approach and philosophy",{"type":27,"tag":109,"props":1995,"children":1996},{},[1997],{"type":32,"value":1998},"Database scripts and setup commands",{"type":27,"tag":109,"props":2000,"children":2001},{},[2002],{"type":32,"value":2003},"Test commands and how to run them",{"type":27,"tag":109,"props":2005,"children":2006},{},[2007],{"type":32,"value":2008},"Selector conventions (data-testid vs. role selectors, etc.)",{"type":27,"tag":109,"props":2010,"children":2011},{},[2012],{"type":32,"value":2013},"Coding style preferences",{"type":27,"tag":109,"props":2015,"children":2016},{},[2017],{"type":32,"value":2018},"Project structure and where things live",{"type":27,"tag":28,"props":2020,"children":2021},{},[2022],{"type":32,"value":2023},"Think of project rules as the manual your A.I. assistant reads before starting work. The better the manual, the better the work.",{"type":27,"tag":28,"props":2025,"children":2026},{},[2027,2029,2035],{"type":32,"value":2028},"You can also split rules into separate rule files in the ",{"type":27,"tag":653,"props":2030,"children":2032},{"className":2031},[],[2033],{"type":32,"value":2034},".cursor/rules",{"type":32,"value":2036}," folder. This way, each test file gets exactly the context it needs.",{"type":27,"tag":793,"props":2038,"children":2043},{"className":2039,"code":2041,"language":2042,"meta":5},[2040],"language-treeview",".cursor/\n└── rules/\n  ├── selectors.mdc\n  ├── test-structure.mdc\n  └── database.mdc\n","treeview",[2044],{"type":27,"tag":653,"props":2045,"children":2046},{"__ignoreMap":5},[2047],{"type":32,"value":2041},{"type":27,"tag":28,"props":2049,"children":2050},{},[2051,2053,2059],{"type":32,"value":2052},"You can also glob your rules for tests to specific ",{"type":27,"tag":653,"props":2054,"children":2056},{"className":2055},[],[2057],{"type":32,"value":2058},".spec.ts",{"type":32,"value":2060}," files and reference files, such as your Playwright config.",{"type":27,"tag":45,"props":2062,"children":2064},{"id":2063},"_3-dont-download-other-peoples-rules",[2065],{"type":32,"value":2066},"3. Don’t download other people's rules",{"type":27,"tag":28,"props":2068,"children":2069},{},[2070],{"type":32,"value":2071},"I tried downloading rule sets from GitHub in the beginning and I must admit that most of them weren't worth it. The main problem is that they are either too generic or designed for someone else's workflow.",{"type":27,"tag":28,"props":2073,"children":2074},{},[2075],{"type":32,"value":2076},"Another problem is that they numb you to how your your IDE works. Which explanation works well for the LLM? Which makes it halucinate? Without writing your own rules you are losing all that learning.",{"type":27,"tag":28,"props":2078,"children":2079},{},[2080],{"type":32,"value":2081},"Create your rules organically instead. When Cursor makes a mistake, go back to your rule file and adjust it so that you prevent it from happening again. When you find yourself giving the same instructions repeatedly, turn them into a rule.",{"type":27,"tag":45,"props":2083,"children":2085},{"id":2084},"_4-create-custom-commands",[2086],{"type":32,"value":2087},"4. Create custom commands",{"type":27,"tag":28,"props":2089,"children":2090},{},[2091],{"type":32,"value":2092},"Similarly to rules, custom commands set boundaries and guardrails for the A.I. agent. They serve a different purpose though. Custom commands usually describe a certain workflow or a set of steps for the A.I. to follow given a certain task.",{"type":27,"tag":28,"props":2094,"children":2095},{},[2096],{"type":32,"value":2097},"For example, when your tests follow Arrange - Act - Assert pattern, you can create a custom command that will describe a set of steps on how to follow this pattern.",{"type":27,"tag":28,"props":2099,"children":2100},{},[2101,2103,2109,2111,2117],{"type":32,"value":2102},"Custom commands can be easily created by typing ",{"type":27,"tag":653,"props":2104,"children":2106},{"className":2105},[],[2107],{"type":32,"value":2108},"/",{"type":32,"value":2110}," in the chat. They are saved to the ",{"type":27,"tag":653,"props":2112,"children":2114},{"className":2113},[],[2115],{"type":32,"value":2116},".cursor/commands",{"type":32,"value":2118}," folder. You can even use agent to generate the rule and refine details so that they reflect your coding style.",{"type":27,"tag":28,"props":2120,"children":2121},{},[2122],{"type":27,"tag":959,"props":2123,"children":2127},{"alt":2124,"className":2125,"src":2126},"Create custom command in Cursor",[1804,967,966],"cursor_create_command.png",[],{"type":27,"tag":45,"props":2129,"children":2131},{"id":2130},"_5-reference-app-code",[2132],{"type":32,"value":2133},"5. Reference app code",{"type":27,"tag":28,"props":2135,"children":2136},{},[2137,2139,2144],{"type":32,"value":2138},"I keep telling testing community that they need to ",{"type":27,"tag":79,"props":2140,"children":2141},{},[2142],{"type":32,"value":2143},"write their own selectors instead of handing it off to developers",{"type":32,"value":2145},". The era of black box testing is over and being technically savvy is not up for a debate. As a tester, understanding the structure of the application gives you so much power, so why would you want to hand it off to someone else?",{"type":27,"tag":28,"props":2147,"children":2148},{},[2149],{"type":32,"value":2150},"And A.I. can be a great help here. Let’s say you can't find the right component file for an element that you want to reference in your test. Simply open your browser Devtools, copy an element from the elements panel, and paste it into Cursor's chat. Since Cursor indexes your source code, you can ask it to explain stuff for you or help you locate the right file and right selector. How hard is to add your data attribute then?",{"type":27,"tag":1029,"props":2152,"children":2153},{},[2154],{"type":27,"tag":28,"props":2155,"children":2156},{},[2157,2162,2164,2170],{"type":27,"tag":79,"props":2158,"children":2159},{},[2160],{"type":32,"value":2161},"Extra tip:",{"type":32,"value":2163}," Use folder names in your prompts instead of the ",{"type":27,"tag":653,"props":2165,"children":2167},{"className":2166},[],[2168],{"type":32,"value":2169},"@folderName",{"type":32,"value":2171}," syntax or dropping folders into chat. This prevents Cursor from reading every single file in that folder, which would bloat your context window and make A.I. prone to mistakes.",{"type":27,"tag":45,"props":2173,"children":2175},{"id":2174},"_6-screenshots-can-be-more-helpful-than-text",[2176],{"type":32,"value":2177},"6. Screenshots can be more helpful than text",{"type":27,"tag":28,"props":2179,"children":2180},{},[2181],{"type":32,"value":2182},"Not sure why, but I was holding back on using screenshots for the longest time. That was a mistake.",{"type":27,"tag":28,"props":2184,"children":2185},{},[2186,2188,2193],{"type":32,"value":2187},"A.I. is ",{"type":27,"tag":302,"props":2189,"children":2190},{},[2191],{"type":32,"value":2192},"amazing",{"type":32,"value":2194}," at connecting visual context with code context. When you say:",{"type":27,"tag":105,"props":2196,"children":2197},{},[2198,2203,2208],{"type":27,"tag":109,"props":2199,"children":2200},{},[2201],{"type":32,"value":2202},"\"This is my page\" (screenshot)",{"type":27,"tag":109,"props":2204,"children":2205},{},[2206],{"type":32,"value":2207},"\"This is the test I written\" (reference file)",{"type":27,"tag":109,"props":2209,"children":2210},{},[2211],{"type":32,"value":2212},"\"This is how the page looks when the test fails\" (screenshot)",{"type":27,"tag":28,"props":2214,"children":2215},{},[2216],{"type":32,"value":2217},"the A.I. suddenly understands exactly what you're trying to do. It's like the difference between describing a room over the phone versus showing someone a photo.",{"type":27,"tag":45,"props":2219,"children":2221},{"id":2220},"_7-learn-how-to-use-playwright-mcp-efficiently",[2222],{"type":32,"value":2223},"7. Learn how to use Playwright MCP efficiently",{"type":27,"tag":28,"props":2225,"children":2226},{},[2227],{"type":32,"value":2228},"If you want to use Playwright MCP to generate your test, the results can be a bit odd and slow. I never had good results using it instead of codegen.",{"type":27,"tag":28,"props":2230,"children":2231},{},[2232,2234,2238],{"type":32,"value":2233},"I strongly believe that ",{"type":27,"tag":79,"props":2235,"children":2236},{},[2237],{"type":32,"value":1498},{"type":32,"value":2239}," is a much better use case for this tool. Playwright already collects a lot of information about the test run. This makes it really valuable when in need of providing info to the LLM. I tried to leverage this even before Playwright came up with their own MCP server. I created my own debugging tool that could figure out what went wrong with a test. That project ended up going nowhere, because Playwright MCP is actually good enough for this use case.",{"type":27,"tag":28,"props":2241,"children":2242},{},[2243],{"type":32,"value":2244},"If you want to debug a test that is failing you can start as simple as this prompt:",{"type":27,"tag":793,"props":2246,"children":2251},{"className":2247,"code":2249,"language":2250,"meta":5},[2248],"language-plaintext","\"This test fails on line 6. Open the browser, run the test,\nand tell me what's wrong.\"\n","plaintext",[2252],{"type":27,"tag":653,"props":2253,"children":2254},{"__ignoreMap":5},[2255],{"type":32,"value":2249},{"type":27,"tag":28,"props":2257,"children":2258},{},[2259],{"type":32,"value":2260},"The \"open the browser\" part will make sure that A.I. knows that it should call Playwright MCP.",{"type":27,"tag":28,"props":2262,"children":2263},{},[2264],{"type":32,"value":2265},"But you don’t have to stop there. You can combine multiple MCPs. For example, you can use the GitHub MCP to fetch your last CI test run, pick the failed test and then run Playwright MCP to debug the testlocally. The screenshots, traces, and snapshots provide the A.I. with exactly the right context to understand what went wrong.",{"type":27,"tag":45,"props":2267,"children":2269},{"id":2268},"_8-jump-outside-cursor-for-planning",[2270],{"type":32,"value":2271},"8. Jump Outside Cursor for Planning",{"type":27,"tag":28,"props":2273,"children":2274},{},[2275],{"type":32,"value":2276},"Cursor is a fantastic coding tool, but it’s important to understand that whenever you start a conversation with the LLM, Cursor adds a lot of its own information to it. Athough Cursor has recently implemented planning mode, I still find myself jumping outside Cursor for tasks that are not coding related.",{"type":27,"tag":28,"props":2278,"children":2279},{},[2280,2282,2289],{"type":32,"value":2281},"For example I use ",{"type":27,"tag":172,"props":2283,"children":2286},{"href":2284,"rel":2285},"https://github.com/yamadashy/repomix",[696],[2287],{"type":32,"value":2288},"repomix",{"type":32,"value":2290}," to bundle my source code into XML format, then paste it into Gemini (or Claude) to:",{"type":27,"tag":105,"props":2292,"children":2293},{},[2294,2299,2304,2309],{"type":27,"tag":109,"props":2295,"children":2296},{},[2297],{"type":32,"value":2298},"Create project rules",{"type":27,"tag":109,"props":2300,"children":2301},{},[2302],{"type":32,"value":2303},"Design workflows",{"type":27,"tag":109,"props":2305,"children":2306},{},[2307],{"type":32,"value":2308},"Generate test plan prompts",{"type":27,"tag":109,"props":2310,"children":2311},{},[2312],{"type":32,"value":2313},"Create mermaid diagrams for test architecture",{"type":27,"tag":28,"props":2315,"children":2316},{},[2317],{"type":32,"value":2318},"I find especially the prompt generation to be really useful. Whenever I’m unsure which parts of codebase are going to be relevant, this approach helps.",{"type":27,"tag":45,"props":2320,"children":2322},{"id":2321},"_9-testing-is-more-important-than-ever",[2323],{"type":32,"value":2324},"9. Testing Is More Important Than Ever",{"type":27,"tag":28,"props":2326,"children":2327},{},[2328],{"type":32,"value":2329},"Technically not a Cursor tip, but it needs to be said.",{"type":27,"tag":28,"props":2331,"children":2332},{},[2333],{"type":32,"value":2334},"With the rise of \"vibe coding\" platforms where A.I. generates entire applications, testing has become absolutely critical. Without testing, there's no way A.I. delivers good software. You can generate code at lightning speed, but if you don't verify it works, you're just moving faster toward broken software.",{"type":27,"tag":28,"props":2336,"children":2337},{},[2338,2340,2347],{"type":32,"value":2339},"At ",{"type":27,"tag":172,"props":2341,"children":2344},{"href":2342,"rel":2343},"https://nut.new",[696],[2345],{"type":32,"value":2346},"nut.new",{"type":32,"value":2348},", we're serious about not just writing full-stack apps, but running tests and verifying functionality. The A.I. can write the code, but it's the tests that give us confidence to ship.",{"type":27,"tag":28,"props":2350,"children":2351},{},[2352],{"type":32,"value":2353},"If you're using A.I. to write code, you need to be using A.I. to write tests too. There's no way around it.",{"type":27,"tag":45,"props":2355,"children":2357},{"id":2356},"_10-get-into-experimental-mindset",[2358],{"type":32,"value":2359},"10. Get into experimental mindset",{"type":27,"tag":28,"props":2361,"children":2362},{},[2363,2365,2370],{"type":32,"value":2364},"Even though I just threw 9 tips at you, it doesn’t mean that anything I said will work for you. I find myself customizing my coding experience so that it works for ",{"type":27,"tag":79,"props":2366,"children":2367},{},[2368],{"type":32,"value":2369},"me",{"type":32,"value":2371},". A tester’s mindset is golden here, we are natural at trying out new stuff, seeing what sticks and what doesn’t.",{"type":27,"tag":28,"props":2373,"children":2374},{},[2375,2377,2382,2383,2388,2390,2397],{"type":32,"value":2376},"Hope this helps! If you've got your own tips for working with Cursor and Playwright, I'd love to hear them. Drop me a message on ",{"type":27,"tag":172,"props":2378,"children":2380},{"href":1893,"rel":2379},[696],[2381],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":2384,"children":2386},{"href":1900,"rel":2385},[696],[2387],{"type":32,"value":1598},{"type":32,"value":2389},", or join my ",{"type":27,"tag":172,"props":2391,"children":2394},{"href":2392,"rel":2393},"https://filiphric.com/discord",[696],[2395],{"type":32,"value":2396},"Discord community",{"type":32,"value":2398}," where we talk about this stuff all the time.",{"type":27,"tag":28,"props":2400,"children":2401},{},[2402],{"type":32,"value":2403},"And if you found this useful, share it with your team—chances are they're struggling with the same things.",{"type":27,"tag":28,"props":2405,"children":2406},{},[2407],{"type":32,"value":2408},"Happy testing! 🎭",{"title":5,"searchDepth":320,"depth":320,"links":2410},[2411,2412,2413,2414,2415,2416,2417,2418,2419,2420],{"id":1951,"depth":320,"text":1954},{"id":1967,"depth":320,"text":1970},{"id":2063,"depth":320,"text":2066},{"id":2084,"depth":320,"text":2087},{"id":2130,"depth":320,"text":2133},{"id":2174,"depth":320,"text":2177},{"id":2220,"depth":320,"text":2223},{"id":2268,"depth":320,"text":2271},{"id":2321,"depth":320,"text":2324},{"id":2356,"depth":320,"text":2359},"content:cursor-playwright-tips:index.md","cursor-playwright-tips/index.md","cursor-playwright-tips/index",{"_path":2425,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":2426,"description":2427,"date":2428,"published":10,"slug":2429,"tags":2430,"image":2433,"cypressVersion":17,"playwrightVersion":2434,"vitestVersion":17,"readingTime":2435,"body":2440,"_type":329,"_id":2985,"_source":331,"_file":2986,"_stem":2987,"_extension":334},"/2fa-testing-with-playwright-and-mailosaur","2FA testing with Playwright and Mailosaur","Learn how to automate email magic links, SMS verification codes, and authenticator app logins in Playwright using Mailosaur for comprehensive authentication testing.","2025-10-03","2fa-testing-with-playwright-and-mailosaur",[1629,13,2431,2432],"authentication","mailosaur","2fa_mailosaur_xjsxtr.png","v1.56.0",{"text":2436,"minutes":2437,"time":2438,"words":2439},"8 min read",7.405,444300,1481,{"type":24,"children":2441,"toc":2973},[2442,2447,2452,2457,2471,2489,2494,2500,2505,2514,2519,2542,2554,2563,2568,2574,2579,2588,2593,2598,2603,2614,2653,2674,2685,2690,2699,2705,2710,2718,2723,2732,2737,2746,2767,2776,2782,2787,2792,2800,2805,2813,2818,2828,2833,2842,2847,2856,2862,2868,2873,2879,2892,2901,2907,2912,2921,2927,2932,2941,2946,2955],{"type":27,"tag":28,"props":2443,"children":2444},{},[2445],{"type":32,"value":2446},"When you're writing end-to-end tests, authentication can often become the first gatekeeper. You can't test the actual functionality of your app without first getting past the login screen. But modern authentication methods can make automation difficult, utilizing multiple factors that are difficult to automate (which is actually the point of 2FA).",{"type":27,"tag":28,"props":2448,"children":2449},{},[2450],{"type":32,"value":2451},"This is usually handled either by skipping these methods on test environments or some other workarounds. Some might argue this is not true e2e testing. To be honest, it’s probably a debate for another day, but there is definitely a merit to critiquing the approach of bypassing login.",{"type":27,"tag":28,"props":2453,"children":2454},{},[2455],{"type":32,"value":2456},"So how to handle authentication properly?",{"type":27,"tag":28,"props":2458,"children":2459},{},[2460,2462,2469],{"type":32,"value":2461},"For years now, my go-to solution has been ",{"type":27,"tag":172,"props":2463,"children":2466},{"href":2464,"rel":2465},"https://link.filiphric.com/mailosaur",[696],[2467],{"type":32,"value":2468},"Mailosaur",{"type":32,"value":2470},". It is a testing service that gives you virtual email addresses and phone numbers for automation. Think of it as a testing inbox that your tests can programmatically access. You can:",{"type":27,"tag":105,"props":2472,"children":2473},{},[2474,2479,2484],{"type":27,"tag":109,"props":2475,"children":2476},{},[2477],{"type":32,"value":2478},"Send emails to unique addresses and retrieve them via API",{"type":27,"tag":109,"props":2480,"children":2481},{},[2482],{"type":32,"value":2483},"Receive SMS messages to virtual phone numbers",{"type":27,"tag":109,"props":2485,"children":2486},{},[2487],{"type":32,"value":2488},"Generate authenticator app codes without a physical device",{"type":27,"tag":28,"props":2490,"children":2491},{},[2492],{"type":32,"value":2493},"Let me show you how to set it up.",{"type":27,"tag":45,"props":2495,"children":2497},{"id":2496},"getting-started",[2498],{"type":32,"value":2499},"Getting started",{"type":27,"tag":28,"props":2501,"children":2502},{},[2503],{"type":32,"value":2504},"First, you'll need a Mailosaur account. This is needed to create virtual email addresses and phone numbers for your tests. Once you have one, install the Mailosaur client:",{"type":27,"tag":793,"props":2506,"children":2509},{"className":2507,"code":2508,"language":1084,"meta":5},[1082],"npm install --save-dev mailosaur\n",[2510],{"type":27,"tag":653,"props":2511,"children":2512},{"__ignoreMap":5},[2513],{"type":32,"value":2508},{"type":27,"tag":28,"props":2515,"children":2516},{},[2517],{"type":32,"value":2518},"In your Mailosaur account, you'll need two things:",{"type":27,"tag":105,"props":2520,"children":2521},{},[2522,2532],{"type":27,"tag":109,"props":2523,"children":2524},{},[2525,2530],{"type":27,"tag":79,"props":2526,"children":2527},{},[2528],{"type":32,"value":2529},"API Key",{"type":32,"value":2531},": Found in your account settings",{"type":27,"tag":109,"props":2533,"children":2534},{},[2535,2540],{"type":27,"tag":79,"props":2536,"children":2537},{},[2538],{"type":32,"value":2539},"Server ID",{"type":32,"value":2541},": A unique identifier for your testing inbox",{"type":27,"tag":28,"props":2543,"children":2544},{},[2545,2547,2552],{"type":32,"value":2546},"It’s good to keep these private (especially the API key), so I recommend storing these in environment variables, either in ",{"type":27,"tag":653,"props":2548,"children":2550},{"className":2549},[],[2551],{"type":32,"value":943},{"type":32,"value":2553}," file or in your CI/CD pipeline.",{"type":27,"tag":793,"props":2555,"children":2558},{"className":2556,"code":2557,"language":1084,"meta":5},[1082],"# .env\nMAILOSAUR_API_KEY=your_api_key_here\nMAILOSAUR_SERVER_ID=your_server_id_here\n",[2559],{"type":27,"tag":653,"props":2560,"children":2561},{"__ignoreMap":5},[2562],{"type":32,"value":2557},{"type":27,"tag":28,"props":2564,"children":2565},{},[2566],{"type":32,"value":2567},"Now let's tackle each authentication method.",{"type":27,"tag":45,"props":2569,"children":2571},{"id":2570},"method-1-email-magic-links",[2572],{"type":32,"value":2573},"Method 1: Email magic links",{"type":27,"tag":28,"props":2575,"children":2576},{},[2577],{"type":32,"value":2578},"Magic links are becoming increasingly popular. They can be used instead of a password, but in essence they are the same thing you use when you reset your password. A link that authorizes certain usage (logging in or changing your password) is generated on server and sent to the account owner’s email. Here's how the flow looks like:",{"type":27,"tag":2580,"props":2581,"children":2582},"mermaid",{},[2583],{"type":27,"tag":28,"props":2584,"children":2585},{},[2586],{"type":32,"value":2587},"sequenceDiagram\nparticipant User\nparticipant Browser\nparticipant Server\nUser->>Browser: Enters email address\nBrowser->>Server: POST /api/auth/magic-link/send (email)\nServer->>Server: Generates a temporary, signed token\nServer-->>User: Sends email with login link (containing token)\nServer-->>Browser: Responds with \"Magic link sent\"\nNote over User, Browser: User opens email and clicks the link\nBrowser->>Server: GET /api/auth/magic-link/verify?token=...\nServer->>Server: Verifies token and creates session\nServer-->>Browser: Redirects to /success page with session cookie",{"type":27,"tag":28,"props":2589,"children":2590},{},[2591],{"type":32,"value":2592},"As you can see in the diagram above, the main challenge is that at a certain point the flow disconnects from the browser. When doing browser test automation, this is a problem. How do you enter your inbox?",{"type":27,"tag":28,"props":2594,"children":2595},{},[2596],{"type":32,"value":2597},"This is where Mailosaur comes in. It allows you to programmatically access the inbox that Mailosaur created for you. You’ll retrieve the vital information from the inbox (email containing the link) and then continue on with the test.",{"type":27,"tag":28,"props":2599,"children":2600},{},[2601],{"type":32,"value":2602},"Here's how a test like this would look like in Playwright:",{"type":27,"tag":793,"props":2604,"children":2609},{"className":2605,"code":2607,"language":2608,"meta":5},[2606],"language-typescript","import { test, expect } from '@playwright/test';\nimport { default as MailosaurClient } from 'mailosaur';\n\ntest('should send a magic link to the email address', async ({ page }) => {\n  const mailosaur = new MailosaurClient(process.env.MAILOSAUR_API_KEY as string);\n  const serverId = process.env.MAILOSAUR_SERVER_ID as string;\n  const testEmail = `testing-email@${serverId}.mailosaur.io`;\n\n  await page.goto('/auth/magic-link');\n  \n  await page.getByRole('textbox', { name: 'email' }).fill(testEmail);\n  await page.getByText('Send Magic Link').click();\n\n  const message = await mailosaur.messages.get(serverId, {\n    sentTo: testEmail\n  });\n\n  const link = message.html?.links?.[0]?.href;\n\n  await page.goto(link as string);\n  \n  await expect(page.getByText('Authentication Successful!')).toBeVisible();\n});\n","typescript",[2610],{"type":27,"tag":653,"props":2611,"children":2612},{"__ignoreMap":5},[2613],{"type":32,"value":2607},{"type":27,"tag":1029,"props":2615,"children":2616},{},[2617,2644],{"type":27,"tag":28,"props":2618,"children":2619},{},[2620,2622,2627,2629,2635,2637,2643],{"type":32,"value":2621},"Note: In order to use ",{"type":27,"tag":653,"props":2623,"children":2625},{"className":2624},[],[2626],{"type":32,"value":943},{"type":32,"value":2628}," variables in Playwright, you need to import the ",{"type":27,"tag":653,"props":2630,"children":2632},{"className":2631},[],[2633],{"type":32,"value":2634},"dotenv",{"type":32,"value":2636}," package to the ",{"type":27,"tag":653,"props":2638,"children":2640},{"className":2639},[],[2641],{"type":32,"value":2642},"playwright.config.ts",{"type":32,"value":786},{"type":27,"tag":793,"props":2645,"children":2648},{"className":2646,"code":2647,"filename":2642,"language":2608,"meta":5},[2606],"import 'dotenv/config';\ndotenv.config();\n/// rest of the config file...  \n",[2649],{"type":27,"tag":653,"props":2650,"children":2651},{"__ignoreMap":5},[2652],{"type":32,"value":2647},{"type":27,"tag":28,"props":2654,"children":2655},{},[2656,2658,2664,2666,2672],{"type":32,"value":2657},"The key part here is the ",{"type":27,"tag":653,"props":2659,"children":2661},{"className":2660},[],[2662],{"type":32,"value":2663},"mailosaur.messages.get()",{"type":32,"value":2665}," method. It automatically waits for the email to arrive, and parses the email content. You can access them via ",{"type":27,"tag":653,"props":2667,"children":2669},{"className":2668},[],[2670],{"type":32,"value":2671},"message.html.links",{"type":32,"value":2673},". Each link object contains:",{"type":27,"tag":793,"props":2675,"children":2680},{"className":2676,"code":2678,"language":2679,"meta":5},[2677],"language-javascript","{\n  href: 'https://example.com/verify?token=abc123',\n  text: 'Verify your account'\n}\n","javascript",[2681],{"type":27,"tag":653,"props":2682,"children":2683},{"__ignoreMap":5},[2684],{"type":32,"value":2678},{"type":27,"tag":28,"props":2686,"children":2687},{},[2688],{"type":32,"value":2689},"If your email contains multiple links, you can filter them:",{"type":27,"tag":793,"props":2691,"children":2694},{"className":2692,"code":2693,"language":2679,"meta":5},[2677],"const verifyLink = message.html.links.find(\n  link => link.href.includes('/verify')\n);\n",[2695],{"type":27,"tag":653,"props":2696,"children":2697},{"__ignoreMap":5},[2698],{"type":32,"value":2693},{"type":27,"tag":45,"props":2700,"children":2702},{"id":2701},"method-2-sms-verification-codes",[2703],{"type":32,"value":2704},"Method 2: SMS verification codes",{"type":27,"tag":28,"props":2706,"children":2707},{},[2708],{"type":32,"value":2709},"In essence, SMS-based authentication is the same as email magic links, but instead of an email, it sends a numeric code to your phone. You enter this code to prove you own that phone number. Here's the flow:",{"type":27,"tag":2580,"props":2711,"children":2712},{},[2713],{"type":27,"tag":28,"props":2714,"children":2715},{},[2716],{"type":32,"value":2717},"sequenceDiagram\nparticipant User\nparticipant Browser\nparticipant Server\nUser->>Browser: Enters phone number\nBrowser->>Server: POST /api/auth/sms/send (phone)\nServer->>Server: Generates & stores OTP code\nServer-->>User: Sends SMS with OTP code\nServer-->>Browser: Responds with \"SMS sent\"\nUser->>Browser: Enters the received OTP code\nBrowser->>Server: POST /api/auth/sms/verify (phone, code)\nServer->>Server: Creates session\nServer-->>Browser: Responds with success and sets session cookie\nBrowser->>Browser: Redirects to /success page",{"type":27,"tag":28,"props":2719,"children":2720},{},[2721],{"type":32,"value":2722},"With Mailosaur, you can create a virtual phone number that will receive your SMS messages to.",{"type":27,"tag":28,"props":2724,"children":2725},{},[2726],{"type":27,"tag":959,"props":2727,"children":2731},{"alt":2728,"className":2729,"src":2730},"Choosing a phone number with Mailosaur",[963,966,967],"mailosaur_sms.png",[],{"type":27,"tag":28,"props":2733,"children":2734},{},[2735],{"type":32,"value":2736},"Once you have one, you can start using it in your tests. Here's how to automate this flow:",{"type":27,"tag":793,"props":2738,"children":2741},{"className":2739,"code":2740,"language":2608,"meta":5},[2606],"import { test, expect } from '@playwright/test';\nimport { default as MailosaurClient } from 'mailosaur';\n\ntest('should send SMS code and verify authentication', async ({ page }) => {\n  const mailosaur = new MailosaurClient(process.env.MAILOSAUR_API_KEY as string);\n  const serverId = process.env.MAILOSAUR_SERVER_ID as string;\n  const testPhone = '+12345678910'; // your Mailosaur phone number\n\n  await page.goto('/auth/sms');\n  \n  await page.getByRole('textbox', { name: 'phone' }).fill(testPhone);\n  await page.getByText('Send SMS Code').click();\n\n  await expect(page.getByText('SMS sent! Enter the verification code.')).toBeVisible();\n\n  const message = await mailosaur.messages.get(serverId, {\n    sentTo: testPhone\n  });\n\n  // extract 6 digit code from SMS\n  const otpMatch = message.text?.body?.match(/\\b(\\d{6})\\b/);\n  expect(otpMatch).toBeTruthy();\n  const otpCode = otpMatch![1];\n\n  await page.getByRole('textbox', { name: 'otp' }).fill(otpCode);\n  await page.getByText('Verify Code').click();\n\n  await expect(page.getByText('Authentication Successful!')).toBeVisible();\n});\n\n",[2742],{"type":27,"tag":653,"props":2743,"children":2744},{"__ignoreMap":5},[2745],{"type":32,"value":2740},{"type":27,"tag":28,"props":2747,"children":2748},{},[2749,2751,2757,2759,2765],{"type":32,"value":2750},"Just like with email links, Mailosaur automatically extracts verification codes from SMS messages. You can access them via ",{"type":27,"tag":653,"props":2752,"children":2754},{"className":2753},[],[2755],{"type":32,"value":2756},"message.text.codes",{"type":32,"value":2758},". If your SMS contains multiple codes (though this is rare), they'll all be available in the ",{"type":27,"tag":653,"props":2760,"children":2762},{"className":2761},[],[2763],{"type":32,"value":2764},"codes",{"type":32,"value":2766}," array.",{"type":27,"tag":793,"props":2768,"children":2771},{"className":2769,"code":2770,"language":2679,"meta":5},[2677],"const code = message.text.codes[0].value; // e.g., \"564214\"\n",[2772],{"type":27,"tag":653,"props":2773,"children":2774},{"__ignoreMap":5},[2775],{"type":32,"value":2770},{"type":27,"tag":45,"props":2777,"children":2779},{"id":2778},"method-3-authenticator-apps-totp",[2780],{"type":32,"value":2781},"Method 3: Authenticator apps (TOTP)",{"type":27,"tag":28,"props":2783,"children":2784},{},[2785],{"type":32,"value":2786},"Authenticator apps like Google Authenticator or Authy generate time-based one-time passwords (TOTP). These codes change every 30 seconds and are generated using a shared secret. There are actually two steps in this flow.",{"type":27,"tag":28,"props":2788,"children":2789},{},[2790],{"type":32,"value":2791},"In the first step, you setup the authenticator app with the shared secret.",{"type":27,"tag":2580,"props":2793,"children":2794},{},[2795],{"type":27,"tag":28,"props":2796,"children":2797},{},[2798],{"type":32,"value":2799},"sequenceDiagram\nparticipant User\nparticipant AuthenticatorApp as Authenticator App\nparticipant Browser\nparticipant Server\nUser->>Browser: Enters identifier (email/phone)\nBrowser->>Server: POST /api/auth/totp/setup (identifier)\nServer->>Server: Generates & stores a unique secret\nServer-->>Browser: Responds with QR code (containing secret)\nBrowser-->>User: Displays QR Code for scanning\nUser->>AuthenticatorApp: Scans QR Code\nAuthenticatorApp->>User: Now generates 6-digit codes",{"type":27,"tag":28,"props":2801,"children":2802},{},[2803],{"type":32,"value":2804},"In the second step, you use the authenticator app to generate a code and enter it into the login form. This code will then be validated on the server.",{"type":27,"tag":2580,"props":2806,"children":2807},{},[2808],{"type":27,"tag":28,"props":2809,"children":2810},{},[2811],{"type":32,"value":2812},"sequenceDiagram\nparticipant User\nparticipant AuthenticatorApp as Authenticator App\nparticipant Browser\nparticipant Server\nUser->>Browser: Opens login page\nUser->>AuthenticatorApp: Gets current 6-digit code\nUser->>Browser: Enters 6-digit code\nBrowser->>Server: POST /api/auth/totp/verify (identifier, code)\nServer->>Server: Validates code against stored secret\nServer->>Server: Creates session\nServer-->>Browser: Responds with success and sets session cookie\nBrowser->>Browser: Redirects to /success page",{"type":27,"tag":28,"props":2814,"children":2815},{},[2816],{"type":32,"value":2817},"The secret is usually presented as a QR code or a string of characters during setup. When integrating with Mailosaur, you can manually set up the first step inside Mailosaur’s service:",{"type":27,"tag":28,"props":2819,"children":2820},{},[2821],{"type":27,"tag":959,"props":2822,"children":2827},{"alt":2823,"className":2824,"src":2826},"Authenticator setup with Mailosaur",[2825,966,967],"shadow-block-lime","authenticator_setup.png",[],{"type":27,"tag":28,"props":2829,"children":2830},{},[2831],{"type":32,"value":2832},"This allows you to interact with the authenticator during a manual test. But you can also set up things automatically in your tests. Here's how the test for a TOTP authentication flow would look like:",{"type":27,"tag":793,"props":2834,"children":2837},{"className":2835,"code":2836,"language":2608,"meta":5},[2606],"import { test, expect } from '@playwright/test';\nimport { default as MailosaurClient } from 'mailosaur';\n\ntest('login with authenticator app', async ({ page }) => {\n  const mailosaur = new MailosaurClient(process.env.MAILOSAUR_API_KEY as string);\n  const serverId = process.env.MAILOSAUR_SERVER_ID;\n  const testEmail = `testing-totp@${serverId}.mailosaur.net`;\n\n  await page.goto('/auth/totp');\n\n  await page.getByRole('textbox', { name: 'Email or Phone' }).fill(testEmail);\n  await page.getByText('Setup TOTP').click();\n\n  const secretElement = page.locator('code');\n  const sharedSecret = await secretElement.textContent() as string;\n  \n  const otp = await mailosaur.devices.otp(sharedSecret);\n\n  await page.locator('#totpCode').fill(otp.code as string);\n  await page.getByRole('button', { name: 'Verify & Complete Setup' }).click();\n\n  await expect(page.getByText('Authentication Successful!')).toBeVisible();\n});\n",[2838],{"type":27,"tag":653,"props":2839,"children":2840},{"__ignoreMap":5},[2841],{"type":32,"value":2836},{"type":27,"tag":28,"props":2843,"children":2844},{},[2845],{"type":32,"value":2846},"As you can notice, we are extracting the shared secret from an element on the page. You‘ll usually find this option on TOTP setup pages. But if you don't, you can always use QR code decoding to get the key.",{"type":27,"tag":28,"props":2848,"children":2849},{},[2850],{"type":27,"tag":959,"props":2851,"children":2855},{"alt":2852,"className":2853,"src":2854},"TOTP setup page",[2825,966,967],"TOTP_setup.png",[],{"type":27,"tag":45,"props":2857,"children":2859},{"id":2858},"common-gotchas-and-tips",[2860],{"type":32,"value":2861},"Common gotchas and tips",{"type":27,"tag":1033,"props":2863,"children":2865},{"id":2864},"timing-issues-with-totp-codes",[2866],{"type":32,"value":2867},"Timing issues with TOTP codes",{"type":27,"tag":28,"props":2869,"children":2870},{},[2871],{"type":32,"value":2872},"TOTP codes expire every 30 seconds. If your test is slow or runs near a boundary, the code might expire between generation and use. To handle this:",{"type":27,"tag":1033,"props":2874,"children":2876},{"id":2875},"email-and-sms-delivery-delays",[2877],{"type":32,"value":2878},"Email and SMS delivery delays",{"type":27,"tag":28,"props":2880,"children":2881},{},[2882,2884,2890],{"type":32,"value":2883},"While Mailosaur's ",{"type":27,"tag":653,"props":2885,"children":2887},{"className":2886},[],[2888],{"type":32,"value":2889},"messages.get()",{"type":32,"value":2891}," method waits automatically, you can customize the timeout:",{"type":27,"tag":793,"props":2893,"children":2896},{"className":2894,"code":2895,"language":2679,"meta":5},[2677],"const message = await mailosaur.messages.get(\n  serverId,\n  { sentTo: testEmail },\n  { timeout: 20000 } // 20 seconds instead of default 10 seconds\n);\n",[2897],{"type":27,"tag":653,"props":2898,"children":2899},{"__ignoreMap":5},[2900],{"type":32,"value":2895},{"type":27,"tag":1033,"props":2902,"children":2904},{"id":2903},"cleaning-up-test-data",[2905],{"type":32,"value":2906},"Cleaning up test data",{"type":27,"tag":28,"props":2908,"children":2909},{},[2910],{"type":32,"value":2911},"If your inbox gets clogged with test data, you can clean it up:",{"type":27,"tag":793,"props":2913,"children":2916},{"className":2914,"code":2915,"language":2679,"meta":5},[2677],"test.afterEach(async () => {\n  const mailosaur = new MailosaurClient(process.env.MAILOSAUR_API_KEY);\n  await mailosaur.messages.deleteAll(process.env.MAILOSAUR_SERVER_ID);\n});\n",[2917],{"type":27,"tag":653,"props":2918,"children":2919},{"__ignoreMap":5},[2920],{"type":32,"value":2915},{"type":27,"tag":1033,"props":2922,"children":2924},{"id":2923},"using-search-criteria-to-find-specific-emails",[2925],{"type":32,"value":2926},"Using search criteria to find specific emails",{"type":27,"tag":28,"props":2928,"children":2929},{},[2930],{"type":32,"value":2931},"Instead of relying on unique email addresses, you can search for emails based on their content or subject. This is especially useful when multiple tests might send emails to the same address:",{"type":27,"tag":793,"props":2933,"children":2936},{"className":2934,"code":2935,"language":2679,"meta":5},[2677],"const message = await mailosaur.messages.get(serverId, {\n  sentTo: testEmail,\n  subject: 'Login',\n  body: 'verification code'\n});\n",[2937],{"type":27,"tag":653,"props":2938,"children":2939},{"__ignoreMap":5},[2940],{"type":32,"value":2935},{"type":27,"tag":28,"props":2942,"children":2943},{},[2944],{"type":32,"value":2945},"For SMS, you typically have a limited number of virtual phone numbers, so use search criteria to find the right message:",{"type":27,"tag":793,"props":2947,"children":2950},{"className":2948,"code":2949,"language":2679,"meta":5},[2677],"const message = await mailosaur.messages.get(serverId, {\n  sentTo: phoneNumber,\n  body: 'Your verification code' // Search by message content\n});\n",[2951],{"type":27,"tag":653,"props":2952,"children":2953},{"__ignoreMap":5},[2954],{"type":32,"value":2949},{"type":27,"tag":28,"props":2956,"children":2957},{},[2958,2960,2965,2966,2971],{"type":32,"value":2959},"Hope this helps! If you found this useful, feel free to share it with others who might be struggling with authentication testing. You can also find me on ",{"type":27,"tag":172,"props":2961,"children":2963},{"href":1893,"rel":2962},[696],[2964],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":2967,"children":2969},{"href":1900,"rel":2968},[696],[2970],{"type":32,"value":1598},{"type":32,"value":2972}," where I share more testing tips and tricks.",{"title":5,"searchDepth":320,"depth":320,"links":2974},[2975,2976,2977,2978,2979],{"id":2496,"depth":320,"text":2499},{"id":2570,"depth":320,"text":2573},{"id":2701,"depth":320,"text":2704},{"id":2778,"depth":320,"text":2781},{"id":2858,"depth":320,"text":2861,"children":2980},[2981,2982,2983,2984],{"id":2864,"depth":1606,"text":2867},{"id":2875,"depth":1606,"text":2878},{"id":2903,"depth":1606,"text":2906},{"id":2923,"depth":1606,"text":2926},"content:2fa-testing-with-playwright-and-mailosaur:index.md","2fa-testing-with-playwright-and-mailosaur/index.md","2fa-testing-with-playwright-and-mailosaur/index",{"_path":2989,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":2990,"description":2991,"date":2992,"published":10,"slug":174,"tags":2993,"image":2995,"cypressVersion":17,"playwrightVersion":17,"vitestVersion":17,"readingTime":2996,"body":3000,"_type":329,"_id":3214,"_source":331,"_file":3215,"_stem":3216,"_extension":334},"/testing-will-become-more-important-not-less","Testing Will Become More Important, Not Less","LLMs speed up coding but quality concerns remain. Testing becomes more crucial, evolving to include AI-generated tests while human expertise in test design stays valuable.","2025-04-16",[13,2994,579],"software development","testing_rvni3y.png",{"text":585,"minutes":2997,"time":2998,"words":2999},3.565,213900,713,{"type":24,"children":3001,"toc":3207},[3002,3019,3024,3029,3034,3039,3044,3049,3054,3059,3065,3070,3075,3080,3086,3091,3112,3118,3123,3128,3134,3139,3144,3149,3154,3160,3165,3179,3184,3189,3194],{"type":27,"tag":28,"props":3003,"children":3004},{},[3005,3007,3011,3013,3018],{"type":32,"value":3006},"LLMs have sped up code generation to insane levels. But I'm not sure if it's fair to say that it has improved the speed of delivery. There are legitimate questions about the quality of the outcome. Not only quality of the ",{"type":27,"tag":302,"props":3008,"children":3009},{},[3010],{"type":32,"value":653},{"type":32,"value":3012},", but the ",{"type":27,"tag":302,"props":3014,"children":3015},{},[3016],{"type":32,"value":3017},"actual product",{"type":32,"value":256},{"type":27,"tag":28,"props":3020,"children":3021},{},[3022],{"type":32,"value":3023},"It seems we still validate the outcome mostly manually - either by reviewing the generated code, or by actually testing the application.",{"type":27,"tag":28,"props":3025,"children":3026},{},[3027],{"type":32,"value":3028},"And although the generated code is not a black box (yet), implementation details tend to get fuzzy as the number of lines of code grows.",{"type":27,"tag":28,"props":3030,"children":3031},{},[3032],{"type":32,"value":3033},"And so - testing the application manually seems like the best way to validate the outcome.",{"type":27,"tag":28,"props":3035,"children":3036},{},[3037],{"type":32,"value":3038},"But although this makes testing MORE important, it doesn't necessarily mean we will be getting more testing job postings (as some would like to believe).",{"type":27,"tag":28,"props":3040,"children":3041},{},[3042],{"type":32,"value":3043},"In fact, I think this is going to create even more pressure for testing to be done faster. As much as I hate it when testing is called \"the bottleneck\", we may be entering an era where that's actually true.",{"type":27,"tag":28,"props":3045,"children":3046},{},[3047],{"type":32,"value":3048},"We can all agree that manual QA has undeniable benefits. But it's hard to make the argument that all testing should be done this way. In most cases, it's slower and more expensive than the alternatives.",{"type":27,"tag":28,"props":3050,"children":3051},{},[3052],{"type":32,"value":3053},"So how are we going to realistically keep the software at high quality?",{"type":27,"tag":28,"props":3055,"children":3056},{},[3057],{"type":32,"value":3058},"Here are five of my predictions and possible outcomes.",{"type":27,"tag":45,"props":3060,"children":3062},{"id":3061},"_1-testing-will-become-more-embedded-into-software-creation",[3063],{"type":32,"value":3064},"#1 Testing will become more embedded into software creation",{"type":27,"tag":28,"props":3066,"children":3067},{},[3068],{"type":32,"value":3069},"In other words, when application code is generated, tests will be generated along with it. This will mean that greenlighting the produced code will actually mean greenlighting passing tests as well.",{"type":27,"tag":28,"props":3071,"children":3072},{},[3073],{"type":32,"value":3074},"Since modern AI tools are good at generating code, there's nothing really stopping them from generating tests as well. Of course, it's still good to manually review them (see point #2), but having them generated alongside the code already translates to a lot of saved time.",{"type":27,"tag":28,"props":3076,"children":3077},{},[3078],{"type":32,"value":3079},"And as we iterate over new versions, we avoid regressions by running those tests generated in previous iterations.",{"type":27,"tag":45,"props":3081,"children":3083},{"id":3082},"_2-more-agents-and-ai-solutions-will-add-test-automation",[3084],{"type":32,"value":3085},"#2 More agents and A.I. solutions will add test automation",{"type":27,"tag":28,"props":3087,"children":3088},{},[3089],{"type":32,"value":3090},"We are going to see more agents and A.I. solutions that will add test automation alongside produced code. Those that will generate tests along with produced code will see less regressions and will scale much better.",{"type":27,"tag":28,"props":3092,"children":3093},{},[3094,3096,3102,3104,3111],{"type":32,"value":3095},"There are already solutions on the market that do their own testing of produced code and avoid regressions or bad outputs by running tests in the background and feeding the results back to LLM such as ",{"type":27,"tag":172,"props":3097,"children":3099},{"href":2342,"rel":3098},[696],[3100],{"type":32,"value":3101},"Nut.new",{"type":32,"value":3103}," from ",{"type":27,"tag":172,"props":3105,"children":3108},{"href":3106,"rel":3107},"https://replay.io",[696],[3109],{"type":32,"value":3110},"Replay.io",{"type":32,"value":256},{"type":27,"tag":45,"props":3113,"children":3115},{"id":3114},"_3-manual-testing-will-evolve-not-disappear",[3116],{"type":32,"value":3117},"#3 Manual testing will evolve not disappear",{"type":27,"tag":28,"props":3119,"children":3120},{},[3121],{"type":32,"value":3122},"Manual testing will not disappear, but it will be more granular, focused on manual testing of the new functionality and code reviews.",{"type":27,"tag":28,"props":3124,"children":3125},{},[3126],{"type":32,"value":3127},"Instead of huge test suites that will be manually reviewed one by one, testers will mostly test changes, while regression testing will be a domain of test automation.",{"type":27,"tag":45,"props":3129,"children":3131},{"id":3130},"_4-good-test-design-will-continue-to-be-highly-valued",[3132],{"type":32,"value":3133},"#4 Good test design will continue to be highly valued",{"type":27,"tag":28,"props":3135,"children":3136},{},[3137],{"type":32,"value":3138},"Great test engineers understand that putting together some Playwright code and getting test coverage is simply not enough.",{"type":27,"tag":28,"props":3140,"children":3141},{},[3142],{"type":32,"value":3143},"Keeping good testing practices, being able to properly architect test data and properly determine risk areas is more crucial than ever.",{"type":27,"tag":28,"props":3145,"children":3146},{},[3147],{"type":32,"value":3148},"For example - when test code is generated using LLM, that generation is guided by a highly skilled tester and reviewed. This will help ensuring the quality of the tests.",{"type":27,"tag":28,"props":3150,"children":3151},{},[3152],{"type":32,"value":3153},"It is exactly the same principle as when a skilled developer drives and reviews the generated application code.",{"type":27,"tag":45,"props":3155,"children":3157},{"id":3156},"_5-application-and-test-runtime-will-become-a-huge-challenge",[3158],{"type":32,"value":3159},"#5 Application and Test Runtime Will Become a Huge Challenge",{"type":27,"tag":28,"props":3161,"children":3162},{},[3163],{"type":32,"value":3164},"While trace-viewing such as we see in Playwright is becoming a standard, we usually don't have this kind of information in the application runtime.",{"type":27,"tag":28,"props":3166,"children":3167},{},[3168,3170,3177],{"type":32,"value":3169},"As ",{"type":27,"tag":172,"props":3171,"children":3174},{"href":3172,"rel":3173},"https://techcrunch.com/2025/04/10/ai-models-still-struggle-to-debug-software-microsoft-study-shows/",[696],[3175],{"type":32,"value":3176},"this article from Techcrunch",{"type":32,"value":3178}," shows, AI models still struggle to debug software and not even the better models out there are good at it.",{"type":27,"tag":28,"props":3180,"children":3181},{},[3182],{"type":32,"value":3183},"The reason for that is, that large language models are trained on a large amounts of code, but they lack information on how does the written code actually run.",{"type":27,"tag":28,"props":3185,"children":3186},{},[3187],{"type":32,"value":3188},"My prediction is that observability tools will be the missing piece in making A.I. produce really reliable code.",{"type":27,"tag":28,"props":3190,"children":3191},{},[3192],{"type":32,"value":3193},"This might mean that manual testing and need for excellence in both development and testing is not leaving anytime soon. In fact, it might be needed more than ever.",{"type":27,"tag":28,"props":3195,"children":3196},{},[3197,3199,3206],{"type":32,"value":3198},"But let me know what you think. Will testing be in higher demand, now that development is speeding up? Or will we see the exact opposite? Feel free to discuss on ",{"type":27,"tag":172,"props":3200,"children":3203},{"href":3201,"rel":3202},"https://links.filiphric.com",[696],[3204],{"type":32,"value":3205},"social media",{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":3208},[3209,3210,3211,3212,3213],{"id":3061,"depth":320,"text":3064},{"id":3082,"depth":320,"text":3085},{"id":3114,"depth":320,"text":3117},{"id":3130,"depth":320,"text":3133},{"id":3156,"depth":320,"text":3159},"content:testing-will-become-more-important-not-less:index.md","testing-will-become-more-important-not-less/index.md","testing-will-become-more-important-not-less/index",{"_path":3218,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":3219,"description":3220,"date":3221,"published":10,"slug":3222,"tags":3223,"image":3226,"webdriverioVersion":3227,"readingTime":3228,"body":3233,"_type":329,"_id":3444,"_source":331,"_file":3445,"_stem":3446,"_extension":334},"/understanding-timeouts-in-webdriverio","Understanding timeouts in WebdriverIO","Learn how to effectively use timeouts in WebdriverIO to create reliable end-to-end tests","2025-02-19","understanding-timeouts-in-webdriverio",[3224,13,3225],"webdriverIO","timeout","webdriverio_dmuzr8.png","9.2.12",{"text":3229,"minutes":3230,"time":3231,"words":3232},"3 min read",2.945,176700,589,{"type":24,"children":3234,"toc":3438},[3235,3240,3245,3251,3256,3265,3270,3279,3285,3290,3298,3303,3308,3317,3330,3335,3345,3388,3393,3402,3408,3413,3418,3427,3433],{"type":27,"tag":28,"props":3236,"children":3237},{},[3238],{"type":32,"value":3239},"Timeouts are one of the vital parts of UI end-to-end testing. When testing user interfaces, we often need to deal with various forms of randomness (or apparent randomness) in how elements appear and interact.",{"type":27,"tag":28,"props":3241,"children":3242},{},[3243],{"type":32,"value":3244},"WebdriverIO handles this by having commands that run in a loop, trying to locate elements or make assertions until they either succeed or eventually fail. You can think of timeouts as upper limits - if the desired action happens within the timeout period, the script continues.",{"type":27,"tag":45,"props":3246,"children":3248},{"id":3247},"timeouts-vs-hard-waits",[3249],{"type":32,"value":3250},"Timeouts vs. Hard Waits",{"type":27,"tag":28,"props":3252,"children":3253},{},[3254],{"type":32,"value":3255},"So how are timeouts different from hard waits? Hard waits or pauses stop the test execution altogether. When using a hard wait, we essentially disconnect from the application under test, hoping that during this pause the application reaches the desired state. This approach is flaky by design because it's detached from what the application is actually doing.",{"type":27,"tag":793,"props":3257,"children":3260},{"className":3258,"code":3259,"language":1513,"meta":5},[1510],"// Don't use hard waits\nconst button = $('aria/Submit')\n // ❌ Stops execution for 2 seconds regardless of state\nbrowser.pause(2000)\nawait expect(button).toBeDisplayed()\n",[3261],{"type":27,"tag":653,"props":3262,"children":3263},{"__ignoreMap":5},[3264],{"type":32,"value":3259},{"type":27,"tag":28,"props":3266,"children":3267},{},[3268],{"type":32,"value":3269},"Timeouts, on the other hand, are a great way to stay connected to the application under test. Because they enable us to constantly check the state of the application, they are typically faster - we move on to the next command as soon as the condition is met.",{"type":27,"tag":793,"props":3271,"children":3274},{"className":3272,"code":3273,"language":1513,"meta":5},[1510],"// Much better approach\nconst button = $('aria/Submit')\n// ✅ Continues as soon as element is found\nawait expect(button).toBeDisplayed() \n",[3275],{"type":27,"tag":653,"props":3276,"children":3277},{"__ignoreMap":5},[3278],{"type":32,"value":3273},{"type":27,"tag":45,"props":3280,"children":3282},{"id":3281},"types-of-timeouts-in-webdriverio",[3283],{"type":32,"value":3284},"Types of Timeouts in WebdriverIO",{"type":27,"tag":28,"props":3286,"children":3287},{},[3288],{"type":32,"value":3289},"Let's look at timeouts in a real-life scenario. I have a small game application with three closed doors that open to reveal different characters - some are enemies and some we should protect.",{"type":27,"tag":28,"props":3291,"children":3292},{},[3293],{"type":27,"tag":959,"props":3294,"children":3297},{"alt":3295,"src":3296},"Game application","/ghosts_goqsla.png",[],{"type":27,"tag":28,"props":3299,"children":3300},{},[3301],{"type":32,"value":3302},"Whenever we run this game, the time it takes for a character to appear is random, which poses a challenge for our test.",{"type":27,"tag":28,"props":3304,"children":3305},{},[3306],{"type":32,"value":3307},"Here's a basic test that handles this randomness:",{"type":27,"tag":793,"props":3309,"children":3312},{"className":3310,"code":3311,"language":1513,"meta":5},[1510],"import { browser, $ } from '@wdio/globals'\nit('open all three doors (waitForDisplayed)', async () => {\n  await browser.url('/')\n\n  const startGameButton = $('[data-test=\"start-game-button\"]')\n  \n  // Start the game\n  await startGameButton.waitForDisplayed()\n  await startGameButton.click()\n  \n  const door1 = $('aria/Door 1')\n  const door2 = $('aria/Door 2')\n  const door3 = $('aria/Door 3')\n  \n  await door1.waitForDisplayed()\n  await door2.waitForDisplayed()\n  await door3.waitForDisplayed()\n})\n",[3313],{"type":27,"tag":653,"props":3314,"children":3315},{"__ignoreMap":5},[3316],{"type":32,"value":3311},{"type":27,"tag":28,"props":3318,"children":3319},{},[3320,3322,3328],{"type":32,"value":3321},"This test passes even with random timing because the ",{"type":27,"tag":653,"props":3323,"children":3325},{"className":3324},[],[3326],{"type":32,"value":3327},"waitForDisplayed",{"type":32,"value":3329}," command keeps retrying until it finds the element. But how long does it wait?",{"type":27,"tag":28,"props":3331,"children":3332},{},[3333],{"type":32,"value":3334},"The answer is in the WebdriverIO config:",{"type":27,"tag":793,"props":3336,"children":3340},{"className":3337,"code":3338,"filename":3339,"language":1513,"meta":5},[1510],"export const config: Options.Testrunner = {\n  // ... existing code ...\n  waitforTimeout: 10000, // Default timeout for all waitFor commands\n  // ... existing code ...\n  mochaOpts: {\n    ui: 'bdd',\n    timeout: 30000 // Overall test timeout\n  },\n}\n","wdio.conf.ts",[3341],{"type":27,"tag":653,"props":3342,"children":3343},{"__ignoreMap":5},[3344],{"type":32,"value":3338},{"type":27,"tag":28,"props":3346,"children":3347},{},[3348,3350,3356,3358,3364,3366,3371,3373,3379,3380,3386],{"type":32,"value":3349},"The ",{"type":27,"tag":653,"props":3351,"children":3353},{"className":3352},[],[3354],{"type":32,"value":3355},"waitforTimeout",{"type":32,"value":3357}," setting dictates how long we want to wait for all ",{"type":27,"tag":653,"props":3359,"children":3361},{"className":3360},[],[3362],{"type":32,"value":3363},"waitFor",{"type":32,"value":3365}," commands (like ",{"type":27,"tag":653,"props":3367,"children":3369},{"className":3368},[],[3370],{"type":32,"value":3327},{"type":32,"value":3372},", ",{"type":27,"tag":653,"props":3374,"children":3376},{"className":3375},[],[3377],{"type":32,"value":3378},"waitForClickable",{"type":32,"value":3372},{"type":27,"tag":653,"props":3381,"children":3383},{"className":3382},[],[3384],{"type":32,"value":3385},"waitForEnabled",{"type":32,"value":3387},", etc.). If we were to change this timeout to 1 second, our test would likely fail since elements often take longer to appear.",{"type":27,"tag":28,"props":3389,"children":3390},{},[3391],{"type":32,"value":3392},"We can also set timeouts for individual commands:",{"type":27,"tag":793,"props":3394,"children":3397},{"className":3395,"code":3396,"language":1513,"meta":5},[1510],"await door3.waitForDisplayed({ timeout: 1000 }) // Override timeout just for this command\n",[3398],{"type":27,"tag":653,"props":3399,"children":3400},{"__ignoreMap":5},[3401],{"type":32,"value":3396},{"type":27,"tag":45,"props":3403,"children":3405},{"id":3404},"test-suite-timeouts",[3406],{"type":32,"value":3407},"Test Suite Timeouts",{"type":27,"tag":28,"props":3409,"children":3410},{},[3411],{"type":32,"value":3412},"Another important timeout determines the length of the whole test. In our example, the test takes about 12 seconds to finish. For longer tests, we might want to set an upper limit using Mocha's timeout option.",{"type":27,"tag":28,"props":3414,"children":3415},{},[3416],{"type":32,"value":3417},"The default Mocha timeout is 30 seconds, but we can adjust this in the config:",{"type":27,"tag":793,"props":3419,"children":3422},{"className":3420,"code":3421,"language":1513,"meta":5},[1510],"mochaOpts: {\n  ui: 'bdd',\n  timeout: 30000 // Adjust this value to set test timeout\n}\n",[3423],{"type":27,"tag":653,"props":3424,"children":3425},{"__ignoreMap":5},[3426],{"type":32,"value":3421},{"type":27,"tag":45,"props":3428,"children":3430},{"id":3429},"best-practices",[3431],{"type":32,"value":3432},"Best Practices",{"type":27,"tag":28,"props":3434,"children":3435},{},[3436],{"type":32,"value":3437},"Timeouts are a great way to set upper limits for actions in our tests, but you need to be careful not to set them too high. Remember that the timeout is also the time it takes for your test to fail. If you have multiple tests in your suite that are going to fail, high timeouts will significantly increase the overall execution time.",{"title":5,"searchDepth":320,"depth":320,"links":3439},[3440,3441,3442,3443],{"id":3247,"depth":320,"text":3250},{"id":3281,"depth":320,"text":3284},{"id":3404,"depth":320,"text":3407},{"id":3429,"depth":320,"text":3432},"content:understanding-timeouts-in-webdriverio:index.md","understanding-timeouts-in-webdriverio/index.md","understanding-timeouts-in-webdriverio/index",{"_path":3448,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":3449,"description":3450,"date":3451,"published":10,"slug":3452,"tags":3453,"image":3458,"vitestVersion":3459,"readingTime":3460,"body":3464,"_type":329,"_id":4019,"_source":331,"_file":4020,"_stem":4021,"_extension":334},"/introduction-to-testing-in-vitest","Introduction to testing in Vitest","A comprehensive guide to Vitest testing framework, covering its core features, capabilities and practical examples for unit and component testing","2024-12-19","introduction-to-testing-in-vitest",[3454,3455,3456,3457],"unit testing","vitest","component","code coverage","vitest_ykdhso.png","2.1.5",{"text":19,"minutes":3461,"time":3462,"words":3463},8.26,495600,1652,{"type":24,"children":3465,"toc":4010},[3466,3471,3476,3482,3487,3501,3509,3514,3526,3531,3541,3546,3552,3557,3566,3571,3579,3584,3595,3604,3610,3623,3635,3640,3649,3662,3674,3687,3697,3718,3729,3735,3740,3752,3757,3770,3798,3803,3818,3823,3829,3834,3847,3859,3872,3885,3890,3898,3910,3919,3925,3938,3943,3956,3965,3986,3994,4000,4005],{"type":27,"tag":28,"props":3467,"children":3468},{},[3469],{"type":32,"value":3470},"Lately I’ve been playing with Vitest - a testing framework for Javascript and TypeScript apps. I’ve been playing with the tool recently and I have been positively surprised with it’s capabilities.",{"type":27,"tag":28,"props":3472,"children":3473},{},[3474],{"type":32,"value":3475},"Typically, you’d use Vitest for unit and component test testing, but there’s much more the tool offers. In this blogpost we’ll take a look at the different features and use cases.",{"type":27,"tag":45,"props":3477,"children":3479},{"id":3478},"understanding-the-basics",[3480],{"type":32,"value":3481},"Understanding the basics",{"type":27,"tag":28,"props":3483,"children":3484},{},[3485],{"type":32,"value":3486},"If you have a background mostly in e2e testing as I do, you’ll quickly notice the main difference in the testing approach with a tool like Vitest. In end-to-end testing, you start by opening the app in the browser and following a particular user story. With Vitest, your test focuses on a specific part of your app. This part can be a component, a function or something else. The approach is usually to import that chunk of your application into a test, and verify different scenarios in isolation.",{"type":27,"tag":28,"props":3488,"children":3489},{},[3490,3492,3499],{"type":32,"value":3491},"As an example I will be using a ",{"type":27,"tag":172,"props":3493,"children":3496},{"href":3494,"rel":3495},"https://github.com/filiphric/status-page-example",[696],[3497],{"type":32,"value":3498},"simple status page application",{"type":32,"value":3500}," written in Next.js. The application displays current status of a system and a list of historical statuses.",{"type":27,"tag":28,"props":3502,"children":3503},{},[3504],{"type":27,"tag":959,"props":3505,"children":3508},{"alt":3506,"src":3507},"Status page application","status_page_example_suwgr2.png",[],{"type":27,"tag":28,"props":3510,"children":3511},{},[3512],{"type":32,"value":3513},"Inside this application I have a function that returns a color based on the status of a service.",{"type":27,"tag":793,"props":3515,"children":3521},{"className":3516,"code":3518,"filename":3519,"language":3520,"meta":5},[3517],"language-ts","export function getStatusColor(status: Status): string {\n  switch (status) {\n    case \"operational\":\n      return \"bg-green-500\";\n    case \"partial-outage\":\n      return \"bg-yellow-500\";\n    case \"major-outage\":\n      return \"bg-red-500\";\n    default:\n      return \"bg-gray-500\";\n  }\n}\n","statusData.ts","ts",[3522],{"type":27,"tag":653,"props":3523,"children":3524},{"__ignoreMap":5},[3525],{"type":32,"value":3518},{"type":27,"tag":28,"props":3527,"children":3528},{},[3529],{"type":32,"value":3530},"My testing goal would be to make sure that the function returns the correct color for each status. The test in Vitest would look like this:",{"type":27,"tag":793,"props":3532,"children":3536},{"className":3533,"code":3534,"filename":3535,"language":3520,"meta":5},[3517],"import { test, expect } from \"vitest\";\nimport { getStatusColor } from \"./statusData\";\n\ntest(\"getStatusColor returns the correct color for each status\", () => {\n  expect(getStatusColor(\"operational\")).toBe(\"bg-green-500\");\n  expect(getStatusColor(\"partial-outage\")).toBe(\"bg-yellow-500\");\n  expect(getStatusColor(\"major-outage\")).toBe(\"bg-red-500\");\n  expect(getStatusColor(\"unknown\")).toBe(\"bg-gray-500\");\n});\n","statusData.spec.ts",[3537],{"type":27,"tag":653,"props":3538,"children":3539},{"__ignoreMap":5},[3540],{"type":32,"value":3534},{"type":27,"tag":28,"props":3542,"children":3543},{},[3544],{"type":32,"value":3545},"The test simply verifies every possible case for this function and verifies that the return value is what we expect. For a simple function like this, the test feels simplistic, but these kinds of tests start to bring more value once we need to deal with multiple functions, imports, or once we refactor existing functionality.",{"type":27,"tag":45,"props":3547,"children":3549},{"id":3548},"running-tests",[3550],{"type":32,"value":3551},"Running tests",{"type":27,"tag":28,"props":3553,"children":3554},{},[3555],{"type":32,"value":3556},"There are couple of way to run the tests. The simplest one is to use command line:",{"type":27,"tag":793,"props":3558,"children":3561},{"className":3559,"code":3560,"language":1084,"meta":5},[1082],"npx vitest\n",[3562],{"type":27,"tag":653,"props":3563,"children":3564},{"__ignoreMap":5},[3565],{"type":32,"value":3560},{"type":27,"tag":28,"props":3567,"children":3568},{},[3569],{"type":32,"value":3570},"If you are using VS Code or Cursor, there’s an extention tha allows you to run your test right inside your code editor. There’s support for other editors as well.",{"type":27,"tag":28,"props":3572,"children":3573},{},[3574],{"type":27,"tag":959,"props":3575,"children":3578},{"alt":3576,"src":3577},"VS Code extension for Vitest","vs_code_vitest_zdeg8j.png",[],{"type":27,"tag":28,"props":3580,"children":3581},{},[3582],{"type":32,"value":3583},"Vitest also comes with a very handy UI mode, that contains a dashboard for all your tests. You can run it by typing the following into the command line:",{"type":27,"tag":793,"props":3585,"children":3590},{"className":3586,"code":3588,"language":3589,"meta":5},[3587],"language-shell","npx vitest --ui\n","shell",[3591],{"type":27,"tag":653,"props":3592,"children":3593},{"__ignoreMap":5},[3594],{"type":32,"value":3588},{"type":27,"tag":28,"props":3596,"children":3597},{},[3598,3602],{"type":27,"tag":959,"props":3599,"children":3601},{"alt":3576,"src":3600},"vitest_ui_uteito.png",[],{"type":32,"value":3603},"\nUI mode automatically runs in watch mode, so that anytime you change the test or the source file, test will re-run and give you immediate feedback.",{"type":27,"tag":45,"props":3605,"children":3607},{"id":3606},"testing-components",[3608],{"type":32,"value":3609},"Testing components",{"type":27,"tag":28,"props":3611,"children":3612},{},[3613,3615,3621],{"type":32,"value":3614},"In the same way as you test your functions, you can test your components. Let’s take a look at a component that implements these colors and contains a bit more logic. Here’s an example of a component that renders individual list item. The important thing to point out here is that this components takes in a propert called ",{"type":27,"tag":653,"props":3616,"children":3618},{"className":3617},[],[3619],{"type":32,"value":3620},"item",{"type":32,"value":3622},". Later we will want to make sure that it is properly handled.",{"type":27,"tag":793,"props":3624,"children":3630},{"className":3625,"code":3627,"filename":3628,"language":3629,"meta":5},[3626],"language-tsx","import type { HistoricalStatus } from \"../utils/statusData\";\nimport { getStatusColor } from \"../utils/statusData\";\n\ninterface HistoricalStatusItemProps {\n  item: HistoricalStatus;\n}\n\nexport function HistoricalStatusItem({ item }: HistoricalStatusItemProps) {\n  return (\n    \u003Cdiv className=\"flex items-center justify-between\">\n      \u003Cspan>{item.date}\u003C/span>\n      \u003Cdiv className=\"flex items-center space-x-2\">\n        \u003Cspan className=\"capitalize\">{item.status.replace(\"-\", \" \")}\u003C/span>\n        \u003Cdiv className={`w-2 h-2 rounded-full ${getStatusColor(item.status)}`} />\n      \u003C/div>\n    \u003C/div>\n  );\n} \n","historical-status-item.tsx","tsx",[3631],{"type":27,"tag":653,"props":3632,"children":3633},{"__ignoreMap":5},[3634],{"type":32,"value":3627},{"type":27,"tag":28,"props":3636,"children":3637},{},[3638],{"type":32,"value":3639},"There’s a little bit more that we need to do in order to test our component. In order to test it, we need to render it and properly set up. For that, we‘ll install a couple of utilities that will help us with that.",{"type":27,"tag":793,"props":3641,"children":3644},{"className":3642,"code":3643,"language":3589,"meta":5},[3587],"npm i @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom\n",[3645],{"type":27,"tag":653,"props":3646,"children":3647},{"__ignoreMap":5},[3648],{"type":32,"value":3643},{"type":27,"tag":28,"props":3650,"children":3651},{},[3652,3654,3660],{"type":32,"value":3653},"These utilities will expand capabilities of Vitest, add the proper testing environment and help us set up our components to be tested. Firstly, we need to add react plugin to the ",{"type":27,"tag":653,"props":3655,"children":3657},{"className":3656},[],[3658],{"type":32,"value":3659},"vitest.config.ts",{"type":32,"value":3661}," file, as well as set up the testing environment.",{"type":27,"tag":793,"props":3663,"children":3669},{"className":3664,"code":3665,"filename":3659,"highlights":3666,"language":3520,"meta":5},[3517],"import { defineConfig } from 'vitest/config'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({\n  plugins: [react()],\n  test: {\n    environment: 'jsdom'\n  },\n})\n",[3667,3668],5,7,[3670],{"type":27,"tag":653,"props":3671,"children":3672},{"__ignoreMap":5},[3673],{"type":32,"value":3665},{"type":27,"tag":28,"props":3675,"children":3676},{},[3677,3679,3685],{"type":32,"value":3678},"Now, everything is ready to write our test. We want to render our ",{"type":27,"tag":653,"props":3680,"children":3682},{"className":3681},[],[3683],{"type":32,"value":3684},"\u003CHistoricalStatusItem />",{"type":32,"value":3686}," component. As we mentioned, this component requires data to be passed in. We can simply create mock data item in our test, pass it into our component and make our assertions.",{"type":27,"tag":793,"props":3688,"children":3692},{"className":3689,"code":3690,"filename":3691,"language":3629,"meta":5},[3626],"import '@testing-library/jest-dom/vitest' \nimport { render, screen } from '@testing-library/react'\nimport { HistoricalStatusItem } from './historical-status-item'\nimport { expect, test } from 'vitest'\n\ntest('renders date and status correctly', () => {\n  const mockItem = {\n    date: '2023-05-30',\n    status: 'partial-outage' as const\n  }\n\n  render(\u003CHistoricalStatusItem item={mockItem} />)\n\n  expect(screen.getByText('2023-05-30')).toBeInTheDocument()\n  expect(screen.getByText('partial outage')).toBeInTheDocument()\n})\n","historical-status-item.spec.ts",[3693],{"type":27,"tag":653,"props":3694,"children":3695},{"__ignoreMap":5},[3696],{"type":32,"value":3690},{"type":27,"tag":28,"props":3698,"children":3699},{},[3700,3702,3708,3710,3716],{"type":32,"value":3701},"Once we create multiple component tests, it may be worth moving some of the common stuff into its own file. A typical example of this would be ",{"type":27,"tag":653,"props":3703,"children":3705},{"className":3704},[],[3706],{"type":32,"value":3707},"@testing-library/jest-dom",{"type":32,"value":3709}," import, that will be needed everytime we want to work with a DOM element. We can move that import into ",{"type":27,"tag":653,"props":3711,"children":3713},{"className":3712},[],[3714],{"type":32,"value":3715},"vitest.setup.ts",{"type":32,"value":3717}," file and add it as a setup file to our config.",{"type":27,"tag":793,"props":3719,"children":3724},{"className":3720,"code":3721,"filename":3659,"highlights":3722,"language":3520,"meta":5},[3517],"import { defineConfig } from 'vitest/config'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({\n  plugins: [react()],\n  test: {\n    environment: 'jsdom',\n    setupFiles: ['./vitest.setup.ts'],\n  },\n})\n",[3723],8,[3725],{"type":27,"tag":653,"props":3726,"children":3727},{"__ignoreMap":5},[3728],{"type":32,"value":3721},{"type":27,"tag":45,"props":3730,"children":3732},{"id":3731},"components-with-managed-state",[3733],{"type":32,"value":3734},"Components with managed state",{"type":27,"tag":28,"props":3736,"children":3737},{},[3738],{"type":32,"value":3739},"Testing components that use state management can be a bit trickier. In this application, we're using Zustand for state management. The component displays system statuses taken from the store and displays it in a card layout:",{"type":27,"tag":793,"props":3741,"children":3747},{"className":3742,"code":3743,"filename":3744,"highlights":3745,"language":3629,"meta":5},[3626],"\"use client\"\n\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport { getStatusColor, getOverallStatus } from \"@/utils/statusData\"\nimport { useStatusStore } from \"@/stores/statusStore\"\n\nexport function CurrentStatus() {\n  const systems = useStatusStore((state) => state.systems)\n  const overallStatus = getOverallStatus(systems)\n  const statusColor = getStatusColor(overallStatus)\n\n  return (\n    \u003CCard>\n      \u003CCardHeader>\n        \u003CCardTitle>Current Status\u003C/CardTitle>\n      \u003C/CardHeader>\n      \u003CCardContent>\n        \u003Cdiv className=\"flex items-center space-x-2 mb-4\">\n          \u003Cdiv className={`w-3 h-3 rounded-full ${statusColor}`} />\n          \u003Cspan className=\"font-semibold capitalize\">{overallStatus.replace(\"-\", \" \")}\u003C/span>\n        \u003C/div>\n        \u003Cdiv className=\"grid gap-2\">\n          {systems.map((system) => (\n            \u003Cdiv key={system.name} className=\"flex items-center justify-between\">\n              \u003Cspan>{system.name}\u003C/span>\n              \u003Cdiv className={`w-2 h-2 rounded-full ${getStatusColor(system.status)}`} />\n            \u003C/div>\n          ))}\n        \u003C/div>\n      \u003C/CardContent>\n    \u003C/Card>\n  )\n}\n","current-status.tsx",[3723,3746],9,[3748],{"type":27,"tag":653,"props":3749,"children":3750},{"__ignoreMap":5},[3751],{"type":32,"value":3743},{"type":27,"tag":28,"props":3753,"children":3754},{},[3755],{"type":32,"value":3756},"To test this component, we need to mock the store. We don't want to rely on the actual state management in our tests. In many real-life scenarios, the state might not even be easily accessible. This is where Vitest's mocking capabilities come in handy.",{"type":27,"tag":28,"props":3758,"children":3759},{},[3760,3762,3768],{"type":32,"value":3761},"We can use ",{"type":27,"tag":653,"props":3763,"children":3765},{"className":3764},[],[3766],{"type":32,"value":3767},"vi.mock()",{"type":32,"value":3769},", to intercept the store import and provide our own data. We are simply passing our own mock data to what the component would normally receive from the store.",{"type":27,"tag":28,"props":3771,"children":3772},{},[3773,3775,3781,3783,3789,3791,3796],{"type":32,"value":3774},"Our component would normally fetch the store data using ",{"type":27,"tag":653,"props":3776,"children":3778},{"className":3777},[],[3779],{"type":32,"value":3780},"useStatusStore",{"type":32,"value":3782}," hook, but in this case, we use ",{"type":27,"tag":653,"props":3784,"children":3786},{"className":3785},[],[3787],{"type":32,"value":3788},"vi.fn()",{"type":32,"value":3790}," to basically tell our component that this is what the ",{"type":27,"tag":653,"props":3792,"children":3794},{"className":3793},[],[3795],{"type":32,"value":3780},{"type":32,"value":3797}," hook now returns.",{"type":27,"tag":28,"props":3799,"children":3800},{},[3801],{"type":32,"value":3802},"We then verify that all mocked data was properly rendered in the component.",{"type":27,"tag":793,"props":3804,"children":3813},{"className":3805,"code":3806,"filename":3807,"highlights":3808,"language":3629,"meta":5},[3626],"import { render, screen } from '@testing-library/react'\nimport { CurrentStatus } from './current-status'\nimport { expect, test, vi } from 'vitest'\n\ntest('renders all system statuses', () => {\n  vi.mock('store data', () => ({\n    useStatusStore: vi.fn((selector) => selector({ systems: [\n      { name: \"API\", status: \"operational\" },\n      { name: \"Web App\", status: \"operational\" },\n      { name: \"Database\", status: \"operational\" }\n    ]}))\n  }))\n\n  render(\u003CCurrentStatus />)\n  \n  expect(screen.getByText('API')).toBeInTheDocument()\n  expect(screen.getByText('Web App')).toBeInTheDocument()\n  expect(screen.getByText('Database')).toBeInTheDocument()\n})\n","current-status.spec.ts",[3809,3668,3723,3746,3810,3811,3812],6,10,11,12,[3814],{"type":27,"tag":653,"props":3815,"children":3816},{"__ignoreMap":5},[3817],{"type":32,"value":3806},{"type":27,"tag":28,"props":3819,"children":3820},{},[3821],{"type":32,"value":3822},"This allows us to test how our component renders and behaves with different system states without needing to set up the entire state management infrastructure.",{"type":27,"tag":45,"props":3824,"children":3826},{"id":3825},"browser-mode",[3827],{"type":32,"value":3828},"Browser mode",{"type":27,"tag":28,"props":3830,"children":3831},{},[3832],{"type":32,"value":3833},"Vitest has another cool feature called browser mode. This is currently in an experimental stage, but it’s already quite capable. This takes component to another level, since you can render your components right inside the browser - and environment where they are supposed to be used.",{"type":27,"tag":28,"props":3835,"children":3836},{},[3837,3839,3845],{"type":32,"value":3838},"You can even interact with the components using either Playwright or Webdriver.io. The instiallatioin is pretty straightforward and a nice CLI tool will guide you through the process after you run ",{"type":27,"tag":653,"props":3840,"children":3842},{"className":3841},[],[3843],{"type":32,"value":3844},"npx vitest init browser",{"type":32,"value":3846},". This will install all the necessary dependencies and set up the config file for you.",{"type":27,"tag":793,"props":3848,"children":3854},{"className":3849,"code":3850,"filename":3659,"highlights":3851,"language":3520,"meta":5},[3517],"import { defineConfig } from 'vitest/config'\nimport react from '@vitejs/plugin-react'\nimport { resolve } from 'path'\n\nexport default defineConfig({\n  plugins: [react()],\n  test: {\n    environment: 'jsdom',\n    setupFiles: ['./vitest.setup.ts'],\n    browser: {\n      enabled: true,\n      name: 'chromium',\n      provider: 'playwright'\n    }\n  }\n})\n",[3810,3811,3812,3852,3853],13,14,[3855],{"type":27,"tag":653,"props":3856,"children":3857},{"__ignoreMap":5},[3858],{"type":32,"value":3850},{"type":27,"tag":28,"props":3860,"children":3861},{},[3862,3864,3870],{"type":32,"value":3863},"Our test now needs a slight update so that it appears inside the browser page. We are simply locating the base element using ",{"type":27,"tag":653,"props":3865,"children":3867},{"className":3866},[],[3868],{"type":32,"value":3869},"page.elementLocator()",{"type":32,"value":3871}," and then using it to make assertions on the component.",{"type":27,"tag":793,"props":3873,"children":3880},{"className":3874,"code":3875,"filename":3807,"highlights":3876,"language":3629,"meta":5},[3626],"import { render } from '@testing-library/react'\nimport { CurrentStatus } from './current-status'\nimport { expect, test, vi } from 'vitest'\nimport { page } from '@vitest/browser/context'\n\ntest('renders all system statuses', async () => {\n  vi.mock('@/stores/statusStore', () => ({\n    useStatusStore: vi.fn((selector) => selector({ systems: [\n      { name: \"API\", status: \"operational\" },\n      { name: \"Web App\", status: \"operational\" },\n      { name: \"Database\", status: \"operational\" }\n    ]}))\n  }))\n\n  const { baseElement } = render(\u003CCurrentStatus />)\n  const screen = page.elementLocator(baseElement)\n  \n  await expect.element(screen.getByText('API')).toBeInTheDocument()\n  await expect.element(screen.getByText('Web App')).toBeInTheDocument()\n  await expect.element(screen.getByText('Database')).toBeInTheDocument()\n})\n",[3877,3878,3879],4,15,16,[3881],{"type":27,"tag":653,"props":3882,"children":3883},{"__ignoreMap":5},[3884],{"type":32,"value":3875},{"type":27,"tag":28,"props":3886,"children":3887},{},[3888],{"type":32,"value":3889},"When you now run Vitest in UI mode, you’ll now see a new section where you can view your component.",{"type":27,"tag":28,"props":3891,"children":3892},{},[3893],{"type":27,"tag":959,"props":3894,"children":3897},{"alt":3895,"src":3896},"Vitest browser mode","vitest_browser_mode_pk4fil.png",[],{"type":27,"tag":28,"props":3899,"children":3900},{},[3901,3903,3908],{"type":32,"value":3902},"On first try, you might not see your styles, so updating your ",{"type":27,"tag":653,"props":3904,"children":3906},{"className":3905},[],[3907],{"type":32,"value":3715},{"type":32,"value":3909}," file to include your css file might help.",{"type":27,"tag":793,"props":3911,"children":3914},{"className":3912,"code":3913,"filename":3715,"language":3520,"meta":5},[3517],"import '@testing-library/jest-dom/vitest' \nimport '@/app/globals.css'\n",[3915],{"type":27,"tag":653,"props":3916,"children":3917},{"__ignoreMap":5},[3918],{"type":32,"value":3913},{"type":27,"tag":45,"props":3920,"children":3922},{"id":3921},"code-coverage",[3923],{"type":32,"value":3924},"Code coverage",{"type":27,"tag":28,"props":3926,"children":3927},{},[3928,3930,3936],{"type":32,"value":3929},"I personally am a fan of code coverage. For me, code coverage is not about chasing numbers, but about revealing blind spots in your code. I’ve played quite a lot with code ",{"type":27,"tag":172,"props":3931,"children":3933},{"href":3932},"/understanding-code-coverage",[3934],{"type":32,"value":3935},"coverage in Cypress",{"type":32,"value":3937}," in the past.",{"type":27,"tag":28,"props":3939,"children":3940},{},[3941],{"type":32,"value":3942},"The coolest thing about code coverage in Vitest is that there’s no app instrumentation needed. Vitest uses Chrome’s v8 coverage to generate the coverage report.",{"type":27,"tag":28,"props":3944,"children":3945},{},[3946,3948,3954],{"type":32,"value":3947},"All you need to do is run your tests with the ",{"type":27,"tag":653,"props":3949,"children":3951},{"className":3950},[],[3952],{"type":32,"value":3953},"--coverage",{"type":32,"value":3955}," flag.",{"type":27,"tag":793,"props":3957,"children":3960},{"className":3958,"code":3959,"language":3589,"meta":5},[3587],"npx vitest --coverage\n",[3961],{"type":27,"tag":653,"props":3962,"children":3963},{"__ignoreMap":5},[3964],{"type":32,"value":3959},{"type":27,"tag":28,"props":3966,"children":3967},{},[3968,3970,3976,3978,3984],{"type":32,"value":3969},"On first run this will prompt you to install ",{"type":27,"tag":653,"props":3971,"children":3973},{"className":3972},[],[3974],{"type":32,"value":3975},"@vitest/coverage-v8",{"type":32,"value":3977}," package. Once you run your test a coverage report will be generated. It’s saved in the ",{"type":27,"tag":653,"props":3979,"children":3981},{"className":3980},[],[3982],{"type":32,"value":3983},"coverage",{"type":32,"value":3985}," folder, but it can be viewed directly in the UI mode.",{"type":27,"tag":28,"props":3987,"children":3988},{},[3989],{"type":27,"tag":959,"props":3990,"children":3993},{"alt":3991,"src":3992},"Vitest code coverage","vitest_code_coverage_sol1hz.png",[],{"type":27,"tag":45,"props":3995,"children":3997},{"id":3996},"conclusion",[3998],{"type":32,"value":3999},"Conclusion",{"type":27,"tag":28,"props":4001,"children":4002},{},[4003],{"type":32,"value":4004},"I tried Vitest pretty much out of curiosity, but I was impressed by how easy it was to set up and how good the overall developer experience is. I’m really excited about the browser mode, which is blazingly fast. The fact that it uses Playwright to interact with the components carries a lot of potential in my view. The component and e2e testing gets a bit closer together. Combined with the code coverage, I think we might be looking at a very cool testing setup.",{"type":27,"tag":28,"props":4006,"children":4007},{},[4008],{"type":32,"value":4009},"I’m looking forward to playing more with Vitest and seeing how it evolves.",{"title":5,"searchDepth":320,"depth":320,"links":4011},[4012,4013,4014,4015,4016,4017,4018],{"id":3478,"depth":320,"text":3481},{"id":3548,"depth":320,"text":3551},{"id":3606,"depth":320,"text":3609},{"id":3731,"depth":320,"text":3734},{"id":3825,"depth":320,"text":3828},{"id":3921,"depth":320,"text":3924},{"id":3996,"depth":320,"text":3999},"content:introduction-to-testing-in-vitest:index.md","introduction-to-testing-in-vitest/index.md","introduction-to-testing-in-vitest/index",{"_path":4023,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":4024,"description":4025,"date":4026,"published":10,"slug":4027,"image":4028,"tags":4029,"playwrightVersion":4030,"readingTime":4031,"body":4036,"_type":329,"_id":4473,"_source":331,"_file":4474,"_stem":4475,"_extension":334},"/how-to-do-authentication-in-playwright","How to do authentication in Playwright","Learn different approaches to handle authentication in Playwright, from basic login sequences to advanced techniques like session storage and API auth.","2024-12-10","how-to-do-authentication-in-playwright","auth_playwright_jiorkj.png",[1629,2431],"1.49.0",{"text":4032,"minutes":4033,"time":4034,"words":4035},"6 min read",5.275,316500,1055,{"type":24,"children":4037,"toc":4463},[4038,4043,4053,4059,4064,4070,4075,4085,4090,4099,4105,4110,4119,4132,4141,4147,4152,4172,4183,4192,4204,4216,4225,4256,4262,4282,4292,4304,4370,4380,4408,4416,4428,4439,4451],{"type":27,"tag":28,"props":4039,"children":4040},{},[4041],{"type":32,"value":4042},"Authentication is usually the first hurdle to overcome when setting up test automation. Depending on how complicated the authentication method is, it can be a daunting task. Let’s start with a simple example of a login sequence.",{"type":27,"tag":793,"props":4044,"children":4048},{"className":4045,"code":4046,"filename":4047,"language":3520,"meta":5},[3517],"import { test, expect } from '@playwright/test';\n\ntest(\"login\", async ({ page }) => {  \n  await page.goto('/login');\n  await page.getByLabel('Email').fill('test@example.com');\n  await page.getByLabel('Password').fill('password');\n  await page.getByRole('button', { name: 'Login' }).click();\n  await expect(page.getByText('Welcome, Filip!')).toBeVisible();\n});\n","login.spec.ts",[4049],{"type":27,"tag":653,"props":4050,"children":4051},{"__ignoreMap":5},[4052],{"type":32,"value":4046},{"type":27,"tag":45,"props":4054,"children":4056},{"id":4055},"abstracting-the-login-sequence",[4057],{"type":32,"value":4058},"Abstracting the login sequence",{"type":27,"tag":28,"props":4060,"children":4061},{},[4062],{"type":32,"value":4063},"Since this is a sequence that will probable need to be repeated in multiple test, we can extract it into a separate block. This could be a function module, or a page object.",{"type":27,"tag":1033,"props":4065,"children":4067},{"id":4066},"function-module",[4068],{"type":32,"value":4069},"Function module",{"type":27,"tag":28,"props":4071,"children":4072},{},[4073],{"type":32,"value":4074},"In case of a function, the module might look like this:",{"type":27,"tag":793,"props":4076,"children":4080},{"className":4077,"code":4078,"filename":4079,"language":3520,"meta":5},[3517],"export const login = async (page: Page) => {\n  await page.goto('/login');\n  await page.getByLabel('Email').fill('test@example.com');\n  await page.getByLabel('Password').fill('password');\n  await page.getByRole('button', { name: 'Login' }).click();\n  await expect(page.getByText('Welcome, Filip!')).toBeVisible();\n}\n","login.ts",[4081],{"type":27,"tag":653,"props":4082,"children":4083},{"__ignoreMap":5},[4084],{"type":32,"value":4078},{"type":27,"tag":28,"props":4086,"children":4087},{},[4088],{"type":32,"value":4089},"We can now use this function in our test.",{"type":27,"tag":793,"props":4091,"children":4094},{"className":4092,"code":4093,"filename":4047,"language":3520,"meta":5},[3517],"import { test, expect } from '@playwright/test';\nimport { login } from './login';\n\ntest(\"login\", async ({ page }) => {  \n  await login(page);\n  await expect(page.getByText('Welcome, Filip!')).toBeVisible();\n});\n",[4095],{"type":27,"tag":653,"props":4096,"children":4097},{"__ignoreMap":5},[4098],{"type":32,"value":4093},{"type":27,"tag":1033,"props":4100,"children":4102},{"id":4101},"page-object",[4103],{"type":32,"value":4104},"Page object",{"type":27,"tag":28,"props":4106,"children":4107},{},[4108],{"type":32,"value":4109},"As Playwright encourages the use of page objects, we can create one that might look like this:",{"type":27,"tag":793,"props":4111,"children":4114},{"className":4112,"code":4113,"filename":4079,"language":3520,"meta":5},[3517],"export class LoginPage {\n  constructor(private page: Page) {}\n\n  async login() {\n    await this.page.goto('/login');\n    await this.page.getByLabel('Email').fill('test@example.com');\n    await this.page.getByLabel('Password').fill('password');\n    await this.page.getByRole('button', { name: 'Login' }).click();\n    await expect(this.page.getByText('Welcome, Filip!')).toBeVisible();\n  }\n}\n",[4115],{"type":27,"tag":653,"props":4116,"children":4117},{"__ignoreMap":5},[4118],{"type":32,"value":4113},{"type":27,"tag":28,"props":4120,"children":4121},{},[4122,4124,4130],{"type":32,"value":4123},"This page object can now be used in our test. We’ll need to create an instance of it and call the ",{"type":27,"tag":653,"props":4125,"children":4127},{"className":4126},[],[4128],{"type":32,"value":4129},"login",{"type":32,"value":4131}," method. This approach is simplistic, in a real world scenario you’ll likely pass different login parameters and make the page object more flexible.",{"type":27,"tag":793,"props":4133,"children":4136},{"className":4134,"code":4135,"filename":4047,"language":3520,"meta":5},[3517],"import { test, expect } from '@playwright/test';\nimport { LoginPage } from './login';\n\ntest(\"login\", async ({ page }) => {  \n  const loginPage = new LoginPage(page);\n  await loginPage.login();\n  await expect(page.getByText('Welcome, Filip!')).toBeVisible();\n});\n",[4137],{"type":27,"tag":653,"props":4138,"children":4139},{"__ignoreMap":5},[4140],{"type":32,"value":4135},{"type":27,"tag":45,"props":4142,"children":4144},{"id":4143},"storing-browser-state",[4145],{"type":32,"value":4146},"Storing browser state",{"type":27,"tag":28,"props":4148,"children":4149},{},[4150],{"type":32,"value":4151},"However, there are some problems with these approaches. While we stick to the DRY principle in terms of the code, when it comes to execution, we are repeating ourselves over and over. Each test will navigate to the login page, fill in the form, click the login button and then check if the login was successful.",{"type":27,"tag":28,"props":4153,"children":4154},{},[4155,4157,4163,4165,4171],{"type":32,"value":4156},"With modern web testing tools such as Playwright this is no longer inevitable. We can capture the authentication state and reuse it in multiple tests. To do this, we’ll use two parts of Playwright’s API that help us with this: ",{"type":27,"tag":653,"props":4158,"children":4160},{"className":4159},[],[4161],{"type":32,"value":4162},"context.storageState",{"type":32,"value":4164}," and ",{"type":27,"tag":653,"props":4166,"children":4168},{"className":4167},[],[4169],{"type":32,"value":4170},"test.use",{"type":32,"value":256},{"type":27,"tag":28,"props":4173,"children":4174},{},[4175,4176,4181],{"type":32,"value":3349},{"type":27,"tag":653,"props":4177,"children":4179},{"className":4178},[],[4180],{"type":32,"value":4162},{"type":32,"value":4182}," is a property that allows us to store the browser state. It can either be saved to a separate file or returned from the funtion to be saved in a variable. A simple demonstration of how this works:",{"type":27,"tag":793,"props":4184,"children":4187},{"className":4185,"code":4186,"filename":4047,"language":3520,"meta":5},[3517],"import { test, expect } from '@playwright/test';\n\ntest(\"login\", async ({ page }) => {  \n  await page.goto('/login');\n  await page.getByLabel('Email').fill('test@example.com');\n  await page.getByLabel('Password').fill('password');\n  await page.getByRole('button', { name: 'Login' }).click();\n  await expect(page.getByText('Welcome, Filip!')).toBeVisible();\n  await page.context().storageState({ path: 'playwright/.auth.json' }); \n});\n",[4188],{"type":27,"tag":653,"props":4189,"children":4190},{"__ignoreMap":5},[4191],{"type":32,"value":4186},{"type":27,"tag":28,"props":4193,"children":4194},{},[4195,4197,4203],{"type":32,"value":4196},"This will save the browser cookies and local storage state to a file in the ",{"type":27,"tag":653,"props":4198,"children":4200},{"className":4199},[],[4201],{"type":32,"value":4202},"playwright/.auth.json",{"type":32,"value":786},{"type":27,"tag":28,"props":4205,"children":4206},{},[4207,4209,4214],{"type":32,"value":4208},"This file can then be reused in multiple tests. The ",{"type":27,"tag":653,"props":4210,"children":4212},{"className":4211},[],[4213],{"type":32,"value":4170},{"type":32,"value":4215}," function is a hook that allows us to use the authentication state in our tests.",{"type":27,"tag":793,"props":4217,"children":4220},{"className":4218,"code":4219,"filename":4047,"language":3520,"meta":5},[3517],"import { test, expect } from '@playwright/test';\n\ntest.use({ storageState: 'playwright/.auth.json' });\n\ntest(\"login\", async ({ page }) => {  \n  await page.goto('/login');\n  await expect(page.getByText('Welcome, Filip!')).toBeVisible();\n});\n",[4221],{"type":27,"tag":653,"props":4222,"children":4223},{"__ignoreMap":5},[4224],{"type":32,"value":4219},{"type":27,"tag":1029,"props":4226,"children":4227},{},[4228,4234,4247],{"type":27,"tag":1033,"props":4229,"children":4231},{"id":4230},"watch-out",[4232],{"type":32,"value":4233},"Watch out!",{"type":27,"tag":28,"props":4235,"children":4236},{},[4237,4239,4245],{"type":32,"value":4238},"If you save your browser state to a file, you need to make sure that the file or the folder is not committed to your repository. This file contains sensitive information. Use ",{"type":27,"tag":653,"props":4240,"children":4242},{"className":4241},[],[4243],{"type":32,"value":4244},".gitignore",{"type":32,"value":4246}," to ignore the file or the folder:",{"type":27,"tag":793,"props":4248,"children":4251},{"className":4249,"code":4250,"filename":4244,"language":2250,"meta":5},[2248],"playwright/.auth.json\n",[4252],{"type":27,"tag":653,"props":4253,"children":4254},{"__ignoreMap":5},[4255],{"type":32,"value":4250},{"type":27,"tag":45,"props":4257,"children":4259},{"id":4258},"global-setup",[4260],{"type":32,"value":4261},"Global setup",{"type":27,"tag":28,"props":4263,"children":4264},{},[4265,4267,4273,4275,4280],{"type":32,"value":4266},"Since the majority of tests will need authentication we can create a setup file that act as a dependency for all of our tests. This way we can include the setup globally and not have to repeat it in each test. It’s a good practice to distinguish this setup file from specs by using the ",{"type":27,"tag":653,"props":4268,"children":4270},{"className":4269},[],[4271],{"type":32,"value":4272},".setup.ts",{"type":32,"value":4274}," extension instead of ",{"type":27,"tag":653,"props":4276,"children":4278},{"className":4277},[],[4279],{"type":32,"value":2058},{"type":32,"value":4281},". Setup files will then become a good place for all the logic that’s responsible for setting up the state, data or other dependencies for the tests.",{"type":27,"tag":793,"props":4283,"children":4287},{"className":4284,"code":4285,"filename":4286,"language":3520,"meta":5},[3517],"import { test as setup, expect } from '@playwright/test';\n\nsetup(\"user authentication\", async ({ page }) => {  \n  await page.goto('/');\n  await expect(page.getByTestId('cookie-consent-message')).toBeVisible();\n  \n  await page.getByRole('button', { name: 'Accept' }).click();\n  await expect(page.getByText('Cookie Preference Saved')).toBeVisible();\n  \n  await page.context().storageState({ path: 'playwright/.auth.json' });\n});\n","auth.setup.ts",[4288],{"type":27,"tag":653,"props":4289,"children":4290},{"__ignoreMap":5},[4291],{"type":32,"value":4285},{"type":27,"tag":28,"props":4293,"children":4294},{},[4295,4297,4302],{"type":32,"value":4296},"Once the setup file is created, we can set it up as a dependency in ",{"type":27,"tag":653,"props":4298,"children":4300},{"className":4299},[],[4301],{"type":32,"value":2642},{"type":32,"value":4303},". The logic goes as follows:",{"type":27,"tag":851,"props":4305,"children":4306},{},[4307,4333,4352],{"type":27,"tag":109,"props":4308,"children":4309},{},[4310,4311,4317,4319,4324,4326,4331],{"type":32,"value":3349},{"type":27,"tag":653,"props":4312,"children":4314},{"className":4313},[],[4315],{"type":32,"value":4316},"setup",{"type":32,"value":4318}," project is used to run the setup file. This will run our ",{"type":27,"tag":653,"props":4320,"children":4322},{"className":4321},[],[4323],{"type":32,"value":4286},{"type":32,"value":4325}," file which creates the ",{"type":27,"tag":653,"props":4327,"children":4329},{"className":4328},[],[4330],{"type":32,"value":4202},{"type":32,"value":4332}," file with the browser state (lines 6-9).",{"type":27,"tag":109,"props":4334,"children":4335},{},[4336,4337,4343,4345,4350],{"type":32,"value":3349},{"type":27,"tag":653,"props":4338,"children":4340},{"className":4339},[],[4341],{"type":32,"value":4342},"chromium",{"type":32,"value":4344}," project depends on the ",{"type":27,"tag":653,"props":4346,"children":4348},{"className":4347},[],[4349],{"type":32,"value":4316},{"type":32,"value":4351}," project, therefore it will run the setup file before running the tests (line 16).",{"type":27,"tag":109,"props":4353,"children":4354},{},[4355,4356,4361,4363,4368],{"type":32,"value":3349},{"type":27,"tag":653,"props":4357,"children":4359},{"className":4358},[],[4360],{"type":32,"value":4342},{"type":32,"value":4362}," project is used to run the tests and will use the ",{"type":27,"tag":653,"props":4364,"children":4366},{"className":4365},[],[4367],{"type":32,"value":4202},{"type":32,"value":4369}," file as the browser state (line 14).",{"type":27,"tag":793,"props":4371,"children":4375},{"className":4372,"code":4373,"filename":2642,"highlights":4374,"language":3520,"meta":5},[3517],"import { defineConfig } from '@playwright/test';\n\nexport default defineConfig({\n  testDir: './tests',\n  projects: [\n    { \n      name: 'setup', \n      testMatch: /.*\\.setup\\.ts/\n    },\n    {\n      name: 'chromium',\n      use: { \n        ...devices['Desktop Chrome'], \n        storageState: './playwright/.auth.json' \n      },\n      dependencies: ['setup'],\n    },\n  ],\n});\n",[3809,3668,3723,3746,3853,3879],[4376],{"type":27,"tag":653,"props":4377,"children":4378},{"__ignoreMap":5},[4379],{"type":32,"value":4373},{"type":27,"tag":28,"props":4381,"children":4382},{},[4383,4385,4391,4393,4398,4400,4406],{"type":32,"value":4384},"Note that if you want to try this in ",{"type":27,"tag":653,"props":4386,"children":4388},{"className":4387},[],[4389],{"type":32,"value":4390},"--ui",{"type":32,"value":4392}," mode, you need to make sure you’ll have the ",{"type":27,"tag":653,"props":4394,"children":4396},{"className":4395},[],[4397],{"type":32,"value":4316},{"type":32,"value":4399}," project checked in the ",{"type":27,"tag":653,"props":4401,"children":4403},{"className":4402},[],[4404],{"type":32,"value":4405},"projects",{"type":32,"value":4407}," list otherwise it will be skipped. This might be obvious to experienced users, but it didn’t occured to me when I was first trying to set it up (headless mode works fine, because it runs all projects by default).",{"type":27,"tag":28,"props":4409,"children":4410},{},[4411],{"type":27,"tag":959,"props":4412,"children":4415},{"alt":4413,"src":4414},"playwright ui","playwright_ui_projects_jhk2f.png",[],{"type":27,"tag":28,"props":4417,"children":4418},{},[4419,4421,4426],{"type":32,"value":4420},"The approach with setup file works really well because you’ll also test your login flow. Once that is working Playwright moves on to other tests. But there might be more cases where you don’t want to be logged in. For these tests you can simply fall back to the ",{"type":27,"tag":653,"props":4422,"children":4424},{"className":4423},[],[4425],{"type":32,"value":4170},{"type":32,"value":4427}," hook and reset the state for current test.",{"type":27,"tag":793,"props":4429,"children":4434},{"className":4430,"code":4431,"filename":4432,"highlights":4433,"language":3520,"meta":5},[3517],"import { test, expect } from '@playwright/test';\n\n// Reset the state for current test\ntest.use({ storageState: { cookies: [], origins: [] } });\n\ntest(\"redirected to login\", async ({ page }) => {  \n  await page.goto('/home');\n  await expect(page.getByText('Please login to continue')).toBeVisible();\n});\n","redirect.spec.ts",[3877],[4435],{"type":27,"tag":653,"props":4436,"children":4437},{"__ignoreMap":5},[4438],{"type":32,"value":4431},{"type":27,"tag":28,"props":4440,"children":4441},{},[4442,4444,4449],{"type":32,"value":4443},"Storing state and reusing it in multiple tests is ",{"type":27,"tag":79,"props":4445,"children":4446},{},[4447],{"type":32,"value":4448},"the",{"type":32,"value":4450}," best way to handle authentication. It reduces the time to execute tests and even makes them more reliable. If your application is secured against brute force attacks (as it should be), you will limit the number of requests to your login endpoint. This creates less strain on your backend, but also means that you have lesser chance to get locked out by captcha or other security measures.",{"type":27,"tag":28,"props":4452,"children":4453},{},[4454,4456,4462],{"type":32,"value":4455},"Hope you like this blogpost. If it helped you, please share it with your friends and colleagues. If you have any questions, please feel free to reach out to me on my ",{"type":27,"tag":172,"props":4457,"children":4459},{"href":3201,"rel":4458},[696],[4460],{"type":32,"value":4461},"social media profiles",{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":4464},[4465,4469,4472],{"id":4055,"depth":320,"text":4058,"children":4466},[4467,4468],{"id":4066,"depth":1606,"text":4069},{"id":4101,"depth":1606,"text":4104},{"id":4143,"depth":320,"text":4146,"children":4470},[4471],{"id":4230,"depth":1606,"text":4233},{"id":4258,"depth":320,"text":4261},"content:how-to-do-authentication-in-playwright:index.md","how-to-do-authentication-in-playwright/index.md","how-to-do-authentication-in-playwright/index",{"_path":4477,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":4478,"description":4479,"date":4480,"published":10,"slug":4481,"tags":4482,"image":4484,"cypressVersion":17,"readingTime":4485,"body":4489,"_type":329,"_id":4754,"_source":331,"_file":4755,"_stem":4756,"_extension":334},"/test-like-a-developer-develop-like-a-tester","Test like a developer, develop like a tester","How testers and developers can work better together. This is a written version of my keynote talk from Front-End Test Fest 2023.","2024-11-27","test-like-a-developer-develop-like-a-tester",[13,4483],"development","test-dev_k03rpj.png",{"text":4032,"minutes":4486,"time":4487,"words":4488},5.765,345900,1153,{"type":24,"children":4490,"toc":4745},[4491,4506,4511,4516,4521,4526,4531,4536,4542,4547,4552,4558,4563,4568,4574,4586,4591,4629,4634,4640,4645,4650,4660,4665,4670,4676,4681,4686,4691,4713,4719,4724,4729,4734,4740],{"type":27,"tag":1029,"props":4492,"children":4493},{},[4494],{"type":27,"tag":28,"props":4495,"children":4496},{},[4497,4499],{"type":32,"value":4498},"Note: this blogpost is a written version of my keynote talk from ",{"type":27,"tag":172,"props":4500,"children":4503},{"href":4501,"rel":4502},"https://www.youtube.com/watch?v=seBOsmxW_tc",[696],[4504],{"type":32,"value":4505},"Front-End Test Fest 2023",{"type":27,"tag":28,"props":4507,"children":4508},{},[4509],{"type":32,"value":4510},"For years now, I’ve been living a double life. During the day, I do my job as a tester. I write test automation, go to meetings, do exploratory testing, take notes, and perform to the best of my abilities. But when night falls, I transform - I become a developer working on my own homepage, creating and enhancing applications, wrestling with bundlers, frameworks, CSS, databases, and APIs.",{"type":27,"tag":28,"props":4512,"children":4513},{},[4514],{"type":32,"value":4515},"Being in both of these worlds got me thinking about life of a developer and a tester. I’ve seen way too many companies where the barrier between these two roles is very high. Testers and developers don’t sit next to each other, don’t talk and worst of all, don’t understand each other. They live their working lives in separate rooms, in separate buildings, or even in separate companies.",{"type":27,"tag":28,"props":4517,"children":4518},{},[4519],{"type":32,"value":4520},"While testers and developers differ in skills, they share common goals (or at least we should). In my opinion, testing and development are two sides of the same coin. When a developer runs their web application in a browser, are they suddenly not a developer? When a tester designs an automated script, have they stopped being a tester?",{"type":27,"tag":28,"props":4522,"children":4523},{},[4524],{"type":32,"value":4525},"Of course not.",{"type":27,"tag":28,"props":4527,"children":4528},{},[4529],{"type":32,"value":4530},"I think this barrier does us both a disservice. Developers have developed amazing strategies for delivering great software and growing as a team. Testers have done the same. By sharing this knowledge, we could create something greater than the sum of our parts - where one plus one truly equals three.",{"type":27,"tag":28,"props":4532,"children":4533},{},[4534],{"type":32,"value":4535},"This blogpost is a collection of thoughts and ideas on how we can work better together. Enjoy.",{"type":27,"tag":45,"props":4537,"children":4539},{"id":4538},"testers-should-get-good-at-understanding-code",[4540],{"type":32,"value":4541},"Testers should get good at understanding code",{"type":27,"tag":28,"props":4543,"children":4544},{},[4545],{"type":32,"value":4546},"I know this thought might trigger some testers, especially those who have been able to build their careers without a deep understanding of code. But I’m not suggesting that testers become developers or start doing code reviews. Nor am I suggesting that if you don’t posses this skill, it makes you a worse tester.",{"type":27,"tag":28,"props":4548,"children":4549},{},[4550],{"type":32,"value":4551},"I’m simply suggesting that getting a better understanding of the building blocks of the application you’re testing will give you another layer of insight. It empowers you to look for issues in places you might not have considered before. It might help you have better technical conversations with developers. It gives you opportunities to ask better questions whether it is during testing or planning.",{"type":27,"tag":45,"props":4553,"children":4555},{"id":4554},"developer-should-work-on-test-flakiness",[4556],{"type":32,"value":4557},"Developer should work on test flakiness",{"type":27,"tag":28,"props":4559,"children":4560},{},[4561],{"type":32,"value":4562},"While it’s often being stuck to test automation engineers, developers should really think about test flakiness as their problem too. I often say that test flakiness is almost always a problem with the application, not with the test. My experience at Replay.io has taught me that. As part of my job I have gone through hundreds of flaky tests and examine the runtime only to discover a re-render, errors in state management or missing interactivity.",{"type":27,"tag":28,"props":4564,"children":4565},{},[4566],{"type":32,"value":4567},"These are all issues that test automation scripts reveal, but are often brushed off as \"test is running too fast\" or \"test is running on a different environment\". But these issues actually reveal issues that might occur with real users too.",{"type":27,"tag":45,"props":4569,"children":4571},{"id":4570},"testers-should-treat-test-automation-as-development",[4572],{"type":32,"value":4573},"Testers should treat test automation as development",{"type":27,"tag":28,"props":4575,"children":4576},{},[4577,4579,4584],{"type":32,"value":4578},"Test automation code is often treated as a second-class citizen, with many teams keeping it separate from the main codebase. This artificial split between development and testing needs to end because ",{"type":27,"tag":79,"props":4580,"children":4581},{},[4582],{"type":32,"value":4583},"test automation is development",{"type":32,"value":4585},". As a test automation engineer, you are both a developer and a tester.",{"type":27,"tag":28,"props":4587,"children":4588},{},[4589],{"type":32,"value":4590},"To elevate your test automation:",{"type":27,"tag":105,"props":4592,"children":4593},{},[4594,4599,4604,4609,4614,4619,4624],{"type":27,"tag":109,"props":4595,"children":4596},{},[4597],{"type":32,"value":4598},"Use developer tools like ESLint, Prettier, and TypeScript",{"type":27,"tag":109,"props":4600,"children":4601},{},[4602],{"type":32,"value":4603},"Build reusable libraries and utilities",{"type":27,"tag":109,"props":4605,"children":4606},{},[4607],{"type":32,"value":4608},"Write comprehensive documentation",{"type":27,"tag":109,"props":4610,"children":4611},{},[4612],{"type":32,"value":4613},"Implement peer reviews with both developers and testers",{"type":27,"tag":109,"props":4615,"children":4616},{},[4617],{"type":32,"value":4618},"Create quality checklists",{"type":27,"tag":109,"props":4620,"children":4621},{},[4622],{"type":32,"value":4623},"Prioritize test efforts as a team",{"type":27,"tag":109,"props":4625,"children":4626},{},[4627],{"type":32,"value":4628},"Celebrate wins and demonstrate value",{"type":27,"tag":28,"props":4630,"children":4631},{},[4632],{"type":32,"value":4633},"By treating test automation with the same rigor as product development, we can break down barriers between developers and testers while delivering higher quality software.",{"type":27,"tag":45,"props":4635,"children":4637},{"id":4636},"developer-should-shift-focus-on-users",[4638],{"type":32,"value":4639},"Developer should shift focus on users",{"type":27,"tag":28,"props":4641,"children":4642},{},[4643],{"type":32,"value":4644},"My friend Andy Knight, in his remarkable talk about \"8 Software Testing Convictions\", talked about the concepts of shifting left and right in testing. \"Shifting right\" in testing emphasizes the importance of what happens after shipping features. As testers, we track bugs, assess their severity, and understand user impact. Developers should build this mindset too.",{"type":27,"tag":28,"props":4646,"children":4647},{},[4648],{"type":32,"value":4649},"At my previous company, we used a simple but effective bug classification system based on quality and quantity. Critical incidents affecting many users were top priority, while low-impact issues affecting few users were lower priority. This created a shared understanding across the organization about what issues truly mattered.",{"type":27,"tag":28,"props":4651,"children":4652},{},[4653],{"type":27,"tag":959,"props":4654,"children":4659},{"alt":4655,"className":4656,"src":4658},"Bug classification system",[4657],"invert","/abcd_jfp6kf.png",[],{"type":27,"tag":28,"props":4661,"children":4662},{},[4663],{"type":32,"value":4664},"Whether you were the CEO, a helpline worker, a tester, or an engineer, this shared model kept everyone focused on what truly mattered. It created a common language around quality and helped everyone understand the vital role of testing in the organization.",{"type":27,"tag":28,"props":4666,"children":4667},{},[4668],{"type":32,"value":4669},"This user-centric approach reminds us that users care about having a working product, not the technical details. As testers, we can help keep development focused on the end user experience.",{"type":27,"tag":45,"props":4671,"children":4673},{"id":4672},"testers-should-focus-on-test-execution",[4674],{"type":32,"value":4675},"Testers should focus on test execution",{"type":27,"tag":28,"props":4677,"children":4678},{},[4679],{"type":32,"value":4680},"The \"Don't Repeat Yourself\" (DRY) principle is a solid foundation for any codebase, though like all principles, it can be taken to extremes. Senior developers excel at making design decisions that enhance code maintainability and readability. As testers responsible for our test code, we should adopt this same mindset.",{"type":27,"tag":28,"props":4682,"children":4683},{},[4684],{"type":32,"value":4685},"However, DRY in test automation extends beyond simply avoiding duplicate functions. We need to apply the DRY principle not just to code creation, but to code execution as well.",{"type":27,"tag":28,"props":4687,"children":4688},{},[4689],{"type":32,"value":4690},"Smart test execution decisions can dramatically improve efficiency. My top two contestants for this are test tagging and code coverage. Test tagging allows you to run relevant tests when needed. Code coverage gets bad reputation for focusing on vanity metrics, but it can be really helpful for identifying gaps and redundancies in your test suite.",{"type":27,"tag":28,"props":4692,"children":4693},{},[4694,4696,4703,4704,4711],{"type":32,"value":4695},"For common scenarios like login, avoid repetitive UI interactions by using API calls or browser state management that tools like ",{"type":27,"tag":172,"props":4697,"children":4700},{"href":4698,"rel":4699},"https://docs.cypress.io/api/commands/session",[696],[4701],{"type":32,"value":4702},"Cypress",{"type":32,"value":4164},{"type":27,"tag":172,"props":4705,"children":4708},{"href":4706,"rel":4707},"https://playwright.dev/docs/auth",[696],[4709],{"type":32,"value":4710},"Playwright",{"type":32,"value":4712}," provide out of the box. This can significantly reduce execution time while maintaining test coverage.",{"type":27,"tag":45,"props":4714,"children":4716},{"id":4715},"developers-should-not-forget-about-testability",[4717],{"type":32,"value":4718},"Developers should not forget about testability",{"type":27,"tag":28,"props":4720,"children":4721},{},[4722],{"type":32,"value":4723},"Let's circle back to the concept of shifting left and an often-overlooked aspect of early testing: testability. As a tester, I once experienced that dreaded moment when suddenly all my tests failed. I investigated, dug through logs, and then discovered the cause - developers introduced CAPTCHA to the application without considering the test environment.",{"type":27,"tag":28,"props":4725,"children":4726},{},[4727],{"type":32,"value":4728},"These situations shouldn't happen. Testing isn't an afterthought - it's a fundamental part of your development strategy. When developers build testability into their applications from the start, they're not just helping testers; they're helping themselves ensure their code works as intended.",{"type":27,"tag":28,"props":4730,"children":4731},{},[4732],{"type":32,"value":4733},"There are numerous ways developers can support testability. Create environment-specific switches to disable rate limiting in test environments. Build tools that help test automation scripts handle authentication smoothly. Work closely with your testing team to understand their needs and implement solutions that enable thorough testing of your code.",{"type":27,"tag":45,"props":4735,"children":4737},{"id":4736},"testers-and-developers-should-share-what-they-have-learned",[4738],{"type":32,"value":4739},"Testers and developers should share what they have learned",{"type":27,"tag":28,"props":4741,"children":4742},{},[4743],{"type":32,"value":4744},"Testers and developers aren't opponents. We're partners in delivering quality software that serves users. By breaking down these artificial barriers, we can create something truly remarkable.",{"title":5,"searchDepth":320,"depth":320,"links":4746},[4747,4748,4749,4750,4751,4752,4753],{"id":4538,"depth":320,"text":4541},{"id":4554,"depth":320,"text":4557},{"id":4570,"depth":320,"text":4573},{"id":4636,"depth":320,"text":4639},{"id":4672,"depth":320,"text":4675},{"id":4715,"depth":320,"text":4718},{"id":4736,"depth":320,"text":4739},"content:test-like-a-developer-develop-like-a-tester:index.md","test-like-a-developer-develop-like-a-tester/index.md","test-like-a-developer-develop-like-a-tester/index",{"_path":4758,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":4759,"description":4760,"date":4761,"published":10,"slug":4762,"tags":4763,"image":4767,"cypressVersion":17,"readingTime":4768,"body":4772,"_type":329,"_id":5040,"_source":331,"_file":5041,"_stem":5042,"_extension":334},"/laid-off-as-a-tester-what-now","Laid off as a tester. What now?","Navigating the changing QA job market: tips for staying relevant, expanding your skillset, and preparing for potential layoffs in software testing","2024-11-16","laid-off-as-a-tester-what-now",[13,4764,4765,4766],"qa","job","layoff","terminated_aomfom.png",{"text":927,"minutes":4769,"time":4770,"words":4771},4.86,291600,972,{"type":24,"children":4773,"toc":5031},[4774,4779,4793,4799,4804,4809,4837,4842,4847,4853,4867,4873,4878,4890,4902,4920,4933,4938,4944,4949,4954,4960,4974,4979,4984,4989,4995,5000,5005,5010,5016,5021,5026],{"type":27,"tag":28,"props":4775,"children":4776},{},[4777],{"type":32,"value":4778},"If you've been in QA for a couple of years now, you might have noticed that the field is rapidly changing. Unfortunately, this also means there are waves of layoffs happening, affecting both junior and senior testers.",{"type":27,"tag":28,"props":4780,"children":4781},{},[4782,4784,4791],{"type":32,"value":4783},"I recently came across a ",{"type":27,"tag":172,"props":4785,"children":4788},{"href":4786,"rel":4787},"https://www.reddit.com/r/QualityAssurance/comments/1gqui2l/comment/lx2572g/",[696],[4789],{"type":32,"value":4790},"Reddit post",{"type":32,"value":4792}," from someone who's been a manual QA for 15 years, and their company is going through some changes. There's a lot of fear about what's coming next, and I wanted to share some thoughts about this because, honestly, this situation isn't uncommon at all.",{"type":27,"tag":45,"props":4794,"children":4796},{"id":4795},"being-stuck-in-a-company-bubble",[4797],{"type":32,"value":4798},"Being Stuck in a company bubble",{"type":27,"tag":28,"props":4800,"children":4801},{},[4802],{"type":32,"value":4803},"As testers, we dive deep into our company's expertise. But when we're no longer with that company, it feels really hard to apply that expertise somewhere else. We oftentimes connect our value to the company we work for, and when that changes, we feel lost. But there’s a lot we have to offer!",{"type":27,"tag":28,"props":4805,"children":4806},{},[4807],{"type":32,"value":4808},"Think about it - testers are usually jack of all trades:",{"type":27,"tag":105,"props":4810,"children":4811},{},[4812,4817,4822,4827,4832],{"type":27,"tag":109,"props":4813,"children":4814},{},[4815],{"type":32,"value":4816},"They do release management",{"type":27,"tag":109,"props":4818,"children":4819},{},[4820],{"type":32,"value":4821},"They find bugs",{"type":27,"tag":109,"props":4823,"children":4824},{},[4825],{"type":32,"value":4826},"They have technical knowledge",{"type":27,"tag":109,"props":4828,"children":4829},{},[4830],{"type":32,"value":4831},"They might know how to code",{"type":27,"tag":109,"props":4833,"children":4834},{},[4835],{"type":32,"value":4836},"They know how to write documentation",{"type":27,"tag":28,"props":4838,"children":4839},{},[4840],{"type":32,"value":4841},"I find it important to write down your knowledge and really understand your own strengths, expertise and experience.",{"type":27,"tag":28,"props":4843,"children":4844},{},[4845],{"type":32,"value":4846},"Because here's the thing - in different companies, you can find that the same skill you posses may be framed in a different way! There might be a company out there looking for exactly your skills, but maybe they're not calling it a \"tester\" role. Maybe it's a product owner, or a scrum master, or a release manager, technical writer, developer experience engineer or something else.",{"type":27,"tag":45,"props":4848,"children":4850},{"id":4849},"never-too-late-to-learn-automation",[4851],{"type":32,"value":4852},"Never too late to learn automation",{"type":27,"tag":28,"props":4854,"children":4855},{},[4856,4858,4865],{"type":32,"value":4857},"If you've been working as a manual QA and haven't really gotten into test automation but think maybe now's the time - I highly recommend checking out ",{"type":27,"tag":172,"props":4859,"children":4862},{"href":4860,"rel":4861},"https://testautomationu.applitools.com/",[696],[4863],{"type":32,"value":4864},"Test Automation University by Applitools",{"type":32,"value":4866},". There's a ton of free resources there, and you can learn any kind of test automation. Don't feel like you've missed the train - it's never too late to start!",{"type":27,"tag":45,"props":4868,"children":4870},{"id":4869},"let-s-talk-about-ai",[4871],{"type":32,"value":4872},"Let s talk about AI",{"type":27,"tag":28,"props":4874,"children":4875},{},[4876],{"type":32,"value":4877},"I know some of you might roll your eyes hearing this (trust me, I get it!), because there's way too much noise out there. It's really hard to figure out what's good info and what's not.",{"type":27,"tag":28,"props":4879,"children":4880},{},[4881,4883,4888],{"type":32,"value":4882},"And while all the AI chatter might be overwhelming, the reality is that AI is here to stay. Many companies are shifting, and if you want to stay on top, you need to gain at least ",{"type":27,"tag":302,"props":4884,"children":4885},{},[4886],{"type":32,"value":4887},"some",{"type":32,"value":4889}," knowledge.",{"type":27,"tag":28,"props":4891,"children":4892},{},[4893,4895,4900],{"type":32,"value":4894},"But gaining AI knowledge doesn't mean you have to become a \"prompt engineer\". Instead, think about how you can apply your existing expertise and ",{"type":27,"tag":302,"props":4896,"children":4897},{},[4898],{"type":32,"value":4899},"enhance",{"type":32,"value":4901}," it with AI. For example, if you're doing test automation, you can:",{"type":27,"tag":105,"props":4903,"children":4904},{},[4905,4910,4915],{"type":27,"tag":109,"props":4906,"children":4907},{},[4908],{"type":32,"value":4909},"Make yourself faster using AI",{"type":27,"tag":109,"props":4911,"children":4912},{},[4913],{"type":32,"value":4914},"Try to make your test automation more stable",{"type":27,"tag":109,"props":4916,"children":4917},{},[4918],{"type":32,"value":4919},"Use AI to boost your learning",{"type":27,"tag":28,"props":4921,"children":4922},{},[4923,4925,4931],{"type":32,"value":4924},"For example, I've been using ",{"type":27,"tag":172,"props":4926,"children":4929},{"href":4927,"rel":4928},"https://www.cursor.com/",[696],[4930],{"type":32,"value":582},{"type":32,"value":4932}," for a couple of months now, and it's been a huge booster to my learning. When it generates code that I don't understand, I can just highlight that piece and ask for an explanation.",{"type":27,"tag":28,"props":4934,"children":4935},{},[4936],{"type":32,"value":4937},"You can think of AI as a tool that will not replace your skills, but will help you enhance them.",{"type":27,"tag":45,"props":4939,"children":4941},{"id":4940},"make-your-work-visible",[4942],{"type":32,"value":4943},"Make your work visible",{"type":27,"tag":28,"props":4945,"children":4946},{},[4947],{"type":32,"value":4948},"Given the current job market, you really want to make your work visible. Even if you're just starting with code, I recommend creating a GitHub account, pushing your repositories and showing what you're working on.",{"type":27,"tag":28,"props":4950,"children":4951},{},[4952],{"type":32,"value":4953},"It’s been a while since I’ve been conducting job interviews, so take my advice with a grain of salt. But whenever I saw candidate with a GitHub account showing their work, they immediately got bonus points in my eyes (for the record, I was never the only decision maker in that process). It also made my job a bit easier because I could see where they're at technically. But most importantly, it shows they're willing to learn and experiment.",{"type":27,"tag":45,"props":4955,"children":4957},{"id":4956},"start-that-blog",[4958],{"type":32,"value":4959},"Start that blog",{"type":27,"tag":28,"props":4961,"children":4962},{},[4963,4965,4972],{"type":32,"value":4964},"Another amazing career hack is to write a blog and share what you're learning. As Angie Jones ",{"type":27,"tag":172,"props":4966,"children":4969},{"href":4967,"rel":4968},"https://www.linkedin.com/feed/update/urn:li:activity:7257027147591041024/",[696],[4970],{"type":32,"value":4971},"mentioned recently",{"type":32,"value":4973},", it can open many doors for you. I am a living example of that.",{"type":27,"tag":28,"props":4975,"children":4976},{},[4977],{"type":32,"value":4978},"Sometimes I get asked for advice on how to start a blog. One thing I see many people do is to try to create a backlog of articles before they start. I strongly advice against this approach, because soon enough, the backlog will run out, and authors create anxiety and pressure on themselves.",{"type":27,"tag":28,"props":4980,"children":4981},{},[4982],{"type":32,"value":4983},"Instead, I advice to make blogpost short. Create something you can write about every week. Focus on one topic only, and write about something you learned this week that you didn't know last week.",{"type":27,"tag":28,"props":4985,"children":4986},{},[4987],{"type":32,"value":4988},"There’s always something to learn and share. Think about it - how cool would it be to find your own blog article that answers exactly what you were struggling with a week ago? If you focus on those who want to learn, you'll always find your audience.",{"type":27,"tag":45,"props":4990,"children":4992},{"id":4991},"get-out-there-and-network",[4993],{"type":32,"value":4994},"Get out there and network",{"type":27,"tag":28,"props":4996,"children":4997},{},[4998],{"type":32,"value":4999},"Go to local meetups, or even travel if there aren't any near you. People usually attend meetups for the content, but I'd argue the most important part is meeting new people. If you want to be prepared for fluctuations in the job market, networking is a great way to do that.",{"type":27,"tag":28,"props":5001,"children":5002},{},[5003],{"type":32,"value":5004},"There may be someone in your network who is currently hiring and looking for interesting people - and maybe you're that interesting person! Having a good network can be a huge help in finding new opportunities.",{"type":27,"tag":28,"props":5006,"children":5007},{},[5008],{"type":32,"value":5009},"Plus, it helps you not stay isolated, which happened to many of us after the pandemic.",{"type":27,"tag":45,"props":5011,"children":5013},{"id":5012},"one-last-thing",[5014],{"type":32,"value":5015},"One Last Thing...",{"type":27,"tag":28,"props":5017,"children":5018},{},[5019],{"type":32,"value":5020},"Even if you're not currently looking for a new job or haven't been laid off, it's really important to think about this stuff. Ask yourself - what's left without the company you're currently working for? There's probably a lot you can offer and a lot you can improve.",{"type":27,"tag":28,"props":5022,"children":5023},{},[5024],{"type":32,"value":5025},"Even if you love your current company, there might be situations where they won't be able to afford to pay you anymore. In those cases, it's really good to be prepared.",{"type":27,"tag":28,"props":5027,"children":5028},{},[5029],{"type":32,"value":5030},"Happy testing, everyone! 🚀",{"title":5,"searchDepth":320,"depth":320,"links":5032},[5033,5034,5035,5036,5037,5038,5039],{"id":4795,"depth":320,"text":4798},{"id":4849,"depth":320,"text":4852},{"id":4869,"depth":320,"text":4872},{"id":4940,"depth":320,"text":4943},{"id":4956,"depth":320,"text":4959},{"id":4991,"depth":320,"text":4994},{"id":5012,"depth":320,"text":5015},"content:laid-off-as-a-tester-what-now:index.md","laid-off-as-a-tester-what-now/index.md","laid-off-as-a-tester-what-now/index",{"_path":5044,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":5045,"description":5046,"date":5047,"published":10,"slug":5048,"tags":5049,"image":5051,"cypressVersion":17,"readingTime":5052,"body":5056,"_type":329,"_id":5269,"_source":331,"_file":5270,"_stem":5271,"_extension":334},"/testing-as-an-entry-gate-to-it","Testing as an entry gate to IT","Some people don’t like the phrase “testing can be a great entry gate into the world of IT”. I kinda disagree, I think testing can be a great entry into the world of IT. This post discusses some reasons why I think that.","2024-01-12","testing-as-an-entry-gate-to-it",[13,5050],"junior","entry_gate_qm8auc.png",{"text":4032,"minutes":5053,"time":5054,"words":5055},5.045,302700,1009,{"type":24,"children":5057,"toc":5264},[5058,5063,5068,5073,5078,5084,5089,5107,5118,5123,5129,5134,5139,5144,5149,5160,5166,5171,5184,5189,5194,5199,5204,5214,5219,5227,5239,5244,5249,5254,5259],{"type":27,"tag":28,"props":5059,"children":5060},{},[5061],{"type":32,"value":5062},"I’ve been hearing the phrase “testing can be a great entry gate into the world of IT” a lot during the years. Meaning - if you have no experience, (for example you come from non-technical field of work) then testing might be the craft that will make the transition much smoother. The expectation is, that the learning curve is less steep compared to for example Mobile app developer.",{"type":27,"tag":28,"props":5064,"children":5065},{},[5066],{"type":32,"value":5067},"Some people don’t like this. They push back against this, saying that they don’t like it when people think of testing this way. That testing is a craft of it’s own and should not merely be thought of as a stepping stone for your career growth.",{"type":27,"tag":28,"props":5069,"children":5070},{},[5071],{"type":32,"value":5072},"And I have to say… I kinda disagree, I think testing can be a great entry into the world of IT. This post discusses some reasons why I think that.",{"type":27,"tag":28,"props":5074,"children":5075},{},[5076],{"type":32,"value":5077},"Let’s first break down what is it that people don’t like about the thought of testing being an entry gate to IT.",{"type":27,"tag":45,"props":5079,"children":5081},{"id":5080},"problematic-parts",[5082],{"type":32,"value":5083},"Problematic parts",{"type":27,"tag":28,"props":5085,"children":5086},{},[5087],{"type":32,"value":5088},"When someone says these words, there’s two problematic parts.",{"type":27,"tag":851,"props":5090,"children":5091},{},[5092,5102],{"type":27,"tag":109,"props":5093,"children":5094},{},[5095,5097],{"type":32,"value":5096},"Testing as an ",{"type":27,"tag":79,"props":5098,"children":5099},{},[5100],{"type":32,"value":5101},"entry gate",{"type":27,"tag":109,"props":5103,"children":5104},{},[5105],{"type":32,"value":5106},"Seemingly hierarchical relationship between testing and the world of IT.",{"type":27,"tag":28,"props":5108,"children":5109},{},[5110,5112,5116],{"type":32,"value":5111},"Viewing testing as an ",{"type":27,"tag":79,"props":5113,"children":5114},{},[5115],{"type":32,"value":5101},{"type":32,"value":5117}," creates an impression that testing is something that anyone can just start doing. It puts a bad light on the people doing this job, because It makes it seem like anyone who is doing testing is doing this entry-level job and that testing is essentially a junior position, no matter how long you have been doing it. And that can be insulting to the people who have been in testing for a long time, and love testing.",{"type":27,"tag":28,"props":5119,"children":5120},{},[5121],{"type":32,"value":5122},"Sometimes, the relationship between testing and the world of IT creates an impression that testing is at the very bottom of a whole hierarchy of IT jobs.",{"type":27,"tag":45,"props":5124,"children":5126},{"id":5125},"practical-implications",[5127],{"type":32,"value":5128},"Practical implications",{"type":27,"tag":28,"props":5130,"children":5131},{},[5132],{"type":32,"value":5133},"Besides these two problematic parts of the statement, there are also the practical implications for this view.",{"type":27,"tag":28,"props":5135,"children":5136},{},[5137],{"type":32,"value":5138},"Maybe you are a QA lead that tries to build a team. If you are in this position, you know how hard it is to find people that have the just the right skills, but also a passion for learning and some other great qualities.",{"type":27,"tag":28,"props":5140,"children":5141},{},[5142],{"type":32,"value":5143},"You probably know the pain when you build a team of great people, and then some of them leave after they gain some developer skills. Even if you wish them the best, it can be frustrating for you as a lead and it can make your team a little bit more fragile.",{"type":27,"tag":28,"props":5145,"children":5146},{},[5147],{"type":32,"value":5148},"Or maybe there’s tension between QA team and developer team and you constantly have to fight for your position in the company. You may be hearing sentences such as “oh the feature is done, we are just waiting for testers”, or “testers are slowing us down”.",{"type":27,"tag":28,"props":5150,"children":5151},{},[5152,5154,5159],{"type":32,"value":5153},"And these are just some of the issues that surface when we you hear the phrase ",{"type":27,"tag":302,"props":5155,"children":5156},{},[5157],{"type":32,"value":5158},"testing is as an entry gate to the world of IT",{"type":32,"value":256},{"type":27,"tag":45,"props":5161,"children":5163},{"id":5162},"about-testing",[5164],{"type":32,"value":5165},"About testing",{"type":27,"tag":28,"props":5167,"children":5168},{},[5169],{"type":32,"value":5170},"Let’s talk about testing for a minute. I’m sure you could find a good definition for testing somewhere on wikipedia, but I like to think about testing as combination of two activities:",{"type":27,"tag":851,"props":5172,"children":5173},{},[5174,5179],{"type":27,"tag":109,"props":5175,"children":5176},{},[5177],{"type":32,"value":5178},"doing",{"type":27,"tag":109,"props":5180,"children":5181},{},[5182],{"type":32,"value":5183},"analysing",{"type":27,"tag":28,"props":5185,"children":5186},{},[5187],{"type":32,"value":5188},"I know this is oversimplified, but bear with me.",{"type":27,"tag":28,"props":5190,"children":5191},{},[5192],{"type":32,"value":5193},"When people say that testing is an entry gate to IT, I think generally they are talking about the “doing” part.",{"type":27,"tag":28,"props":5195,"children":5196},{},[5197],{"type":32,"value":5198},"We build software that is intended to be used by humans. So if you are a human, you can do a good job of doing stuff with the software. In fact the better human you are, the better job you will do.",{"type":27,"tag":28,"props":5200,"children":5201},{},[5202],{"type":32,"value":5203},"So the reason why I don’t think it is in particular harmful to think of testing as an entry gate is that it is in fact open to a pretty broad range of people. Because in order to start doing this job well, you don’t need a technical skill, but you need human skill, which many people have.",{"type":27,"tag":28,"props":5205,"children":5206},{},[5207,5209],{"type":32,"value":5208},"We as testers know how important it is to empathise with our users, to put ourselves in their shoes and understand their perspective when testing. It is a very important quality for testers and it is a testament to the fact that ",{"type":27,"tag":79,"props":5210,"children":5211},{},[5212],{"type":32,"value":5213},"even if testing is the entry gate to the world of IT, it does not mean there are no requirements.",{"type":27,"tag":28,"props":5215,"children":5216},{},[5217],{"type":32,"value":5218},"So the next time you hear someone saying that testing is an entry gate to the world of testing, you can say - \"yeah, and this is what you need to enter\", \"these are the requirements\" and maybe even \"- don’t worry, you got this or\" “I can help you if you want”.",{"type":27,"tag":1029,"props":5220,"children":5221},{},[5222],{"type":27,"tag":28,"props":5223,"children":5224},{},[5225],{"type":32,"value":5226},"There’s no need for gatekeeping the entry gate.",{"type":27,"tag":28,"props":5228,"children":5229},{},[5230,5232,5237],{"type":32,"value":5231},"\"",{"type":27,"tag":302,"props":5233,"children":5234},{},[5235],{"type":32,"value":5236},"But what about the whole craft of testing?",{"type":32,"value":5238},"\", I hear you say. Just because someone has empathy and can think about different perspectives, it does not mean that they are suddenly a great tester. And you’d be absolutely right, there’s more to testing than that.",{"type":27,"tag":28,"props":5240,"children":5241},{},[5242],{"type":32,"value":5243},"Particularly when we think about the whole “doing” and “analysing” thing, really good testers are really good at “doing” and they are really good at the “analyzing” part.",{"type":27,"tag":28,"props":5245,"children":5246},{},[5247],{"type":32,"value":5248},"In other words, if you want to become a great tester, then once you pass that entry gate, you shouldn’t just stop, because you already entered. You should ideally keep on learning.",{"type":27,"tag":28,"props":5250,"children":5251},{},[5252],{"type":32,"value":5253},"The times when testers were simply executing test cases are long gone. We are now developers SREs, performance and security testers, we deal with product and design, and there’s simply a lot more than is being done in the testing craft nowadays. We can safely say that black box testing is over. Testing has become a profession that is filled with different skills, and there are many areas that you can specialize in.",{"type":27,"tag":28,"props":5255,"children":5256},{},[5257],{"type":32,"value":5258},"And this is another reason why thinking of testing as an entry gate to the world of IT is not a bad thing. Testers often find themselves being at the intersection of different professions and oftentimes become jack of all trades, or even specialists in one particular area of testing. I think we should keep the gate open for new people to come in, and help people specialize in the testing flavor they like. But let me know your thoughts.",{"type":27,"tag":28,"props":5260,"children":5261},{},[5262],{"type":32,"value":5263},"Happy testing everyone!",{"title":5,"searchDepth":320,"depth":320,"links":5265},[5266,5267,5268],{"id":5080,"depth":320,"text":5083},{"id":5125,"depth":320,"text":5128},{"id":5162,"depth":320,"text":5165},"content:testing-as-an-entry-gate-to-it:index.md","testing-as-an-entry-gate-to-it/index.md","testing-as-an-entry-gate-to-it/index",{"_path":5273,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":5274,"description":5275,"date":5276,"published":10,"slug":5277,"tags":5278,"image":5281,"cypressVersion":17,"readingTime":5282,"body":5286,"_type":329,"_id":5449,"_source":331,"_file":5450,"_stem":5451,"_extension":334},"/next-big-trend-in-testing-debugging","Next big trend in testing? Debugging","Debugging has been with us and will be for a long time. Making improvements on that experience might just be the next big thing.","2023-08-14","next-big-trend-in-testing-debugging",[1498,5279,1629,5280],"cypress","replay","debugging_yctsuj.png",{"text":585,"minutes":5283,"time":5284,"words":5285},3.825,229500,765,{"type":24,"children":5287,"toc":5443},[5288,5294,5308,5313,5319,5333,5389,5395,5400,5405,5410,5416,5421,5434,5439],{"type":27,"tag":45,"props":5289,"children":5291},{"id":5290},"test-replay-coming-to-cypress",[5292],{"type":32,"value":5293},"Test Replay coming to Cypress",{"type":27,"tag":28,"props":5295,"children":5296},{},[5297,5299,5306],{"type":32,"value":5298},"You may have heard the news about Cypress. With the version 13 they are preparing the biggest release ever. It will introduce a feature called ",{"type":27,"tag":172,"props":5300,"children":5303},{"href":5301,"rel":5302},"https://www.youtube.com/watch?v=hX9Br8QSYgc",[696],[5304],{"type":32,"value":5305},"Test Replay",{"type":32,"value":5307},", that will allow you to browse through your test run. During your test run, all your DOM snapshots, network calls and console logs will be recorded, providing you with a great level of insight into the test run.",{"type":27,"tag":28,"props":5309,"children":5310},{},[5311],{"type":32,"value":5312},"Cypress is an open source solution, but it has a company behind the solution. The premium service that the company offers is the Cypress Cloud service, which allows for an easier parallelisation, test analytics and now - Test Replays.",{"type":27,"tag":45,"props":5314,"children":5316},{"id":5315},"improving-debugging-experience",[5317],{"type":32,"value":5318},"Improving debugging experience",{"type":27,"tag":28,"props":5320,"children":5321},{},[5322,5324,5331],{"type":32,"value":5323},"The choice of this feature to be the next big thing for Cypress is not a coincidence. Providing an insight into an a test that has already ran is something that all testing frameworks need, although it does not really make it to the front of a landing page. Probably because it deals with the less fun part of test automation - debugging. Funilly enough, I polled people on social networks, and ",{"type":27,"tag":172,"props":5325,"children":5328},{"href":5326,"rel":5327},"https://www.linkedin.com/feed/update/urn:li:activity:7099281573690105856/",[696],[5329],{"type":32,"value":5330},"more than 60% people say",{"type":32,"value":5332}," that debugging tests takes the bigger portion of their day.",{"type":27,"tag":28,"props":5334,"children":5335},{},[5336,5338,5345,5347,5354,5356,5363,5364,5371,5373,5380,5381,5388],{"type":32,"value":5337},"Debugging is not often considered to be a fun part, but it’s definitely a daily task for developers and testers alike. While debugging is not as flashy as autonomous testing or AI-powered features are, we’ve been seeing a lot of work being done in this field. Cypress has had Test replays as the vital part of their test runner since the very beginning (timeline in the interactive mode), before it made it to the Cypress Cloud service. Playwright team ",{"type":27,"tag":172,"props":5339,"children":5342},{"href":5340,"rel":5341},"https://github.com/microsoft/playwright/releases/tag/v1.12.0",[696],[5343],{"type":32,"value":5344},"released trace viewer in 2021",{"type":32,"value":5346}," and then has built a ",{"type":27,"tag":172,"props":5348,"children":5351},{"href":5349,"rel":5350},"https://github.com/microsoft/playwright/releases/tag/v1.32.0",[696],[5352],{"type":32,"value":5353},"whole user interface around it",{"type":32,"value":5355},". In the world of Vue, the ",{"type":27,"tag":172,"props":5357,"children":5360},{"href":5358,"rel":5359},"https://devtools.nuxtjs.org/",[696],[5361],{"type":32,"value":5362},"Nuxt.js DevTools",{"type":32,"value":4164},{"type":27,"tag":172,"props":5365,"children":5368},{"href":5366,"rel":5367},"https://devtools.vuejs.org/guide/installation.html#chrome",[696],[5369],{"type":32,"value":5370},"Vue.js DevTools",{"type":32,"value":5372}," have been an inevitable part of development workflow. The same goes for ",{"type":27,"tag":172,"props":5374,"children":5377},{"href":5375,"rel":5376},"https://github.com/reduxjs/redux-devtools",[696],[5378],{"type":32,"value":5379},"React",{"type":32,"value":4164},{"type":27,"tag":172,"props":5382,"children":5385},{"href":5383,"rel":5384},"https://v7.ngrx.io/guide/store-devtools",[696],[5386],{"type":32,"value":5387},"Angular",{"type":32,"value":256},{"type":27,"tag":45,"props":5390,"children":5392},{"id":5391},"debugging-is-a-search-for-knowledge",[5393],{"type":32,"value":5394},"Debugging is a search for knowledge",{"type":27,"tag":28,"props":5396,"children":5397},{},[5398],{"type":32,"value":5399},"All these tools are providing an important aspect to the development workflow - insight.",{"type":27,"tag":28,"props":5401,"children":5402},{},[5403],{"type":32,"value":5404},"And while it has not been making the headlines, it is definitely an area where a lot of work has been done in recent years. There are visible efforts from framework maintainers to improve developer experience by providing great debugging tools.",{"type":27,"tag":28,"props":5406,"children":5407},{},[5408],{"type":32,"value":5409},"So why is it so important? You can think of debugging as the process of gathering insight. A search for cause of a bug in your software or a flakiness in your test is basically a search for knowledge about your application or about your test. This is why companies spend time for creating great DevTools or session replay tools. Providing developers and testers with more insight will essentially teach them how to use the framework better. Test replays in Cypress will give you more insight into test execution, Vue DevTools will give you better understanding of Vue’s lifecycle.",{"type":27,"tag":45,"props":5411,"children":5413},{"id":5412},"the-missing-piece-of-debugging",[5414],{"type":32,"value":5415},"The missing piece of debugging",{"type":27,"tag":28,"props":5417,"children":5418},{},[5419],{"type":32,"value":5420},"However, there is a missing piece in all of this. Most of the time these debugging tools will provide you with information that’s scoped to a certain context. Cypress’ Test Replay will give you insight to the test execution. Vue.js DevTools will provide you with insight into app’s state. But what if your test is flaky because of the app’s state? What if your tests are not written properly, but DOM snapshots will not show that and you need to debug further?",{"type":27,"tag":28,"props":5422,"children":5423},{},[5424,5426,5432],{"type":32,"value":5425},"This is where ",{"type":27,"tag":172,"props":5427,"children":5430},{"href":5428,"rel":5429},"https://replay.io/",[696],[5431],{"type":32,"value":3110},{"type":32,"value":5433}," becomes really handy. It’s a tool made entirely around providing a great debugging experience and providing you with insight. It’s a browser for testers and developers that records everything that’s happening in your app. What it creates is an “interactive recording” that combines video with DOM snapshots, network calls, console logs and source code of your application. What makes it interactive is the ability to add print statements into your source code. Basically, you can add a console.log() into this recording and examine what your application was doing at the time of the recording. Since this is a browser, you can use it as part of your development workflow or run your tests against it. As a result, you are getting the whole scope - things that your test does, as well as things that your app does.",{"type":27,"tag":28,"props":5435,"children":5436},{},[5437],{"type":32,"value":5438},"Replay.io proves that it’s worth to build a tool specifically for debugging. Along with Cypress making improvements in debugging experience in Cloud and other frameworks improving their devtools we can definitely see a trend emerging. A quiet one, but definitely one that’s worth keeping an eye on.",{"type":27,"tag":28,"props":5440,"children":5441},{},[5442],{"type":32,"value":5275},{"title":5,"searchDepth":320,"depth":320,"links":5444},[5445,5446,5447,5448],{"id":5290,"depth":320,"text":5293},{"id":5315,"depth":320,"text":5318},{"id":5391,"depth":320,"text":5394},{"id":5412,"depth":320,"text":5415},"content:next-big-trend-in-testing-debugging:index.md","next-big-trend-in-testing-debugging/index.md","next-big-trend-in-testing-debugging/index",{"_path":5453,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":5454,"description":5455,"date":5456,"published":10,"slug":5457,"tags":5458,"image":5460,"cypressVersion":17,"readingTime":5461,"body":5465,"_type":329,"_id":5789,"_source":331,"_file":5790,"_stem":5791,"_extension":334},"/time-travelling-with-replayio","Time-travelling with Replay.io","Replay.io is a tool that records everything your app does and provides you with debugging superpowers. The recording is a combination of video, source code of your application, DOM snapshots and a timetravel-enabled devtools that allow you to retroactively add print statements to your app’s execution.","2023-07-31 9:00:00","time-travelling-with-replayio",[5459,1498],"replay.io","time_machine_qxamyk",{"text":4032,"minutes":5462,"time":5463,"words":5464},5.665,339900,1133,{"type":24,"children":5466,"toc":5782},[5467,5472,5477,5490,5496,5509,5515,5520,5528,5533,5541,5547,5552,5557,5565,5571,5576,5581,5586,5594,5607,5615,5627,5632,5640,5646,5651,5671,5680,5689,5698,5703,5714,5726,5731,5736,5744,5749,5754],{"type":27,"tag":28,"props":5468,"children":5469},{},[5470],{"type":32,"value":5471},"Couple of days ago I looked into a tool called Replay and I got a chance to play with some of the new features but also with the tool as a whole. I was really impressed by the level of insight I got thanks to it as a both tester and a developer.",{"type":27,"tag":28,"props":5473,"children":5474},{},[5475],{"type":32,"value":5476},"I think that Replay has the potential to become an inevitable part of development workflow as it can provide a bridge between developers, testers or anyone involved with the product",{"type":27,"tag":28,"props":5478,"children":5479},{},[5480,5482,5488],{"type":32,"value":5481},"If you ever got your bug report returned by a developer with a comment \"can’t replicate\" or debugged a pesky issue with a bunch of ",{"type":27,"tag":653,"props":5483,"children":5485},{"className":5484},[],[5486],{"type":32,"value":5487},"console.log()",{"type":32,"value":5489},"s, then you know how difficult the debugging process can get. Replay aims to make this process easier, and believe it or not - fun.",{"type":27,"tag":45,"props":5491,"children":5493},{"id":5492},"what-is-replay",[5494],{"type":32,"value":5495},"What is Replay?",{"type":27,"tag":28,"props":5497,"children":5498},{},[5499,5501,5507],{"type":32,"value":5500},"In short, ",{"type":27,"tag":172,"props":5502,"children":5505},{"href":5503,"rel":5504},"https://www.replay.io/",[696],[5506],{"type":32,"value":3110},{"type":32,"value":5508}," is a tool that records everything your app does and provides you with debugging superpowers. The recording is a combination of video, source code of your application, DOM snapshots and a timetravel-enabled devtools that allow you to retroactively add print statements to your app’s execution. The best way to explain its capabilities, is by seeing it in action. Replay.io can be used in different ways, so let’s start with the simplest one first.",{"type":27,"tag":45,"props":5510,"children":5512},{"id":5511},"creating-a-recording",[5513],{"type":32,"value":5514},"Creating a recording",{"type":27,"tag":28,"props":5516,"children":5517},{},[5518],{"type":32,"value":5519},"To create a recording, download Replay.io browser, open it and hit the record button. After you do so, you can start interacting with your application as you would with a normal browser. For example, you can replicate a bug that you are experiencing in your app.",{"type":27,"tag":28,"props":5521,"children":5522},{},[5523],{"type":27,"tag":959,"props":5524,"children":5527},{"alt":5525,"src":5526},"Replay.io first screen","replay_io_first_screen_pxuxg1",[],{"type":27,"tag":28,"props":5529,"children":5530},{},[5531],{"type":32,"value":5532},"After you are done with interacting with your application, you will have your recording available. You can rewind or fast forward recording and look at different keystrokes, mouse clicks or other interactions. If you are in a process of replicating a bug, you can add comments to the recording and share it with developers in your team. This is where the real magic begins.",{"type":27,"tag":28,"props":5534,"children":5535},{},[5536],{"type":27,"tag":959,"props":5537,"children":5540},{"alt":5538,"src":5539},"Replay.io viewer screen","viewer_bs89i9",[],{"type":27,"tag":45,"props":5542,"children":5544},{"id":5543},"time-travelling-with-devtools",[5545],{"type":32,"value":5546},"Time travelling with DevTools",{"type":27,"tag":28,"props":5548,"children":5549},{},[5550],{"type":32,"value":5551},"The fact that this recording can be done by anyone can lift a lot of weight off of developer’s shoulders. But creating the recording is just the beginning of the journey. Replay.io provides you with a set of devtools, that look very similar to Chrome or Firefox devtools.",{"type":27,"tag":28,"props":5553,"children":5554},{},[5555],{"type":32,"value":5556},"But these are now attached to your recording. You can retroactively inspect elements, review API calls in the network panel, look at console logs and so much more.",{"type":27,"tag":28,"props":5558,"children":5559},{},[5560],{"type":27,"tag":959,"props":5561,"children":5564},{"alt":5562,"src":5563},"Replay.io devtools","replay-io_devtools_cmyswj",[],{"type":27,"tag":45,"props":5566,"children":5568},{"id":5567},"examining-app-code",[5569],{"type":32,"value":5570},"Examining app code",{"type":27,"tag":28,"props":5572,"children":5573},{},[5574],{"type":32,"value":5575},"At a first glance, this may seem quite similar to Playwright’s trace-viewer or Cypress’ timeline. They both alow you to travel back in time in their own way. But what Replay.io does is actually much more powerful.",{"type":27,"tag":28,"props":5577,"children":5578},{},[5579],{"type":32,"value":5580},"Besides snapshots and network activity you can access full source code of your application. For example, I can further examine the component of my application that is responsible for creating a to-do item in my application.",{"type":27,"tag":28,"props":5582,"children":5583},{},[5584],{"type":32,"value":5585},"In the code viewer, I can click on a \"+\" button to add a print statement. This will now show the name of the component in our console, essentially giving us information on how many times was the function call being made during the interaction that was recorded.",{"type":27,"tag":28,"props":5587,"children":5588},{},[5589],{"type":27,"tag":959,"props":5590,"children":5593},{"alt":5591,"src":5592},"Print statement","print_statement_ijzi3y",[],{"type":27,"tag":28,"props":5595,"children":5596},{},[5597,5599,5605],{"type":32,"value":5598},"But this print statement is actually much more powerful. We can pass a variable from our application into it. That way the information that has been passed through the application becomes visible to us. Notice how we see \"Hello world 👋\" and \"new todo\" in the console, after we use ",{"type":27,"tag":653,"props":5600,"children":5602},{"className":5601},[],[5603],{"type":32,"value":5604},"newTodo",{"type":32,"value":5606}," variable inside the print statement.",{"type":27,"tag":28,"props":5608,"children":5609},{},[5610],{"type":27,"tag":959,"props":5611,"children":5614},{"alt":5612,"src":5613},"Custom print statement","custom_print_statement_jlsxh1",[],{"type":27,"tag":28,"props":5616,"children":5617},{},[5618,5620,5625],{"type":32,"value":5619},"This allows us to follow the flow of the information and debug the application much more effectively. Not only we can see that something happened, but we can find out more about ",{"type":27,"tag":302,"props":5621,"children":5622},{},[5623],{"type":32,"value":5624},"why",{"type":32,"value":5626}," it happened.",{"type":27,"tag":28,"props":5628,"children":5629},{},[5630],{"type":32,"value":5631},"If you are using React with a state manager such as Jotai or Redux, Replay.io allows you to take a look inside each component’s state. The React panel in Replay.io devtools will allow you to view component’s state throughout the whole timeline.",{"type":27,"tag":28,"props":5633,"children":5634},{},[5635],{"type":27,"tag":959,"props":5636,"children":5639},{"alt":5637,"src":5638},"React panel","react_panel_brh2oe",[],{"type":27,"tag":45,"props":5641,"children":5643},{"id":5642},"debugging-your-tests",[5644],{"type":32,"value":5645},"Debugging your tests",{"type":27,"tag":28,"props":5647,"children":5648},{},[5649],{"type":32,"value":5650},"The recorded information is very useful, but it gets even better. As I mentioned in the beginning, Replay.io is actually a browser. Instead of replicating a bug manually, you can use your test run to create these recordings for you. This can be integrated to both Cypress and Playwright.",{"type":27,"tag":28,"props":5652,"children":5653},{},[5654,5656,5662,5664,5670],{"type":32,"value":5655},"The setup is pretty simple. Replay has a Cypress plugin that works exactly like any other plugin. You install it as a package and include it in your ",{"type":27,"tag":653,"props":5657,"children":5659},{"className":5658},[],[5660],{"type":32,"value":5661},"support/e2e.ts",{"type":32,"value":5663}," file and in yout ",{"type":27,"tag":653,"props":5665,"children":5667},{"className":5666},[],[5668],{"type":32,"value":5669},"cypress.config.ts",{"type":32,"value":256},{"type":27,"tag":793,"props":5672,"children":5675},{"className":5673,"code":5674,"language":1084,"meta":5},[1082],"npm i @replayio/cypress\n",[5676],{"type":27,"tag":653,"props":5677,"children":5678},{"__ignoreMap":5},[5679],{"type":32,"value":5674},{"type":27,"tag":793,"props":5681,"children":5684},{"className":5682,"code":5683,"filename":5661,"language":3520,"meta":5},[3517],"require('@replayio/cypress/support');\n",[5685],{"type":27,"tag":653,"props":5686,"children":5687},{"__ignoreMap":5},[5688],{"type":32,"value":5683},{"type":27,"tag":793,"props":5690,"children":5693},{"className":5691,"code":5692,"filename":5669,"language":3520,"meta":5},[3517],"import { defineConfig } from \"cypress\";\nimport replay from \"@replayio/cypress\";\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n      replay(on, config);\n      return config\n    },\n  },\n});\n",[5694],{"type":27,"tag":653,"props":5695,"children":5696},{"__ignoreMap":5},[5697],{"type":32,"value":5692},{"type":27,"tag":28,"props":5699,"children":5700},{},[5701],{"type":32,"value":5702},"After that, Replay.io needs to be set in your CI. I’m using GitHub Actions, but this can be set up in pretty much any CI provider.",{"type":27,"tag":793,"props":5704,"children":5709},{"className":5705,"code":5707,"language":5708,"meta":5},[5706],"language-yml","name: Replay test\non: [push]\njobs:\n  cypress-run:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Cypress run\n        uses: cypress-io/github-action@v5\n        with:\n          browser: replay-chromium\n          start: npm run dev\n      - name: Upload replays\n        if: always()\n        uses: replayio/action-upload@v0.5.0\n        with:\n          api-key: ${{ secrets.RECORD_REPLAY_API_KEY }}\n","yml",[5710],{"type":27,"tag":653,"props":5711,"children":5712},{"__ignoreMap":5},[5713],{"type":32,"value":5707},{"type":27,"tag":28,"props":5715,"children":5716},{},[5717,5719,5725],{"type":32,"value":5718},"The API key can be obtained right from Replay.io browser and needs to be added as a secret to your GitHub project. If you are unfamiliar with how to set up GitHub Actions, I suggest you check out ",{"type":27,"tag":172,"props":5720,"children":5722},{"href":5721},"/cypress-and-git-hub-actions-step-by-step-guide",[5723],{"type":32,"value":5724},"my blog on this",{"type":32,"value":256},{"type":27,"tag":28,"props":5727,"children":5728},{},[5729],{"type":32,"value":5730},"With this setup, you can run your test and you’ll get all the information you would get before. But in addition to that, Replay.io will record information from your test run as well. These recordings are available right inside Replay.io browser. You can treavel through them in a similar way as you would in the Cypress timeline in open mode.",{"type":27,"tag":28,"props":5732,"children":5733},{},[5734],{"type":32,"value":5735},"But remember - since Replay records everything, you can examine not only your test but also your application. If a test becomes flaky for whatever reason, you no longer need to replicate that flakiness locally, but can use Replay.io to get an insight into what is happening inside both your test and your app.",{"type":27,"tag":28,"props":5737,"children":5738},{},[5739],{"type":27,"tag":959,"props":5740,"children":5743},{"alt":5741,"src":5742},"Cypress test replay","cypress_replay_ktuzio",[],{"type":27,"tag":28,"props":5745,"children":5746},{},[5747],{"type":32,"value":5748},"To wrap it up, Replay.io can be a helpful companion for recording bugs and providing that information to your developers. It can help you debug your application by adding print statements traveling backwards or forwards in time and it can help you debug your tests by providing all of the information right from your test run.",{"type":27,"tag":28,"props":5750,"children":5751},{},[5752],{"type":32,"value":5753},"I think it's a tool that can save you a tons of time it will make debugging a smoother faster and quite frankly an enjoyable experience.",{"type":27,"tag":28,"props":5755,"children":5756},{},[5757,5759,5766,5768,5774,5775,5781],{"type":32,"value":5758},"If you are curious about Replay.io, ",{"type":27,"tag":172,"props":5760,"children":5763},{"href":5761,"rel":5762},"https://github.com/filiphric/cypress-flakiness-debug-examples",[696],[5764],{"type":32,"value":5765},"I’ve created a project",{"type":32,"value":5767}," where I’m planning on creating some flakiness examples with links to recordings. I plan to couple them with short YouTube video explanations on what the bug is and how it can be fixed either on test’s side or on applications side. Subscribe to the YouTube channel to never miss a new video. You can also subscribe to my newsletter (form is at the bottom of this page) or follow me on ",{"type":27,"tag":172,"props":5769,"children":5772},{"href":5770,"rel":5771},"https://twitter.com/filip_hric/",[696],[5773],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":5776,"children":5779},{"href":5777,"rel":5778},"http://www.linkedin.com/in/filip-hric",[696],[5780],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":5783},[5784,5785,5786,5787,5788],{"id":5492,"depth":320,"text":5495},{"id":5511,"depth":320,"text":5514},{"id":5543,"depth":320,"text":5546},{"id":5567,"depth":320,"text":5570},{"id":5642,"depth":320,"text":5645},"content:time-travelling-with-replayio:index.md","time-travelling-with-replayio/index.md","time-travelling-with-replayio/index",{"_path":5793,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":5794,"description":5795,"date":5796,"published":10,"slug":5797,"tags":5798,"image":5800,"cypressVersion":17,"readingTime":5801,"body":5805,"_type":329,"_id":5945,"_source":331,"_file":5946,"_stem":5947,"_extension":334},"/cypress-panic-if-there-even-is-one","Cypress panic (if there even is one)","A reddit post was making rounds on the internet claiming that Cypress.io is dying and you should migrate your projects. I believe this post got way more attention than it deserved. In fact, I believe it deserved no attention whatsoever because by amplifying it, the lies that it contained spread far and wide. ","2023-07-25 12:52:28","cypress-panic-if-there-even-is-one",[5799,5279],"opinion","panic_w4sstc.png",{"text":585,"minutes":5802,"time":5803,"words":5804},3.065,183900,613,{"type":24,"children":5806,"toc":5940},[5807,5812,5818,5823,5846,5860,5866,5876,5881,5903,5909,5923,5928],{"type":27,"tag":28,"props":5808,"children":5809},{},[5810],{"type":32,"value":5811},"Last week a reddit post was making rounds on the internet claiming that Cypress.io is dying and you should migrate your projects. I believe this post got way more attention than it deserved. In fact, I believe it deserved no attention whatsoever because by amplifying it, the lies that it contained spread far and wide. And as all lies do, it left people confused and uncertain. I got people reach out to me asking me whether they should migrate their tests and asked about the future of Cypress. Since it’s already out there and getting attention, I guess there’s no way for me to avoid talking about it. I wrote this post so I don’t have to respond to everyone individually.",{"type":27,"tag":45,"props":5813,"children":5815},{"id":5814},"cypress-updates",[5816],{"type":32,"value":5817},"Cypress updates",{"type":27,"tag":28,"props":5819,"children":5820},{},[5821],{"type":32,"value":5822},"We received the last major update of Cypress back in December 2022, which created a feeling that after a strong year, Cypress development has stalled. Most of the feature updates done since v12 were improvements on Component testing and features interconnecting Cypress App with the Cypress Cloud service.",{"type":27,"tag":28,"props":5824,"children":5825},{},[5826,5828,5835,5837,5844],{"type":32,"value":5827},"By looking at these updates a clue is given to what the company is now focused on. Growing the dev community around browser-based component testing and catering to enterprise customers by ",{"type":27,"tag":172,"props":5829,"children":5832},{"href":5830,"rel":5831},"https://twitter.com/_jessicasachs/status/1681309400877912064",[696],[5833],{"type":32,"value":5834},"making improvements to the Cypress Cloud (dashboard) service",{"type":32,"value":5836},". Funnily enough, I’ve been talking about how I wanted to see more improvements in Cypress Cloud ",{"type":27,"tag":172,"props":5838,"children":5841},{"href":5839,"rel":5840},"https://www.youtube.com/watch?v=xstUjKOL4zY",[696],[5842],{"type":32,"value":5843},"earlier this year",{"type":32,"value":5845},". I guess Cypress watches my YouTube channel 😂",{"type":27,"tag":28,"props":5847,"children":5848},{},[5849,5851,5858],{"type":32,"value":5850},"Jokes aside, it really seems that Cypress is working on making their customers happy and creating a sustainable business. From what I can tell, they are being successful and the company itself is doing fine. Cypress v13 is just around the corner with some kick-ass debugging capabilities. On the community side, Cypress has announced CypressConf, where people all around the world will share their ideas and knowledge around Cypress (",{"type":27,"tag":172,"props":5852,"children":5855},{"href":5853,"rel":5854},"https://bit.ly/cypressconfCFP",[696],[5856],{"type":32,"value":5857},"CFP is here",{"type":32,"value":5859},")",{"type":27,"tag":45,"props":5861,"children":5863},{"id":5862},"internet-drama",[5864],{"type":32,"value":5865},"Internet drama",{"type":27,"tag":28,"props":5867,"children":5868},{},[5869,5871],{"type":32,"value":5870},"Jordan Powell from Cypress tweeted this on the day this Cypress panic (if there even is one) started:\n",{"type":27,"tag":5872,"props":5873,"children":5875},"tweet",{"id":5874},"1681362036494565376",[],{"type":27,"tag":28,"props":5877,"children":5878},{},[5879],{"type":32,"value":5880},"I have to agree with him on this one. Everything that has been said on that day was based on assumptions, mostly by people that have not been talking to the Cypress team nor had insight into the company during last couple of years. Let alone their financial state.",{"type":27,"tag":28,"props":5882,"children":5883},{},[5884,5886,5893,5895,5902],{"type":32,"value":5885},"Many people have ",{"type":27,"tag":172,"props":5887,"children":5890},{"href":5888,"rel":5889},"https://twitter.com/bahmutov/status/1681271097344380928/retweets/with_comments",[696],[5891],{"type":32,"value":5892},"used this as validation",{"type":32,"value":5894}," of their concerns or an additional argument to their critique of Cypress as a tool. Even though the original critique is about the company, not the tool itself. But nuances have a hard time surviving online. Suddenly, opinions shifted into statements, statements turned into facts, facts turned into “the truth”. Lo and behold, the power of social media. That’s what happens ",{"type":27,"tag":172,"props":5896,"children":5899},{"href":5897,"rel":5898},"https://twitter.com/Lachlan19900/status/1681448915839905793",[696],[5900],{"type":32,"value":5901},"when trolls get amplified",{"type":32,"value":256},{"type":27,"tag":45,"props":5904,"children":5906},{"id":5905},"cypress-and-its-future",[5907],{"type":32,"value":5908},"Cypress and its future",{"type":27,"tag":28,"props":5910,"children":5911},{},[5912,5914,5921],{"type":32,"value":5913},"I am still confident about Cypress, both the company and the open source software. I don’t think they are perfect, and would really like to see some improvements being made in both Cypress App and Cypress Cloud. I do think Cypress has a very fierce competition in e2e testing with Playwright. I think it would be quite interesting to ",{"type":27,"tag":172,"props":5915,"children":5918},{"href":5916,"rel":5917},"https://twitter.com/_jessicasachs/status/1681309404153675780",[696],[5919],{"type":32,"value":5920},"merge these tools together",{"type":32,"value":5922},". If that does not come into fruition, Cypress will still be a really cool solution for component testing.",{"type":27,"tag":28,"props":5924,"children":5925},{},[5926],{"type":32,"value":5927},"So what should you do? Migrate all your tests? Please don’t do that. Not based on a reddit post. At least not based on that one.",{"type":27,"tag":5872,"props":5929,"children":5931},{"id":5930},"1681373989287886856",[5932],{"type":27,"tag":1029,"props":5933,"children":5934},{},[5935],{"type":27,"tag":28,"props":5936,"children":5937},{},[5938],{"type":32,"value":5939},"One more thing that I feel like needs to be said. Cypress ambassadors are not paid by Cypress. I never received a single dollar from Cypress. I have used Cypress for test automation since version 0.20.0 and I believe it’s genuinely a great tool. I have not been asked to write this blogpost, nor has anyone seen this text before I published it.",{"title":5,"searchDepth":320,"depth":320,"links":5941},[5942,5943,5944],{"id":5814,"depth":320,"text":5817},{"id":5862,"depth":320,"text":5865},{"id":5905,"depth":320,"text":5908},"content:cypress-panic-if-there-even-is-one:index.md","cypress-panic-if-there-even-is-one/index.md","cypress-panic-if-there-even-is-one/index",{"_path":5949,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":5950,"description":5951,"date":5952,"published":10,"slug":5953,"tags":5954,"image":5958,"cypressVersion":5959,"readingTime":5960,"body":5964,"_type":329,"_id":6472,"_source":331,"_file":6473,"_stem":6474,"_extension":334},"/cypress-basics-using-baseurl","Cypress basics: Using baseUrl","Setting up `baseUrl` helps you write your test in a way that enables running them against multiple environments. This is vital to make your tests available for multiple versions of your app.","2023-04-05","cypress-basics-using-baseurl",[5279,5955,5956,5957],"baseurl","visit","config","baseurl_pbr8ce.png","v10.0.0",{"text":585,"minutes":5961,"time":5962,"words":5963},3.98,238800,796,{"type":24,"children":5965,"toc":6457},[5966,5978,6005,6011,6031,6040,6068,6081,6122,6131,6155,6168,6187,6196,6207,6219,6230,6239,6252,6271,6282,6287,6300,6320,6329,6365,6377,6386,6398,6407,6418,6430,6440,6445],{"type":27,"tag":1029,"props":5967,"children":5968},{},[5969,5974],{"type":27,"tag":28,"props":5970,"children":5971},{},[5972],{"type":32,"value":5973},"This article is a part of series on Cypress basics. You can check out some other articles on my blog where I provide step by step explanations of some Cypress basics + some extra tips on how you can take things one step further. So far, I wrote about:",{"type":27,"tag":5975,"props":5976,"children":5977},"basics-toc",{},[],{"type":27,"tag":28,"props":5979,"children":5980},{},[5981,5983,5988,5990,5996,5998,6003],{"type":32,"value":5982},"Cypress is built for testing ",{"type":27,"tag":302,"props":5984,"children":5985},{},[5986],{"type":32,"value":5987},"your",{"type":32,"value":5989}," application. In other words, it was designed to be able to test an application that you have access to, and are actively developing. For this reason, Cypress comes with a ",{"type":27,"tag":653,"props":5991,"children":5993},{"className":5992},[],[5994],{"type":32,"value":5995},"baseUrl",{"type":32,"value":5997}," parameter which can help you set up the starting point of your testing efforts. In this blogpost we will take a look into what ",{"type":27,"tag":653,"props":5999,"children":6001},{"className":6000},[],[6002],{"type":32,"value":5995},{"type":32,"value":6004}," is and how you can use it.",{"type":27,"tag":45,"props":6006,"children":6008},{"id":6007},"setting-up-the-baseurl-in-cypress-configuration",[6009],{"type":32,"value":6010},"Setting up the baseUrl in Cypress Configuration",{"type":27,"tag":28,"props":6012,"children":6013},{},[6014,6016,6021,6023,6029],{"type":32,"value":6015},"The first step in using the ",{"type":27,"tag":653,"props":6017,"children":6019},{"className":6018},[],[6020],{"type":32,"value":5995},{"type":32,"value":6022}," in Cypress is to configure it in your ",{"type":27,"tag":653,"props":6024,"children":6026},{"className":6025},[],[6027],{"type":32,"value":6028},"cypress.config.js",{"type":32,"value":6030}," file. This file is located at the root of your Cypress project and contains various configuration options for your tests. The configuration will look something like this:",{"type":27,"tag":793,"props":6032,"children":6035},{"className":6033,"code":6034,"filename":6028,"language":1513,"meta":5},[1510],"const { defineConfig } = require('cypress')\n\nmodule.exports = defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n    },\n    baseUrl: 'https://example.com'\n  },\n})\n",[6036],{"type":27,"tag":653,"props":6037,"children":6038},{"__ignoreMap":5},[6039],{"type":32,"value":6034},{"type":27,"tag":28,"props":6041,"children":6042},{},[6043,6045,6051,6053,6059,6061,6066],{"type":32,"value":6044},"You should replace ",{"type":27,"tag":653,"props":6046,"children":6048},{"className":6047},[],[6049],{"type":32,"value":6050},"https://example.com",{"type":32,"value":6052}," with the actual URL of your web application. This can be a ",{"type":27,"tag":653,"props":6054,"children":6056},{"className":6055},[],[6057],{"type":32,"value":6058},"localhost",{"type":32,"value":6060}," address if you are testing locally, staging website or a production site. This configuration makes the specified URL available as the ",{"type":27,"tag":653,"props":6062,"children":6064},{"className":6063},[],[6065],{"type":32,"value":5995},{"type":32,"value":6067}," throughout your test suite.",{"type":27,"tag":45,"props":6069,"children":6071},{"id":6070},"accessing-the-baseurl-in-tests",[6072,6074,6079],{"type":32,"value":6073},"Accessing the ",{"type":27,"tag":653,"props":6075,"children":6077},{"className":6076},[],[6078],{"type":32,"value":5995},{"type":32,"value":6080}," in tests",{"type":27,"tag":28,"props":6082,"children":6083},{},[6084,6086,6091,6093,6099,6100,6106,6107,6113,6115,6120],{"type":32,"value":6085},"Once you have set up the ",{"type":27,"tag":653,"props":6087,"children":6089},{"className":6088},[],[6090],{"type":32,"value":5995},{"type":32,"value":6092}," in your configuration, you can access it different ways. For example, your ",{"type":27,"tag":653,"props":6094,"children":6096},{"className":6095},[],[6097],{"type":32,"value":6098},"cy.visit()",{"type":32,"value":3372},{"type":27,"tag":653,"props":6101,"children":6103},{"className":6102},[],[6104],{"type":32,"value":6105},"cy.request()",{"type":32,"value":4164},{"type":27,"tag":653,"props":6108,"children":6110},{"className":6109},[],[6111],{"type":32,"value":6112},"cy.intercept()",{"type":32,"value":6114}," commands will use this ",{"type":27,"tag":653,"props":6116,"children":6118},{"className":6117},[],[6119],{"type":32,"value":5995},{"type":32,"value":6121},", so instead of typing out the whole url, you only use the path.",{"type":27,"tag":793,"props":6123,"children":6126},{"className":6124,"code":6125,"language":1513,"meta":5},[1510],"cy.visit('/home') \n// resolves to https://example.com/home\n\ncy.request('/api/items') \n// resolves to https://example.com/api/items\n\ncy.intercept('/api/logout') \n// resolves to https://example.com/api/logout\n",[6127],{"type":27,"tag":653,"props":6128,"children":6129},{"__ignoreMap":5},[6130],{"type":32,"value":6125},{"type":27,"tag":1029,"props":6132,"children":6133},{},[6134],{"type":27,"tag":28,"props":6135,"children":6136},{},[6137,6139,6145,6147,6153],{"type":32,"value":6138},"💡 Did you know, that instead of visiting a URL, you can open a plain html file? Just use ",{"type":27,"tag":653,"props":6140,"children":6142},{"className":6141},[],[6143],{"type":32,"value":6144},"cy.visit('index.html')",{"type":32,"value":6146}," to open ",{"type":27,"tag":653,"props":6148,"children":6150},{"className":6149},[],[6151],{"type":32,"value":6152},"index.html",{"type":32,"value":6154}," file in your root folder of your project.",{"type":27,"tag":45,"props":6156,"children":6158},{"id":6157},"changing-the-baseurl-during-test-execution",[6159,6161,6166],{"type":32,"value":6160},"Changing the ",{"type":27,"tag":653,"props":6162,"children":6164},{"className":6163},[],[6165],{"type":32,"value":5995},{"type":32,"value":6167}," During Test Execution",{"type":27,"tag":28,"props":6169,"children":6170},{},[6171,6173,6178,6180,6185],{"type":32,"value":6172},"In some cases, you may need to change the ",{"type":27,"tag":653,"props":6174,"children":6176},{"className":6175},[],[6177],{"type":32,"value":5995},{"type":32,"value":6179}," at a test level. This might be the case for some smoke tests that are intended to cover a larger system, consisting of applications split into multiple domains. To set up a different ",{"type":27,"tag":653,"props":6181,"children":6183},{"className":6182},[],[6184],{"type":32,"value":5995},{"type":32,"value":6186}," on a test, you can use test configuration object:",{"type":27,"tag":793,"props":6188,"children":6191},{"className":6189,"code":6190,"language":1513,"meta":5},[1510],"describe('smoke tests', () => {\n  it('test default domain', () => {\n    cy.visit('/home')\n    // will go to https://example.com/home\n  })\n\n  it('test some other domain', { baseUrl: 'https://other.com '}, () => {\n    cy.visit('/home')\n    // will go to https://other.com/home\n  })\n})\n",[6192],{"type":27,"tag":653,"props":6193,"children":6194},{"__ignoreMap":5},[6195],{"type":32,"value":6190},{"type":27,"tag":45,"props":6197,"children":6199},{"id":6198},"making-assertions-with-baseurl",[6200,6202],{"type":32,"value":6201},"Making Assertions with ",{"type":27,"tag":653,"props":6203,"children":6205},{"className":6204},[],[6206],{"type":32,"value":5995},{"type":27,"tag":28,"props":6208,"children":6209},{},[6210,6212,6217],{"type":32,"value":6211},"In your tests, you may want to make assertions that involve the ",{"type":27,"tag":653,"props":6213,"children":6215},{"className":6214},[],[6216],{"type":32,"value":5995},{"type":32,"value":6218},". For example, you may want to ensure that your application redirects to the correct page after a certain action.",{"type":27,"tag":28,"props":6220,"children":6221},{},[6222,6224,6229],{"type":32,"value":6223},"Here's an example of how to make assertions with the ",{"type":27,"tag":653,"props":6225,"children":6227},{"className":6226},[],[6228],{"type":32,"value":5995},{"type":32,"value":1474},{"type":27,"tag":793,"props":6231,"children":6234},{"className":6232,"code":6233,"language":1513,"meta":5},[1510],"describe('Redirect Test', () => {\n  it('Redirects to the login page after signing out', () => {\n    const baseUrl = Cypress.config('baseUrl');\n    // Perform the sign-out action\n    cy.get('#sign-out-button').click(); \n    // Assert that the user is redirected to the login page\n    cy.url().should('eq', `${baseUrl}/login`);\n  });\n});\n",[6235],{"type":27,"tag":653,"props":6236,"children":6237},{"__ignoreMap":5},[6238],{"type":32,"value":6233},{"type":27,"tag":45,"props":6240,"children":6242},{"id":6241},"passing-baseurl-through-cli",[6243,6245,6250],{"type":32,"value":6244},"Passing ",{"type":27,"tag":653,"props":6246,"children":6248},{"className":6247},[],[6249],{"type":32,"value":5995},{"type":32,"value":6251}," through CLI",{"type":27,"tag":28,"props":6253,"children":6254},{},[6255,6257,6262,6264,6269],{"type":32,"value":6256},"You actually don’t need to set up your ",{"type":27,"tag":653,"props":6258,"children":6260},{"className":6259},[],[6261],{"type":32,"value":5995},{"type":32,"value":6263}," in the ",{"type":27,"tag":653,"props":6265,"children":6267},{"className":6266},[],[6268],{"type":32,"value":6028},{"type":32,"value":6270}," file at all. Instead, it is possible to resolve it when opening Cypress:",{"type":27,"tag":793,"props":6272,"children":6277},{"className":6273,"code":6275,"language":6276,"meta":5},[6274],"language-sh","npx cypress open --config baseUrl=https://staging.example.com\n","sh",[6278],{"type":27,"tag":653,"props":6279,"children":6280},{"__ignoreMap":5},[6281],{"type":32,"value":6275},{"type":27,"tag":28,"props":6283,"children":6284},{},[6285],{"type":32,"value":6286},"This way you can easily switch between different environments and open Cypress against the one you want to test in",{"type":27,"tag":45,"props":6288,"children":6290},{"id":6289},"resolving-baseurl-dynamically",[6291,6293,6298],{"type":32,"value":6292},"Resolving ",{"type":27,"tag":653,"props":6294,"children":6296},{"className":6295},[],[6297],{"type":32,"value":5995},{"type":32,"value":6299}," dynamically",{"type":27,"tag":28,"props":6301,"children":6302},{},[6303,6305,6310,6312,6318],{"type":32,"value":6304},"You can take things one level further and resolve your ",{"type":27,"tag":653,"props":6306,"children":6308},{"className":6307},[],[6309],{"type":32,"value":5995},{"type":32,"value":6311}," dynamically. You can either do this on the parameter itself, or by using ",{"type":27,"tag":653,"props":6313,"children":6315},{"className":6314},[],[6316],{"type":32,"value":6317},"setupNodeEvents()",{"type":32,"value":6319}," function. Let’s take a look at the examples.",{"type":27,"tag":793,"props":6321,"children":6324},{"className":6322,"code":6323,"filename":6028,"language":1513,"meta":5},[1510],"const { defineConfig } = require('cypress')\n\nmodule.exports = defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n    },\n    baseUrl: process.env.CI ? \"https://staging.example.com\" : \"http://localhost:3000\"\n  },\n})\n",[6325],{"type":27,"tag":653,"props":6326,"children":6327},{"__ignoreMap":5},[6328],{"type":32,"value":6323},{"type":27,"tag":28,"props":6330,"children":6331},{},[6332,6334,6340,6342,6348,6350,6355,6357,6363],{"type":32,"value":6333},"In this example, we’ll set up the location ",{"type":27,"tag":653,"props":6335,"children":6337},{"className":6336},[],[6338],{"type":32,"value":6339},"https://staging.example.com",{"type":32,"value":6341}," whenever we run tests on a CI pipeline. Most of CI sercives set up a ",{"type":27,"tag":653,"props":6343,"children":6345},{"className":6344},[],[6346],{"type":32,"value":6347},"CI=1",{"type":32,"value":6349}," variable, which we can use for making the decision. We are adding a condition that will set up ",{"type":27,"tag":653,"props":6351,"children":6353},{"className":6352},[],[6354],{"type":32,"value":6339},{"type":32,"value":6356}," if we are in a CI environment and ",{"type":27,"tag":653,"props":6358,"children":6360},{"className":6359},[],[6361],{"type":32,"value":6362},"http://localhost:3000",{"type":32,"value":6364}," if we are not.",{"type":27,"tag":28,"props":6366,"children":6367},{},[6368,6370,6375],{"type":32,"value":6369},"If we need to switch between multiple URLs, we can use ",{"type":27,"tag":653,"props":6371,"children":6373},{"className":6372},[],[6374],{"type":32,"value":6317},{"type":32,"value":6376}," function:",{"type":27,"tag":793,"props":6378,"children":6381},{"className":6379,"code":6380,"filename":6028,"language":1513,"meta":5},[1510],"const { defineConfig } = require('cypress')\n\nmodule.exports = defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n\n      const version = config.env.VERSION || 'local'\n\n      const urls = {\n        local: \"http://localhost:3000\",\n        staging: \"https://staging.example.com\",\n        prod: \"https://example.com\"\n      }\n\n      // choosing version from urls object\n      config.baseUrl = urls[version]\n\n      return config\n    },\n  },\n})\n",[6382],{"type":27,"tag":653,"props":6383,"children":6384},{"__ignoreMap":5},[6385],{"type":32,"value":6380},{"type":27,"tag":28,"props":6387,"children":6388},{},[6389,6391,6397],{"type":32,"value":6390},"We can now easily switch between different URLs by passing the name of the application version to the ",{"type":27,"tag":653,"props":6392,"children":6394},{"className":6393},[],[6395],{"type":32,"value":6396},"--env",{"type":32,"value":1545},{"type":27,"tag":793,"props":6399,"children":6402},{"className":6400,"code":6401,"language":1084,"meta":5},[1082],"# will use http://localhost:3000\nnpx cypress open --env version=\"local\"\n# will use http://staging.example.com\nnpx cypress open --env version=\"staging\"\n# will use http://example.com\nnpx cypress open --env version=\"prod\"\n# will use fallback to http://localhost:3000\nnpx cypress open \n",[6403],{"type":27,"tag":653,"props":6404,"children":6405},{"__ignoreMap":5},[6406],{"type":32,"value":6401},{"type":27,"tag":45,"props":6408,"children":6410},{"id":6409},"best-practices-using-baseurl",[6411,6413],{"type":32,"value":6412},"Best practices using ",{"type":27,"tag":653,"props":6414,"children":6416},{"className":6415},[],[6417],{"type":32,"value":5995},{"type":27,"tag":28,"props":6419,"children":6420},{},[6421,6423,6428],{"type":32,"value":6422},"A common error that I see people doing is using the ",{"type":27,"tag":653,"props":6424,"children":6426},{"className":6425},[],[6427],{"type":32,"value":5995},{"type":32,"value":6429}," like this:",{"type":27,"tag":793,"props":6431,"children":6435},{"className":6432,"code":6433,"filename":6434,"language":1513,"meta":5},[1510],"cy.visit(Cypress.config('baseUrl') + '/home')\n","❌ don’t use",[6436],{"type":27,"tag":653,"props":6437,"children":6438},{"__ignoreMap":5},[6439],{"type":32,"value":6433},{"type":27,"tag":28,"props":6441,"children":6442},{},[6443],{"type":32,"value":6444},"As shown in the example at the beginning of this post, this serves no purpose and adds redundancy to your tests.",{"type":27,"tag":28,"props":6446,"children":6447},{},[6448,6450,6455],{"type":32,"value":6449},"Setting up ",{"type":27,"tag":653,"props":6451,"children":6453},{"className":6452},[],[6454],{"type":32,"value":5995},{"type":32,"value":6456}," helps you write your test in a way that enables running them agains any environment. This is vital to make your tests flexible.",{"title":5,"searchDepth":320,"depth":320,"links":6458},[6459,6460,6462,6464,6466,6468,6470],{"id":6007,"depth":320,"text":6010},{"id":6070,"depth":320,"text":6461},"Accessing the baseUrl in tests",{"id":6157,"depth":320,"text":6463},"Changing the baseUrl During Test Execution",{"id":6198,"depth":320,"text":6465},"Making Assertions with baseUrl",{"id":6241,"depth":320,"text":6467},"Passing baseUrl through CLI",{"id":6289,"depth":320,"text":6469},"Resolving baseUrl dynamically",{"id":6409,"depth":320,"text":6471},"Best practices using baseUrl","content:cypress-basics-using-baseurl:index.md","cypress-basics-using-baseurl/index.md","cypress-basics-using-baseurl/index",{"_path":6476,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":6477,"description":6478,"date":6479,"published":10,"slug":6480,"tags":6481,"cypressVersion":5959,"readingTime":6485,"body":6490,"_type":329,"_id":7714,"_source":331,"_file":7715,"_stem":7716,"_extension":334},"/cucumber-in-cypress-a-step-by-step-guide","Cucumber in Cypress: A step by step guide","A detailed guide on how to set up cucumber-preprocessor, run your feature files, organize your tests, filter them by tags and set up an HTML reporter.","2023-03-21","cucumber-in-cypress-a-step-by-step-guide",[6482,6483,5279,6484],"cucumber","gherkin","tags",{"text":6486,"minutes":6487,"time":6488,"words":6489},"13 min read",12.585,755100,2517,{"type":24,"children":6491,"toc":7700},[6492,6512,6517,6522,6528,6533,6538,6543,6548,6562,6567,6573,6587,6596,6619,6628,6633,6642,6647,6659,6694,6728,6740,6753,6784,6789,6795,6808,6818,6838,6848,6883,6907,6913,6918,6927,6940,6960,6969,6975,6988,7008,7017,7051,7057,7062,7071,7076,7086,7106,7112,7153,7163,7205,7225,7266,7278,7287,7293,7334,7356,7366,7372,7377,7382,7392,7397,7406,7419,7427,7454,7467,7476,7481,7490,7495,7504,7539,7553,7559,7572,7581,7593,7602,7610,7616,7621,7626,7635,7640,7648,7670,7676,7681],{"type":27,"tag":28,"props":6493,"children":6494},{},[6495,6497,6502,6504,6510],{"type":32,"value":6496},"One of the most common questions I get on webinars and livestreams is: ",{"type":27,"tag":302,"props":6498,"children":6499},{},[6500],{"type":32,"value":6501},"How do I use \"X\" in Cucumber?",{"type":32,"value":6503},". Whether it is API testing, ",{"type":27,"tag":653,"props":6505,"children":6507},{"className":6506},[],[6508],{"type":32,"value":6509},"cy.session()",{"type":32,"value":6511}," or other functionality, Cucumber seems to be a requirement in many teams.",{"type":27,"tag":28,"props":6513,"children":6514},{},[6515],{"type":32,"value":6516},"Main advantage of using Cucumber is the ability to use Gherkin syntax for test definitions. All tests are written as behavior scenarios and therefore test not only fulfill the role of veryfing the functionality, but also serve as a living documentation. The goal of such approach is to provide more visibility into what’s being tested. The benefit is that besides engineering, another company stakeholders can review whether acceptibility criteria is being met.",{"type":27,"tag":28,"props":6518,"children":6519},{},[6520],{"type":32,"value":6521},"I’ve seen this approach work well in medical and banking sector, where tests were not only functional, but were further used to generate documentation or provide a high level report.",{"type":27,"tag":45,"props":6523,"children":6525},{"id":6524},"my-thoughts-on-using-cucumber",[6526],{"type":32,"value":6527},"My thoughts on using Cucumber",{"type":27,"tag":28,"props":6529,"children":6530},{},[6531],{"type":32,"value":6532},"I have been known for criticising the Gherkin syntax approach in the past. My main objection to this is that it uses \"black-box\" approach to testing. I find this approach to not be very effective - especially with Cypress.",{"type":27,"tag":28,"props":6534,"children":6535},{},[6536],{"type":32,"value":6537},"Cypress tests are executing inside browser, giving you the ability to enter application’s internals, accessing API, caching sessions, changing application’s state, mocking network. Well designed Cypress tests can help you achieve a decent coverage with a small amount of tests.",{"type":27,"tag":28,"props":6539,"children":6540},{},[6541],{"type":32,"value":6542},"Using a black-box approach throws away all the powers of Cypress and uses it simply as a test automation tool.",{"type":27,"tag":28,"props":6544,"children":6545},{},[6546],{"type":32,"value":6547},"There’s also a question of maintenance and readability. Cypress commands are readable out of the box, and with some good practices applied, you can keep your tests lean and easy to maintain even with applications that change their behavior on a regular basis.",{"type":27,"tag":28,"props":6549,"children":6550},{},[6551,6553,6560],{"type":32,"value":6552},"Cucumber uses step-based definitions that encapsule each series of commands into its own file. While they can also have a good readability, any change introduced to the application might require multiple steps to be redefined or added. This means that the bigger the system the harder it might be to introduce new changes. I’ve had a ",{"type":27,"tag":172,"props":6554,"children":6557},{"href":6555,"rel":6556},"https://www.youtube.com/watch?v=Fd_1GHSHWHo",[696],[6558],{"type":32,"value":6559},"great explanation provided by Gleb Bahmutov",{"type":32,"value":6561}," on how introducing a change into Cucumber-based test might be challenging.",{"type":27,"tag":28,"props":6563,"children":6564},{},[6565],{"type":32,"value":6566},"That all said, I wanted to create this tutorial so that you can effectively set up Cypress with Cucumber in case this is a requirement in your company. I still believe you can be successful even with this abstraction model, so let’s dive into it.",{"type":27,"tag":45,"props":6568,"children":6570},{"id":6569},"installation",[6571],{"type":32,"value":6572},"Installation",{"type":27,"tag":28,"props":6574,"children":6575},{},[6576,6578,6585],{"type":32,"value":6577},"To start off, you need to install the ",{"type":27,"tag":172,"props":6579,"children":6582},{"href":6580,"rel":6581},"https://www.npmjs.com/package/@badeball/cypress-cucumber-preprocessor",[696],[6583],{"type":32,"value":6584},"cypress-cucumber-preprocessor plugin",{"type":32,"value":6586},". There are currently multiple different versions flowing, but I believe this one is the best and it is actively maintained. You can install it by running the following command:",{"type":27,"tag":793,"props":6588,"children":6591},{"className":6589,"code":6590,"language":3589,"meta":5},[3587],"npm i @badeball/cypress-cucumber-preprocessor\n",[6592],{"type":27,"tag":653,"props":6593,"children":6594},{"__ignoreMap":5},[6595],{"type":32,"value":6590},{"type":27,"tag":28,"props":6597,"children":6598},{},[6599,6601,6608,6610,6617],{"type":32,"value":6600},"Besides installation of the preprocessor, ",{"type":27,"tag":172,"props":6602,"children":6605},{"href":6603,"rel":6604},"https://github.com/badeball/cypress-cucumber-preprocessor/blob/master/docs/quick-start.md",[696],[6606],{"type":32,"value":6607},"plugin docs",{"type":32,"value":6609}," recommend installing the ",{"type":27,"tag":172,"props":6611,"children":6614},{"href":6612,"rel":6613},"https://www.npmjs.com/package/@bahmutov/cypress-esbuild-preprocessor",[696],[6615],{"type":32,"value":6616},"esbuild bundler by Gleb Bahmutov",{"type":32,"value":6618},", which will make your run much faster.",{"type":27,"tag":793,"props":6620,"children":6623},{"className":6621,"code":6622,"language":3589,"meta":5},[3587],"npm i @bahmutov/cypress-esbuild-preprocessor\n",[6624],{"type":27,"tag":653,"props":6625,"children":6626},{"__ignoreMap":5},[6627],{"type":32,"value":6622},{"type":27,"tag":28,"props":6629,"children":6630},{},[6631],{"type":32,"value":6632},"After installation of these packages, you need to configure Cypress to use the plugins. The final configuration will look something like this:",{"type":27,"tag":793,"props":6634,"children":6637},{"className":6635,"code":6636,"filename":5669,"language":3520,"meta":5},[3517],"import { defineConfig } from \"cypress\";\nimport createBundler from \"@bahmutov/cypress-esbuild-preprocessor\";\nimport { addCucumberPreprocessorPlugin } from \"@badeball/cypress-cucumber-preprocessor\";\nimport createEsbuildPlugin from \"@badeball/cypress-cucumber-preprocessor/esbuild\";\n\nexport default defineConfig({\n  e2e: {\n    specPattern: \"**/*.feature\",\n    async setupNodeEvents(\n      on: Cypress.PluginEvents,\n      config: Cypress.PluginConfigOptions\n    ): Promise\u003CCypress.PluginConfigOptions> {\n      await addCucumberPreprocessorPlugin(on, config);\n      on(\n        \"file:preprocessor\",\n        createBundler({\n          plugins: [createEsbuildPlugin(config)],\n        })\n      );\n      return config;\n    },\n  },\n});\n",[6638],{"type":27,"tag":653,"props":6639,"children":6640},{"__ignoreMap":5},[6641],{"type":32,"value":6636},{"type":27,"tag":28,"props":6643,"children":6644},{},[6645],{"type":32,"value":6646},"There’s a lot to unpack here, so let’s go step by step.",{"type":27,"tag":28,"props":6648,"children":6649},{},[6650,6652,6657],{"type":32,"value":6651},"The configuration file is written in TypeScript. A Javascript file might be a tiny bit simpler, but essentially contains all the same parts. We are importing different packages and adding them into our ",{"type":27,"tag":653,"props":6653,"children":6655},{"className":6654},[],[6656],{"type":32,"value":6317},{"type":32,"value":6658}," function.",{"type":27,"tag":28,"props":6660,"children":6661},{},[6662,6663,6669,6671,6677,6679,6685,6687,6692],{"type":32,"value":3349},{"type":27,"tag":653,"props":6664,"children":6666},{"className":6665},[],[6667],{"type":32,"value":6668},"specPattern",{"type":32,"value":6670}," attribute tells Cypress that we want to be looking for ",{"type":27,"tag":653,"props":6672,"children":6674},{"className":6673},[],[6675],{"type":32,"value":6676},".feature",{"type":32,"value":6678}," files in our ",{"type":27,"tag":653,"props":6680,"children":6682},{"className":6681},[],[6683],{"type":32,"value":6684},"e2e",{"type":32,"value":6686}," folder. This means that it will ignore all other formats and only use ",{"type":27,"tag":653,"props":6688,"children":6690},{"className":6689},[],[6691],{"type":32,"value":6676},{"type":32,"value":6693}," files as our test.",{"type":27,"tag":28,"props":6695,"children":6696},{},[6697,6703,6705,6710,6712,6718,6720,6726],{"type":27,"tag":653,"props":6698,"children":6700},{"className":6699},[],[6701],{"type":32,"value":6702},"addCucumberPreprocessorPlugin()",{"type":32,"value":6704}," function takes care of digesting these ",{"type":27,"tag":653,"props":6706,"children":6708},{"className":6707},[],[6709],{"type":32,"value":6676},{"type":32,"value":6711}," files and convert them into Javascript. Since Cypress runs in browser, we need to make sure that everything we run (whether it is ",{"type":27,"tag":653,"props":6713,"children":6715},{"className":6714},[],[6716],{"type":32,"value":6717},".ts",{"type":32,"value":6719}," files ",{"type":27,"tag":653,"props":6721,"children":6723},{"className":6722},[],[6724],{"type":32,"value":6725},".jsx",{"type":32,"value":6727}," or other formats) will eventually get compiled into plain Javascript. This is what preprocessors do.",{"type":27,"tag":28,"props":6729,"children":6730},{},[6731,6732,6738],{"type":32,"value":3349},{"type":27,"tag":653,"props":6733,"children":6735},{"className":6734},[],[6736],{"type":32,"value":6737},"on(\"file:preprocessor\")",{"type":32,"value":6739}," part takes care of combining the esbuild plugin with the cucumber plugin so they play nicely together.",{"type":27,"tag":28,"props":6741,"children":6742},{},[6743,6745,6751],{"type":32,"value":6744},"The final ",{"type":27,"tag":653,"props":6746,"children":6748},{"className":6747},[],[6749],{"type":32,"value":6750},"return config",{"type":32,"value":6752}," statement makes sure that everything we have set up will actually be set into our config. This step is oftentimes forgotten, so if your plugins ever behave as if they are not installed at all, check for this return statement.",{"type":27,"tag":1029,"props":6754,"children":6755},{},[6756],{"type":27,"tag":28,"props":6757,"children":6758},{},[6759,6761,6766,6768,6774,6776,6783],{"type":32,"value":6760},"Since the compilation into Javascript is an important part of working with ",{"type":27,"tag":653,"props":6762,"children":6764},{"className":6763},[],[6765],{"type":32,"value":6676},{"type":32,"value":6767}," files, usually the intitial setup is the biggest hurdle to overcome. I find the ",{"type":27,"tag":172,"props":6769,"children":6771},{"href":6603,"rel":6770},[696],[6772],{"type":32,"value":6773},"setup from the docs",{"type":32,"value":6775}," the easiest one to work with, but if you are working with some other bundler, such as Webpack or Browserify, ",{"type":27,"tag":172,"props":6777,"children":6780},{"href":6778,"rel":6779},"https://github.com/badeball/cypress-cucumber-preprocessor/tree/master/examples",[696],[6781],{"type":32,"value":6782},"you can find examples here",{"type":32,"value":256},{"type":27,"tag":28,"props":6785,"children":6786},{},[6787],{"type":32,"value":6788},"Now that we have the plugin installed and configured, let's explore how to write tests.",{"type":27,"tag":45,"props":6790,"children":6792},{"id":6791},"test-scenarios-and-steps",[6793],{"type":32,"value":6794},"Test Scenarios and Steps",{"type":27,"tag":28,"props":6796,"children":6797},{},[6798,6800,6806],{"type":32,"value":6799},"Let's start by writing a simple test scenario in Gherkin syntax. Create a new file ",{"type":27,"tag":653,"props":6801,"children":6803},{"className":6802},[],[6804],{"type":32,"value":6805},"cypress/e2e/board.feature",{"type":32,"value":6807}," and add the following content:",{"type":27,"tag":793,"props":6809,"children":6813},{"className":6810,"code":6812,"filename":6805,"language":6483,"meta":5},[6811],"language-gherkin","Feature: Board functionality\n\n  Scenario: Create a board\n    Given I am on empty home page\n    When I type and submit in the board name\n    Then I should be redirected to the board detail\n",[6814],{"type":27,"tag":653,"props":6815,"children":6816},{"__ignoreMap":5},[6817],{"type":32,"value":6812},{"type":27,"tag":28,"props":6819,"children":6820},{},[6821,6823,6829,6830,6836],{"type":32,"value":6822},"Now, we need to create step definitions for each step in the scenario. The easiest way to define our steps is to create a new file called ",{"type":27,"tag":653,"props":6824,"children":6826},{"className":6825},[],[6827],{"type":32,"value":6828},"board.ts",{"type":32,"value":6263},{"type":27,"tag":653,"props":6831,"children":6833},{"className":6832},[],[6834],{"type":32,"value":6835},"cypress/e2e",{"type":32,"value":6837}," folder, that may look something like this:",{"type":27,"tag":793,"props":6839,"children":6843},{"className":6840,"code":6841,"filename":6842,"language":3520,"meta":5},[3517],"import { When, Then, Given } from \"@badeball/cypress-cucumber-preprocessor\";\n\nGiven(\"I am on empty home page\", () => {\n  cy.visit(\"/\");\n});\n\nWhen(\"I type and submit in the board name\", () => {\n  cy.get(\"[data-cy=first-board]\").type('new board{enter}');\n});\n\nThen(\"I should be redirected to the board detail\", () => {\n  cy.location(\"pathname\").should('match', /\\/board\\/\\d/);\n});\n","cypress/e2e/board.ts",[6844],{"type":27,"tag":653,"props":6845,"children":6846},{"__ignoreMap":5},[6847],{"type":32,"value":6841},{"type":27,"tag":28,"props":6849,"children":6850},{},[6851,6853,6858,6860,6865,6867,6873,6875,6881],{"type":32,"value":6852},"You can place your ",{"type":27,"tag":653,"props":6854,"children":6856},{"className":6855},[],[6857],{"type":32,"value":6828},{"type":32,"value":6859}," definition file into ",{"type":27,"tag":653,"props":6861,"children":6863},{"className":6862},[],[6864],{"type":32,"value":6835},{"type":32,"value":6866}," folder, or choose a different name and put it into ",{"type":27,"tag":653,"props":6868,"children":6870},{"className":6869},[],[6871],{"type":32,"value":6872},"cypress/e2e/board",{"type":32,"value":6874}," or into ",{"type":27,"tag":653,"props":6876,"children":6878},{"className":6877},[],[6879],{"type":32,"value":6880},"cypress/support/step_definitions",{"type":32,"value":6882}," folder and cucumber preprocessor will automatically pick them up. For a custom path, you need to put explicitly state this in the configuration. We’ll get to the configuration later in this post.",{"type":27,"tag":1029,"props":6884,"children":6885},{},[6886],{"type":27,"tag":28,"props":6887,"children":6888},{},[6889,6891,6898,6900,6905],{"type":32,"value":6890},"To improve your experience when writing tests in VS Code, I recommend installing the ",{"type":27,"tag":172,"props":6892,"children":6895},{"href":6893,"rel":6894},"https://marketplace.visualstudio.com/items?itemName=alexkrechik.cucumberautocomplete",[696],[6896],{"type":32,"value":6897},"extension by Alexander Krechik",{"type":32,"value":6899},". It will give you proper highlighting in ",{"type":27,"tag":653,"props":6901,"children":6903},{"className":6902},[],[6904],{"type":32,"value":6676},{"type":32,"value":6906}," files and easy access to step definitions.",{"type":27,"tag":45,"props":6908,"children":6910},{"id":6909},"adding-parameters-to-step-definitions",[6911],{"type":32,"value":6912},"Adding Parameters to Step Definitions",{"type":27,"tag":28,"props":6914,"children":6915},{},[6916],{"type":32,"value":6917},"Step definitions can accept parameters, allowing you to create more flexible and reusable test scenarios. Let’s rewrite our previous step definition file so that we can pass a board name of our own to our test:",{"type":27,"tag":793,"props":6919,"children":6922},{"className":6920,"code":6921,"filename":6842,"language":3520,"meta":5},[3517],"import { When, Then, Given } from \"@badeball/cypress-cucumber-preprocessor\";\n\nGiven(\"I am on empty home page\", () => {\n  cy.visit(\"/\");\n});\n\nWhen(\"I type in {string} and submit\", (boardName) => {\n  cy.get(\"[data-cy=first-board]\").type(`${boardName}{enter}`);\n});\n\nThen(\"I should be redirected to the board detail\", () => {\n  cy.location(\"pathname\").should('match', /\\/board\\/\\d/);\n});\n",[6923],{"type":27,"tag":653,"props":6924,"children":6925},{"__ignoreMap":5},[6926],{"type":32,"value":6921},{"type":27,"tag":28,"props":6928,"children":6929},{},[6930,6932,6938],{"type":32,"value":6931},"The parameters are automatically passed to the corresponding step definition functions as arguments. Check out the ",{"type":27,"tag":653,"props":6933,"children":6935},{"className":6934},[],[6936],{"type":32,"value":6937},"{string}",{"type":32,"value":6939}," in the step definition. This will actually check whether we are passing the proper type into our step.",{"type":27,"tag":28,"props":6941,"children":6942},{},[6943,6945,6950,6952,6958],{"type":32,"value":6944},"Let's now create a scenario in our ",{"type":27,"tag":653,"props":6946,"children":6948},{"className":6947},[],[6949],{"type":32,"value":6805},{"type":32,"value":6951}," file that accepts the ",{"type":27,"tag":653,"props":6953,"children":6955},{"className":6954},[],[6956],{"type":32,"value":6957},"boardName",{"type":32,"value":6959}," as a parameter. It will look a little something like this:",{"type":27,"tag":793,"props":6961,"children":6964},{"className":6962,"code":6963,"filename":6805,"language":6483,"meta":5},[6811],"Feature: Board functionality\n\n  Scenario: Create a board\n    Given I am on empty home page\n    When I type in \"my board\" and submit\n    Then I should be redirected to the board detail\n",[6965],{"type":27,"tag":653,"props":6966,"children":6967},{"__ignoreMap":5},[6968],{"type":32,"value":6963},{"type":27,"tag":45,"props":6970,"children":6972},{"id":6971},"data-driven-testing",[6973],{"type":32,"value":6974},"Data driven testing",{"type":27,"tag":28,"props":6976,"children":6977},{},[6978,6980,6986],{"type":32,"value":6979},"Another important concept in the cucumber that you should know are data tables. ",{"type":27,"tag":653,"props":6981,"children":6983},{"className":6982},[],[6984],{"type":32,"value":6985},"DataTable",{"type":32,"value":6987}," in Gherkin syntax allows you to pass a table of data to a step, making it easier to handle multiple data sets in your test scenarios. This is particularly useful for data-driven testing, where you want to test the same scenario with different sets of input data.",{"type":27,"tag":28,"props":6989,"children":6990},{},[6991,6993,6999,7001,7006],{"type":32,"value":6992},"Data tables are defined in the ",{"type":27,"tag":653,"props":6994,"children":6996},{"className":6995},[],[6997],{"type":32,"value":6998},"Examples",{"type":32,"value":7000}," section of your ",{"type":27,"tag":653,"props":7002,"children":7004},{"className":7003},[],[7005],{"type":32,"value":6676},{"type":32,"value":7007}," file. Let’s continue with our previous file:",{"type":27,"tag":793,"props":7009,"children":7012},{"className":7010,"code":7011,"filename":6805,"language":6483,"meta":5},[6811],"Feature: Board functionality\n\n  Scenario: Creating a \u003ClistName> list within a board\n    Given I am on empty home page\n    When I type in \"\u003CboardName>\" and submit\n    And Create a list with the name \"\u003ClistName>\"\n    Then I should be redirected to the board detail\n\n  Examples:\n      | boardName | listName |\n      | Shopping list | Groceries |\n      | Rocket launch | Preflight checks |\n",[7013],{"type":27,"tag":653,"props":7014,"children":7015},{"__ignoreMap":5},[7016],{"type":32,"value":7011},{"type":27,"tag":28,"props":7018,"children":7019},{},[7020,7022,7027,7029,7034,7035,7041,7043,7049],{"type":32,"value":7021},"With ",{"type":27,"tag":653,"props":7023,"children":7025},{"className":7024},[],[7026],{"type":32,"value":6998},{"type":32,"value":7028}," steps defined, you’ll run your test multiple times, passing different data with every step. Notice how we create variables ",{"type":27,"tag":653,"props":7030,"children":7032},{"className":7031},[],[7033],{"type":32,"value":6957},{"type":32,"value":4164},{"type":27,"tag":653,"props":7036,"children":7038},{"className":7037},[],[7039],{"type":32,"value":7040},"listName",{"type":32,"value":7042},", wrap them in ",{"type":27,"tag":653,"props":7044,"children":7046},{"className":7045},[],[7047],{"type":32,"value":7048},"\u003C>",{"type":32,"value":7050}," to be passed as parameters into our step definitions.",{"type":27,"tag":45,"props":7052,"children":7054},{"id":7053},"working-array-of-data",[7055],{"type":32,"value":7056},"Working array of data",{"type":27,"tag":28,"props":7058,"children":7059},{},[7060],{"type":32,"value":7061},"These data tables can also be used to feed data into a single step as shown in the following example:",{"type":27,"tag":793,"props":7063,"children":7066},{"className":7064,"code":7065,"filename":6805,"language":6483,"meta":5},[6811],"Feature: Creating cards functionality\n\n  Scenario: Create multiple cards\n    Given I am in board detail\n    When I create cards with names\n    | Milk | Bread | Butter | Jam |\n    Then 4 cards are visible\n",[7067],{"type":27,"tag":653,"props":7068,"children":7069},{"__ignoreMap":5},[7070],{"type":32,"value":7065},{"type":27,"tag":28,"props":7072,"children":7073},{},[7074],{"type":32,"value":7075},"The step however needs to be able to digest the datatable. This is how you can make it work:",{"type":27,"tag":793,"props":7077,"children":7081},{"className":7078,"code":7079,"filename":7080,"language":3520,"meta":5},[3517],"When(\"I create cards with names\", (table: DataTable) => {\n  cy.get('[data-cy=\"new-card\"]')\n    .click()\n\n  table.raw()[0].forEach(item => {\n\n    cy.get('[data-cy=\"new-card-input\"]')\n      .type(`${item}{enter}`)\n\n  })\n});\n","cypress/e2e/cards.ts",[7082],{"type":27,"tag":653,"props":7083,"children":7084},{"__ignoreMap":5},[7085],{"type":32,"value":7079},{"type":27,"tag":28,"props":7087,"children":7088},{},[7089,7090,7096,7098,7104],{"type":32,"value":3349},{"type":27,"tag":653,"props":7091,"children":7093},{"className":7092},[],[7094],{"type":32,"value":7095},"table.raw()[0]",{"type":32,"value":7097}," function will return the first line (",{"type":27,"tag":653,"props":7099,"children":7101},{"className":7100},[],[7102],{"type":32,"value":7103},"[0]",{"type":32,"value":7105},") of the table as an array. Inside the step definition, we are looping over this array to create items in the list.",{"type":27,"tag":45,"props":7107,"children":7109},{"id":7108},"grouping-tests",[7110],{"type":32,"value":7111},"Grouping tests",{"type":27,"tag":28,"props":7113,"children":7114},{},[7115,7117,7123,7124,7130,7131,7137,7138,7144,7146,7151],{"type":32,"value":7116},"In addition to ",{"type":27,"tag":653,"props":7118,"children":7120},{"className":7119},[],[7121],{"type":32,"value":7122},"Given",{"type":32,"value":3372},{"type":27,"tag":653,"props":7125,"children":7127},{"className":7126},[],[7128],{"type":32,"value":7129},"When",{"type":32,"value":3372},{"type":27,"tag":653,"props":7132,"children":7134},{"className":7133},[],[7135],{"type":32,"value":7136},"Then",{"type":32,"value":4164},{"type":27,"tag":653,"props":7139,"children":7141},{"className":7140},[],[7142],{"type":32,"value":7143},"And",{"type":32,"value":7145}," keywords, there are some other ways of how to organize multiple tests in a single ",{"type":27,"tag":653,"props":7147,"children":7149},{"className":7148},[],[7150],{"type":32,"value":6676},{"type":32,"value":7152}," file. Our test so far has been creating a new board and a new list, but let’s change our test slightly and create one test that will just create another board and put it in front of our existing test:",{"type":27,"tag":793,"props":7154,"children":7158},{"className":7155,"code":7156,"filename":6805,"highlights":7157,"language":6483,"meta":5},[6811],"Feature: Board functionality\n\n  Scenario: Opening a board\n    Given I am on empty home page\n    When I type in \"\u003CboardName>\" and submit\n    Then I should be redirected to the board detail\n\n  Scenario: Creating a \u003ClistName> list within a board\n    Given I am on empty home page\n    When I type in \"\u003CboardName>\" and submit\n    And Create a list with the name \"\u003ClistName>\"\n    Then I should be redirected to the board detail\n\n  Examples:\n    | boardName | listName |\n    | Shopping list | Groceries |\n    | Rocket launch | Preflight checks |\n",[1606,3877,3667,3809],[7159],{"type":27,"tag":653,"props":7160,"children":7161},{"__ignoreMap":5},[7162],{"type":32,"value":7156},{"type":27,"tag":28,"props":7164,"children":7165},{},[7166,7168,7174,7175,7181,7182,7188,7190,7196,7198,7203],{"type":32,"value":7167},"Similarly to ",{"type":27,"tag":653,"props":7169,"children":7171},{"className":7170},[],[7172],{"type":32,"value":7173},"describe()",{"type":32,"value":3372},{"type":27,"tag":653,"props":7176,"children":7178},{"className":7177},[],[7179],{"type":32,"value":7180},"context()",{"type":32,"value":4164},{"type":27,"tag":653,"props":7183,"children":7185},{"className":7184},[],[7186],{"type":32,"value":7187},"it()",{"type":32,"value":7189}," blocks in Mocha, we can further organize our tests and group them into logical clusters. ",{"type":27,"tag":653,"props":7191,"children":7193},{"className":7192},[],[7194],{"type":32,"value":7195},"Feature",{"type":32,"value":7197}," keyword acts as a ",{"type":27,"tag":653,"props":7199,"children":7201},{"className":7200},[],[7202],{"type":32,"value":7173},{"type":32,"value":7204}," block and serves as top level group.",{"type":27,"tag":28,"props":7206,"children":7207},{},[7208,7210,7215,7217,7223],{"type":32,"value":7209},"Inside a ",{"type":27,"tag":653,"props":7211,"children":7213},{"className":7212},[],[7214],{"type":32,"value":7195},{"type":32,"value":7216}," scope, you can add a ",{"type":27,"tag":653,"props":7218,"children":7220},{"className":7219},[],[7221],{"type":32,"value":7222},"Rule",{"type":32,"value":7224}," block, that would further split your scenarios into sub-groups.",{"type":27,"tag":28,"props":7226,"children":7227},{},[7228,7230,7236,7238,7244,7246,7251,7252,7257,7259,7264],{"type":32,"value":7229},"As you test different scenarios, you can add a ",{"type":27,"tag":653,"props":7231,"children":7233},{"className":7232},[],[7234],{"type":32,"value":7235},"Background",{"type":32,"value":7237}," step, that will act sort of like a ",{"type":27,"tag":653,"props":7239,"children":7241},{"className":7240},[],[7242],{"type":32,"value":7243},"beforeEach()",{"type":32,"value":7245}," hook in Mocha and run a sequence of steps before every scenario. We can abstract our ",{"type":27,"tag":653,"props":7247,"children":7249},{"className":7248},[],[7250],{"type":32,"value":7122},{"type":32,"value":4164},{"type":27,"tag":653,"props":7253,"children":7255},{"className":7254},[],[7256],{"type":32,"value":7129},{"type":32,"value":7258}," steps from our current ",{"type":27,"tag":653,"props":7260,"children":7262},{"className":7261},[],[7263],{"type":32,"value":6676},{"type":32,"value":7265}," file and make our test a little bit cleaner.",{"type":27,"tag":28,"props":7267,"children":7268},{},[7269,7271,7276],{"type":32,"value":7270},"Together with a ",{"type":27,"tag":653,"props":7272,"children":7274},{"className":7273},[],[7275],{"type":32,"value":7222},{"type":32,"value":7277}," keyword, our test can look a little something like this:",{"type":27,"tag":793,"props":7279,"children":7282},{"className":7280,"code":7281,"filename":6805,"language":6483,"meta":5},[6811],"Feature: Board functionality\n\n  Rule: Happy paths\n\n  Background: Empty board page\n    Given I am on empty home page\n\n  Scenario: Opening a board\n    When I type in \"new board\" and submit\n    Then I should be redirected to the board detail\n\n  Scenario: Creating a \u003ClistName> list within a board\n    When I type in \"\u003CboardName>\" and submit\n    And Create a list with the name \"\u003ClistName>\"\n    Then I should be redirected to the board detail\n\n  Examples:\n    | boardName | listName |\n    | Shopping list | Groceries |\n    | Rocket launch | Preflight checks |\n",[7283],{"type":27,"tag":653,"props":7284,"children":7285},{"__ignoreMap":5},[7286],{"type":32,"value":7281},{"type":27,"tag":45,"props":7288,"children":7290},{"id":7289},"using-hooks",[7291],{"type":32,"value":7292},"Using hooks",{"type":27,"tag":28,"props":7294,"children":7295},{},[7296,7298,7303,7305,7311,7312,7318,7320,7325,7326,7332],{"type":32,"value":7297},"While there is possibility to add ",{"type":27,"tag":653,"props":7299,"children":7301},{"className":7300},[],[7302],{"type":32,"value":7235},{"type":32,"value":7304}," we can still define a ",{"type":27,"tag":653,"props":7306,"children":7308},{"className":7307},[],[7309],{"type":32,"value":7310},"Before",{"type":32,"value":4164},{"type":27,"tag":653,"props":7313,"children":7315},{"className":7314},[],[7316],{"type":32,"value":7317},"After",{"type":32,"value":7319}," steps, that act like ",{"type":27,"tag":653,"props":7321,"children":7323},{"className":7322},[],[7324],{"type":32,"value":7243},{"type":32,"value":4164},{"type":27,"tag":653,"props":7327,"children":7329},{"className":7328},[],[7330],{"type":32,"value":7331},"afterEach()",{"type":32,"value":7333}," hooks in Mocha. Failure in these will not make your tests fail as they are actually running inside your tests.",{"type":27,"tag":28,"props":7335,"children":7336},{},[7337,7342,7343,7348,7350,7355],{"type":27,"tag":653,"props":7338,"children":7340},{"className":7339},[],[7341],{"type":32,"value":7310},{"type":32,"value":4164},{"type":27,"tag":653,"props":7344,"children":7346},{"className":7345},[],[7347],{"type":32,"value":7317},{"type":32,"value":7349}," steps are part of your step definition file, which means you don’t need to add them into ",{"type":27,"tag":653,"props":7351,"children":7353},{"className":7352},[],[7354],{"type":32,"value":6676},{"type":32,"value":786},{"type":27,"tag":793,"props":7357,"children":7361},{"className":7358,"code":7359,"filename":6842,"highlights":7360,"language":3520,"meta":5},[3517],"import { When, Then, Given, Before } from \"@badeball/cypress-cucumber-preprocessor\";\n\nBefore(() => {\n  // reset application\n  cy.request('POST', '/api/reset')\n})\n\nGiven(\"I am on empty home page\", () => {\n  cy.visit(\"/\");\n});\n\nWhen(\"I type in {string} and submit\", (boardName) => {\n  cy.get(\"[data-cy=first-board]\").type(`${boardName}{enter}`);\n});\n\nWhen(\"Create a list with the name {string}\", (listName) => {\n  cy.get('[data-cy=\"add-list-input\"]').type(`${listName}{enter}`);\n});\n\nThen(\"I should be redirected to the board detail\", () => {\n  cy.location(\"pathname\").should('match', /\\/board\\/\\d/);\n});\n",[1606,3877,3667,3809],[7362],{"type":27,"tag":653,"props":7363,"children":7364},{"__ignoreMap":5},[7365],{"type":32,"value":7359},{"type":27,"tag":45,"props":7367,"children":7369},{"id":7368},"test-tagging",[7370],{"type":32,"value":7371},"Test tagging",{"type":27,"tag":28,"props":7373,"children":7374},{},[7375],{"type":32,"value":7376},"Tags are a powerful feature in Cucumber syntax that allows you to categorize and filter scenarios. You can use tags to run specific scenarios or exclude scenarios from the test run.",{"type":27,"tag":28,"props":7378,"children":7379},{},[7380],{"type":32,"value":7381},"To add tags to your scenarios, simply prefix the scenario or feature with an @ symbol followed by the tag name. For example, let's add a @regression tag to the successful login scenario in the cypress/e2e/board.feature file:",{"type":27,"tag":793,"props":7383,"children":7387},{"className":7384,"code":7385,"filename":6805,"highlights":7386,"language":6483,"meta":5},[6811],"Feature: Board functionality\n\n  Rule: Happy paths\n\n  Background: Empty board page\n    Given I am on empty home page\n\n  @smoke\n  Scenario: Opening a board\n    When I type in \"new board\" and submit\n    Then I should be redirected to the board detail\n\n  Scenario: Creating a \u003ClistName> list within a board\n    When I type in \"\u003CboardName>\" and submit\n    And Create a list with the name \"\u003ClistName>\"\n    Then I should be redirected to the board detail\n\n  Examples:\n    | boardName | listName |\n    | Shopping list | Groceries |\n    | Rocket launch | Preflight checks |\n",[3723],[7388],{"type":27,"tag":653,"props":7389,"children":7390},{"__ignoreMap":5},[7391],{"type":32,"value":7385},{"type":27,"tag":28,"props":7393,"children":7394},{},[7395],{"type":32,"value":7396},"To run tests with a specific tag, use the following command:",{"type":27,"tag":793,"props":7398,"children":7401},{"className":7399,"code":7400,"language":1084,"meta":5},[1082],"npx cypress run --env tags=\"@smoke\"\n",[7402],{"type":27,"tag":653,"props":7403,"children":7404},{"__ignoreMap":5},[7405],{"type":32,"value":7400},{"type":27,"tag":28,"props":7407,"children":7408},{},[7409,7411,7417],{"type":32,"value":7410},"This will skip the tests that do not contain the ",{"type":27,"tag":653,"props":7412,"children":7414},{"className":7413},[],[7415],{"type":32,"value":7416},"@smoke",{"type":32,"value":7418}," tag.",{"type":27,"tag":28,"props":7420,"children":7421},{},[7422],{"type":27,"tag":959,"props":7423,"children":7426},{"alt":7424,"src":7425},"tests filtered by tag using cucumber","skipped_tests_ity6mw.png",[],{"type":27,"tag":28,"props":7428,"children":7429},{},[7430,7432,7438,7440,7445,7447,7453],{"type":32,"value":7431},"You can also test this on ",{"type":27,"tag":653,"props":7433,"children":7435},{"className":7434},[],[7436],{"type":32,"value":7437},"open",{"type":32,"value":7439}," mode using the same command but with ",{"type":27,"tag":653,"props":7441,"children":7443},{"className":7442},[],[7444],{"type":32,"value":7437},{"type":32,"value":7446}," instead of ",{"type":27,"tag":653,"props":7448,"children":7450},{"className":7449},[],[7451],{"type":32,"value":7452},"run",{"type":32,"value":256},{"type":27,"tag":28,"props":7455,"children":7456},{},[7457,7459,7465],{"type":32,"value":7458},"In addition to running all tests with certain tag, you can pass a ",{"type":27,"tag":653,"props":7460,"children":7462},{"className":7461},[],[7463],{"type":32,"value":7464},"not",{"type":32,"value":7466}," keyword to run all tags exept specified one.",{"type":27,"tag":793,"props":7468,"children":7471},{"className":7469,"code":7470,"language":1084,"meta":5},[1082],"npx cypress run --env tags=\"not @smoke\"\n",[7472],{"type":27,"tag":653,"props":7473,"children":7474},{"__ignoreMap":5},[7475],{"type":32,"value":7470},{"type":27,"tag":28,"props":7477,"children":7478},{},[7479],{"type":32,"value":7480},"There’s also a way of running all tests that contain either one of tags:",{"type":27,"tag":793,"props":7482,"children":7485},{"className":7483,"code":7484,"language":1084,"meta":5},[1082],"npx cypress run --env tags=\"@smoke or @regression\"\n",[7486],{"type":27,"tag":653,"props":7487,"children":7488},{"__ignoreMap":5},[7489],{"type":32,"value":7484},{"type":27,"tag":28,"props":7491,"children":7492},{},[7493],{"type":32,"value":7494},"Or tests that contain both:",{"type":27,"tag":793,"props":7496,"children":7499},{"className":7497,"code":7498,"language":1084,"meta":5},[1082],"npx cypress run --env tags=\"@smoke and @regression\"\n",[7500],{"type":27,"tag":653,"props":7501,"children":7502},{"__ignoreMap":5},[7503],{"type":32,"value":7498},{"type":27,"tag":28,"props":7505,"children":7506},{},[7507,7509,7515,7516,7522,7524,7530,7532,7537],{"type":32,"value":7508},"To speed up the test execution, you can use ",{"type":27,"tag":653,"props":7510,"children":7512},{"className":7511},[],[7513],{"type":32,"value":7514},"filterSpecs",{"type":32,"value":4164},{"type":27,"tag":653,"props":7517,"children":7519},{"className":7518},[],[7520],{"type":32,"value":7521},"omitFiltered",{"type":32,"value":7523}," options that work similarly to how ",{"type":27,"tag":653,"props":7525,"children":7527},{"className":7526},[],[7528],{"type":32,"value":7529},"@cypress/grep",{"type":32,"value":7531}," plugin works. You can enable this functionality by adding following options into your ",{"type":27,"tag":653,"props":7533,"children":7535},{"className":7534},[],[7536],{"type":32,"value":5669},{"type":32,"value":7538}," file:",{"type":27,"tag":793,"props":7540,"children":7548},{"className":7541,"code":7542,"highlights":7543,"language":3520,"meta":5},[3517],"import { defineConfig } from \"cypress\";\nimport createBundler from \"@bahmutov/cypress-esbuild-preprocessor\";\nimport { addCucumberPreprocessorPlugin } from \"@badeball/cypress-cucumber-preprocessor\";\nimport createEsbuildPlugin from \"@badeball/cypress-cucumber-preprocessor/esbuild\";\n\nexport default defineConfig({\n  e2e: {\n    specPattern: \"**/*.feature\",\n    async setupNodeEvents(\n      on: Cypress.PluginEvents,\n      config: Cypress.PluginConfigOptions\n    ): Promise\u003CCypress.PluginConfigOptions> {\n      await addCucumberPreprocessorPlugin(on, config);\n      on(\n        \"file:preprocessor\",\n        createBundler({\n          plugins: [createEsbuildPlugin(config)],\n        })\n      );\n      return config;\n    },\n    env: {\n      omitFiltered: true,\n      filterSpecs: true\n    },\n    fixturesFolder: false,\n    baseUrl: 'http://localhost:3000'\n  },\n});\n",[7544,7545,7546,7547],22,23,24,25,[7549],{"type":27,"tag":653,"props":7550,"children":7551},{"__ignoreMap":5},[7552],{"type":32,"value":7542},{"type":27,"tag":45,"props":7554,"children":7556},{"id":7555},"configuration",[7557],{"type":32,"value":7558},"Configuration",{"type":27,"tag":28,"props":7560,"children":7561},{},[7562,7564,7570],{"type":32,"value":7563},"There are two way of how you can modify the default configuration of the Cucumber preprocessor. You can either create a ",{"type":27,"tag":653,"props":7565,"children":7567},{"className":7566},[],[7568],{"type":32,"value":7569},".cypress-cucumber-preprocessorrc.json",{"type":32,"value":7571}," config file that may look like this:",{"type":27,"tag":793,"props":7573,"children":7576},{"className":7574,"code":7575,"filename":7569,"language":1004,"meta":5},[1002],"{\n  \"stepDefinitions\": [\n    \"cypress/e2e/[filepath]/**/*.{js,ts}\",\n    \"cypress/e2e/[filepath].{js,ts}\",\n    \"cypress/support/step_definitions/**/*.{js,ts}\",\n  ]\n}\n",[7577],{"type":27,"tag":653,"props":7578,"children":7579},{"__ignoreMap":5},[7580],{"type":32,"value":7575},{"type":27,"tag":28,"props":7582,"children":7583},{},[7584,7586,7591],{"type":32,"value":7585},"Or set everything up right in your ",{"type":27,"tag":653,"props":7587,"children":7589},{"className":7588},[],[7590],{"type":32,"value":734},{"type":32,"value":7592}," by adding the equivalent:",{"type":27,"tag":793,"props":7594,"children":7597},{"className":7595,"code":7596,"filename":734,"language":1004,"meta":5},[1002],"// rest of file skipped for brevity\n\"cypress-cucumber-preprocessor\": {\n  \"stepDefinitions\": [\n    \"cypress/e2e/[filepath]/**/*.{js,ts}\",\n    \"cypress/e2e/[filepath].{js,ts}\",\n    \"cypress/support/step_definitions/**/*.{js,ts}\",\n  ]\n}\n",[7598],{"type":27,"tag":653,"props":7599,"children":7600},{"__ignoreMap":5},[7601],{"type":32,"value":7596},{"type":27,"tag":1029,"props":7603,"children":7604},{},[7605],{"type":27,"tag":28,"props":7606,"children":7607},{},[7608],{"type":32,"value":7609},"The settings from examples are defaults. Unless you want to change anthing, there’s no need to add this to your project.",{"type":27,"tag":45,"props":7611,"children":7613},{"id":7612},"reporting",[7614],{"type":32,"value":7615},"Reporting",{"type":27,"tag":28,"props":7617,"children":7618},{},[7619],{"type":32,"value":7620},"Cucumber plugin for Cypress comes with a variety of options for setting up reporters. I’m goint to show you the simplest one - HTML reporter.",{"type":27,"tag":28,"props":7622,"children":7623},{},[7624],{"type":32,"value":7625},"Pretty much all you need to do is to set up your configuration:",{"type":27,"tag":793,"props":7627,"children":7630},{"className":7628,"code":7629,"language":1004,"meta":5},[1002],"{\n  \"html\": {\n    \"enabled\": true\n  }\n}\n",[7631],{"type":27,"tag":653,"props":7632,"children":7633},{"__ignoreMap":5},[7634],{"type":32,"value":7629},{"type":27,"tag":28,"props":7636,"children":7637},{},[7638],{"type":32,"value":7639},"After you run your test, you will get a nicely formatted HTML report that looks like this:",{"type":27,"tag":28,"props":7641,"children":7642},{},[7643],{"type":27,"tag":959,"props":7644,"children":7647},{"alt":7645,"src":7646},"HTML report for Cucumber in Cypress","cucumber_cypress_report_bnnddf",[],{"type":27,"tag":28,"props":7649,"children":7650},{},[7651,7653,7659,7661,7668],{"type":32,"value":7652},"If you need a more advanced output that you later want to parse and feed into your own reporting system, I recommend checking out ",{"type":27,"tag":653,"props":7654,"children":7656},{"className":7655},[],[7657],{"type":32,"value":7658},"json-formatter",{"type":32,"value":7660}," ",{"type":27,"tag":172,"props":7662,"children":7665},{"href":7663,"rel":7664},"https://github.com/cucumber/json-formatter",[696],[7666],{"type":32,"value":7667},"directly from cucumber authors",{"type":32,"value":7669},". You will need to install it separately and set it up to run in your configuration file.",{"type":27,"tag":45,"props":7671,"children":7673},{"id":7672},"final-thoughts",[7674],{"type":32,"value":7675},"Final thoughts",{"type":27,"tag":28,"props":7677,"children":7678},{},[7679],{"type":32,"value":7680},"As I mentioned in the beginning, there are more effective ways of using Cypress. Doing everything in your end to end tests using UI is a slight overkill and given the design of Cypress, you can unlock much more power by using it the way it was intended.",{"type":27,"tag":28,"props":7682,"children":7683},{},[7684,7686,7691,7693,7698],{"type":32,"value":7685},"However, I hope you found this blogpost useful if you are going to use Cucumber with Cypress. If you have any additional questions, feel free to reach out to me on ",{"type":27,"tag":172,"props":7687,"children":7689},{"href":5770,"rel":7688},[696],[7690],{"type":32,"value":1589},{"type":32,"value":7692}," or on ",{"type":27,"tag":172,"props":7694,"children":7696},{"href":5777,"rel":7695},[696],[7697],{"type":32,"value":1598},{"type":32,"value":7699},". It would mean a world to me if you share this blogpost further.",{"title":5,"searchDepth":320,"depth":320,"links":7701},[7702,7703,7704,7705,7706,7707,7708,7709,7710,7711,7712,7713],{"id":6524,"depth":320,"text":6527},{"id":6569,"depth":320,"text":6572},{"id":6791,"depth":320,"text":6794},{"id":6909,"depth":320,"text":6912},{"id":6971,"depth":320,"text":6974},{"id":7053,"depth":320,"text":7056},{"id":7108,"depth":320,"text":7111},{"id":7289,"depth":320,"text":7292},{"id":7368,"depth":320,"text":7371},{"id":7555,"depth":320,"text":7558},{"id":7612,"depth":320,"text":7615},{"id":7672,"depth":320,"text":7675},"content:cucumber-in-cypress-a-step-by-step-guide:index.md","cucumber-in-cypress-a-step-by-step-guide/index.md","cucumber-in-cypress-a-step-by-step-guide/index",{"_path":7718,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":7719,"description":7720,"date":7721,"published":10,"slug":7722,"tags":7723,"cypressVersion":5959,"readingTime":7725,"body":7729,"_type":329,"_id":8223,"_source":331,"_file":8224,"_stem":8225,"_extension":334},"/how-to-wait-for-page-to-load-in-cypress","How to wait for page to load in Cypress","What if your page takes time to load and your first command fails because of this? In this blogpost I explain how to make sure that your page is fully loaded","2023-03-15","how-to-wait-for-page-to-load-in-cypress",[5279,5956,7724],"loading",{"text":927,"minutes":7726,"time":7727,"words":7728},4.99,299400,998,{"type":24,"children":7730,"toc":8217},[7731,7736,7742,7747,7755,7768,7782,7787,7816,7821,7832,7859,7886,7891,7924,7930,7935,7944,7957,7969,7978,7991,8000,8057,8066,8072,8077,8082,8091,8112,8138,8144,8186,8190],{"type":27,"tag":28,"props":7732,"children":7733},{},[7734],{"type":32,"value":7735},"Cypress test can be pretty fast. Sometimes even faster than the application we are testing. If you find yourself in a situation where Cypress runs faster than you application loads, this blogpost is for you.",{"type":27,"tag":45,"props":7737,"children":7739},{"id":7738},"page-load-event",[7740],{"type":32,"value":7741},"Page load event",{"type":27,"tag":28,"props":7743,"children":7744},{},[7745],{"type":32,"value":7746},"If you found this post via Google search, you might have started your search, because you saw a message that says: \"Timed out after waiting 60000ms for your remote page to load.\"",{"type":27,"tag":28,"props":7748,"children":7749},{},[7750],{"type":27,"tag":959,"props":7751,"children":7754},{"alt":7752,"src":7753},"Page timeout error in Cypress","pageLoadTimeout_error_mxm3ft.png",[],{"type":27,"tag":28,"props":7756,"children":7757},{},[7758,7760,7766],{"type":32,"value":7759},"This message appears when your page does not trigger the ",{"type":27,"tag":653,"props":7761,"children":7763},{"className":7762},[],[7764],{"type":32,"value":7765},"load",{"type":32,"value":7767}," event. But what actually is that?",{"type":27,"tag":28,"props":7769,"children":7770},{},[7771,7773,7780],{"type":32,"value":7772},"According to ",{"type":27,"tag":172,"props":7774,"children":7777},{"href":7775,"rel":7776},"https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event",[696],[7778],{"type":32,"value":7779},"MDN docs",{"type":32,"value":7781},", load event is something that your browser triggers once has downloaded all page assets.",{"type":27,"tag":28,"props":7783,"children":7784},{},[7785],{"type":32,"value":7786},"So what does that mean?",{"type":27,"tag":28,"props":7788,"children":7789},{},[7790,7792,7798,7800,7806,7808,7814],{"type":32,"value":7791},"Whenever you enter an URL into your browser and hit enter, your browser is going to make a ",{"type":27,"tag":653,"props":7793,"children":7795},{"className":7794},[],[7796],{"type":32,"value":7797},"GET",{"type":32,"value":7799}," api call to that address. Similarly to when you do API testing with Postman or with Cypress. Instead of getting a ",{"type":27,"tag":653,"props":7801,"children":7803},{"className":7802},[],[7804],{"type":32,"value":7805},"JSON",{"type":32,"value":7807},"-structured object, you are going to get an ",{"type":27,"tag":653,"props":7809,"children":7811},{"className":7810},[],[7812],{"type":32,"value":7813},"HTML",{"type":32,"value":7815}," document.",{"type":27,"tag":28,"props":7817,"children":7818},{},[7819],{"type":32,"value":7820},"A simplified version of the document that is return will look like this:",{"type":27,"tag":793,"props":7822,"children":7827},{"className":7823,"code":7825,"language":7826,"meta":5},[7824],"language-html","\u003Chtml>\n\u003Chead>\n  \u003Ctitle>My page\u003C/title>\n  \u003Clink rel=\"stylesheet\" href=\"style.css\">\n  \u003Cscript src=\"app.js\" defer>\u003C/script>\n\u003C/head>\n\u003Cbody>\n  Hello world!\n\u003C/body>\n\u003C/html>\n","html",[7828],{"type":27,"tag":653,"props":7829,"children":7830},{"__ignoreMap":5},[7831],{"type":32,"value":7825},{"type":27,"tag":28,"props":7833,"children":7834},{},[7835,7837,7843,7844,7850,7852,7857],{"type":32,"value":7836},"Browser will take a look into the document and will request all files linked to it. Notice we have ",{"type":27,"tag":653,"props":7838,"children":7840},{"className":7839},[],[7841],{"type":32,"value":7842},"style.css",{"type":32,"value":4164},{"type":27,"tag":653,"props":7845,"children":7847},{"className":7846},[],[7848],{"type":32,"value":7849},"app.js",{"type":32,"value":7851}," files. Browser will download them and once it’s done with this, it will trigger the ",{"type":27,"tag":653,"props":7853,"children":7855},{"className":7854},[],[7856],{"type":32,"value":7765},{"type":32,"value":7858}," event that Cypress waits for.",{"type":27,"tag":28,"props":7860,"children":7861},{},[7862,7864,7870,7872,7877,7879,7884],{"type":32,"value":7863},"This is pretty much the best point at which we can say that application is ready. Realistically, many sites have dynamic ",{"type":27,"tag":653,"props":7865,"children":7867},{"className":7866},[],[7868],{"type":32,"value":7869},".js",{"type":32,"value":7871}," files that starts calling APIs that load resources from a server, or animate elements on page. These may cause the application to still be in \"loading\" mode. There’s no telling how many resources these ",{"type":27,"tag":653,"props":7873,"children":7875},{"className":7874},[],[7876],{"type":32,"value":7869},{"type":32,"value":7878}," files will load or how long it will take to load them. While the ",{"type":27,"tag":653,"props":7880,"children":7882},{"className":7881},[],[7883],{"type":32,"value":7765},{"type":32,"value":7885}," event is a good checkpoint, in case of modern web applications, it is rarely last thing that happens on a page.",{"type":27,"tag":28,"props":7887,"children":7888},{},[7889],{"type":32,"value":7890},"If you see an error that Cypress could not load your page, chances are that this download is not happening, or something is blocking it. This may be an issue on your side, so you can treat this as a bug that needs to be fixed.",{"type":27,"tag":1029,"props":7892,"children":7893},{},[7894,7915],{"type":27,"tag":28,"props":7895,"children":7896},{},[7897,7899,7906,7908,7913],{"type":32,"value":7898},"I recently saw the mentioned error happen on ",{"type":27,"tag":172,"props":7900,"children":7903},{"href":7901,"rel":7902},"https://www.saucedemo.com",[696],[7904],{"type":32,"value":7905},"demo site from Sauce labs",{"type":32,"value":7907},". The reason for this problem was how service worker was configured. It caused the ",{"type":27,"tag":653,"props":7909,"children":7911},{"className":7910},[],[7912],{"type":32,"value":7765},{"type":32,"value":7914}," event to never happen. This is not a problem with all service workers, but it caused some trouble for me. The final solution was to stub the service worker API call, essentially disabling the service worker. I added the following intercept and restarted the browser.",{"type":27,"tag":793,"props":7916,"children":7919},{"className":7917,"code":7918,"language":3520,"meta":5},[3517],"cy.intercept('/service-worker.js', {\n body: undefined\n})\n",[7920],{"type":27,"tag":653,"props":7921,"children":7922},{"__ignoreMap":5},[7923],{"type":32,"value":7918},{"type":27,"tag":45,"props":7925,"children":7927},{"id":7926},"waiting-for-network-calls",[7928],{"type":32,"value":7929},"Waiting for network calls",{"type":27,"tag":28,"props":7931,"children":7932},{},[7933],{"type":32,"value":7934},"Another way of ensuring that a page is loaded is to wait for a network call. For example, when you have a to-do app that will load all the to-dos when it’s opened, you can use a network call intercept that will wait for that network call to happen:",{"type":27,"tag":793,"props":7936,"children":7939},{"className":7937,"code":7938,"language":3520,"meta":5},[3517],"it('testing todos', () => {\n  cy.intercept('/todos').as('todos')\n  cy.visit('/')\n  cy.wait('@todos')\n  // page is loaded, continue with the test\n})\n",[7940],{"type":27,"tag":653,"props":7941,"children":7942},{"__ignoreMap":5},[7943],{"type":32,"value":7938},{"type":27,"tag":28,"props":7945,"children":7946},{},[7947,7949,7955],{"type":32,"value":7948},"Since ",{"type":27,"tag":653,"props":7950,"children":7952},{"className":7951},[],[7953],{"type":32,"value":7954},"cy.wait()",{"type":32,"value":7956}," will wait for not only the request to happen, but also the response, it can be a reliable way of making sure that your page is properly loaded.",{"type":27,"tag":28,"props":7958,"children":7959},{},[7960,7962,7967],{"type":32,"value":7961},"If you have multiple API calls, you can intercept them all and make your test wait for them by passing an array of aliases to ",{"type":27,"tag":653,"props":7963,"children":7965},{"className":7964},[],[7966],{"type":32,"value":7954},{"type":32,"value":7968}," command like this:",{"type":27,"tag":793,"props":7970,"children":7973},{"className":7971,"code":7972,"language":3520,"meta":5},[3517],"it('testing todos', () => {\n  cy.intercept('/todos').as('todos')\n  cy.intercept('/profile').as('profile')\n  cy.visit('/')\n  cy.wait(['@todos', '@profile'])\n  // page is loaded, continue with the test\n})\n",[7974],{"type":27,"tag":653,"props":7975,"children":7976},{"__ignoreMap":5},[7977],{"type":32,"value":7972},{"type":27,"tag":28,"props":7979,"children":7980},{},[7981,7983,7989],{"type":32,"value":7982},"Another way of waiting for multiple requests is to use ",{"type":27,"tag":653,"props":7984,"children":7986},{"className":7985},[],[7987],{"type":32,"value":7988},"cy.get('@alias.all')",{"type":32,"value":7990}," syntax. This way you can wait for all requests to happen.",{"type":27,"tag":793,"props":7992,"children":7995},{"className":7993,"code":7994,"language":3520,"meta":5},[3517],"it('testing todos', () => {\n  cy.intercept('**').as('requests')\n  cy.visit('/')\n  cy.get('@requests.all')\n    .should('have.length', 10)\n  // all 10 requests have been sent\n})\n",[7996],{"type":27,"tag":653,"props":7997,"children":7998},{"__ignoreMap":5},[7999],{"type":32,"value":7994},{"type":27,"tag":28,"props":8001,"children":8002},{},[8003,8005,8011,8013,8019,8021,8027,8029,8034,8036,8041,8043,8048,8050,8055],{"type":32,"value":8004},"There are two small gotchas though. ",{"type":27,"tag":653,"props":8006,"children":8008},{"className":8007},[],[8009],{"type":32,"value":8010},"cy.get()",{"type":32,"value":8012}," will wait only for 4000 ms for the request to happen, as it listens to ",{"type":27,"tag":653,"props":8014,"children":8016},{"className":8015},[],[8017],{"type":32,"value":8018},"defaultCommandTimeout",{"type":32,"value":8020}," option instead of ",{"type":27,"tag":653,"props":8022,"children":8024},{"className":8023},[],[8025],{"type":32,"value":8026},"responseTimeout",{"type":32,"value":8028}," as it does with ",{"type":27,"tag":653,"props":8030,"children":8032},{"className":8031},[],[8033],{"type":32,"value":7954},{"type":32,"value":8035},". If your requests take longer to load, you need to change the ",{"type":27,"tag":653,"props":8037,"children":8039},{"className":8038},[],[8040],{"type":32,"value":3225},{"type":32,"value":8042}," on ",{"type":27,"tag":653,"props":8044,"children":8046},{"className":8045},[],[8047],{"type":32,"value":8010},{"type":32,"value":8049}," command. Also, ",{"type":27,"tag":653,"props":8051,"children":8053},{"className":8052},[],[8054],{"type":32,"value":8010},{"type":32,"value":8056}," will only wait for the request to trigger, but will not actually wait for the response. Cypress’ built-in retry-ability can help us though, because we can check the last request status code by referencing the last request:",{"type":27,"tag":793,"props":8058,"children":8061},{"className":8059,"code":8060,"language":3520,"meta":5},[3517],"it('testing todos', () => {\n  cy.intercept('**').as('requests')\n  cy.visit('/')\n  // set longer timeout\n  cy.get('@requests.all', { timeout: 30000 })\n    .should('have.length', 10)\n    .its('9.response.statusCode') // check last request\n    .should('eq', 200)\n})\n",[8062],{"type":27,"tag":653,"props":8063,"children":8064},{"__ignoreMap":5},[8065],{"type":32,"value":8060},{"type":27,"tag":45,"props":8067,"children":8069},{"id":8068},"waiting-for-dom",[8070],{"type":32,"value":8071},"Waiting for DOM",{"type":27,"tag":28,"props":8073,"children":8074},{},[8075],{"type":32,"value":8076},"Another way of making sure that the application is fully loaded is to take a look into the DOM. This way we can actually assure that we don’t end up doing something we would not expect from a real user.",{"type":27,"tag":28,"props":8078,"children":8079},{},[8080],{"type":32,"value":8081},"For example, in real world you would probably not interact with an app while there’s still a \"loading\" animation still overlaid on page. You can add a guard for your test to make sure the loader disappears before you interact with the page.",{"type":27,"tag":793,"props":8083,"children":8086},{"className":8084,"code":8085,"language":3520,"meta":5},[3517],"it('testing todos', () => {\n  cy.visit('/')\n  // page loading, wait for loader to disappear\n  cy.get('.loader')\n    .should('not.exist')\n\n  // continue with our test\n})\n",[8087],{"type":27,"tag":653,"props":8088,"children":8089},{"__ignoreMap":5},[8090],{"type":32,"value":8085},{"type":27,"tag":28,"props":8092,"children":8093},{},[8094,8096,8102,8104,8110],{"type":32,"value":8095},"Negative assertions can be a little tricky, and depending on the page speed or way of how the ",{"type":27,"tag":653,"props":8097,"children":8099},{"className":8098},[],[8100],{"type":32,"value":8101},".loader",{"type":32,"value":8103}," animator works, we might want to add ",{"type":27,"tag":653,"props":8105,"children":8107},{"className":8106},[],[8108],{"type":32,"value":8109},".should('be.visible')",{"type":32,"value":8111}," before we assert on the element’s non-existence.",{"type":27,"tag":28,"props":8113,"children":8114},{},[8115,8117,8122,8124,8129,8131,8137],{"type":32,"value":8116},"To flip this, we can instead just use ",{"type":27,"tag":653,"props":8118,"children":8120},{"className":8119},[],[8121],{"type":32,"value":8010},{"type":32,"value":8123}," for the element we want to first interact with. If that takes longer time to appear, a ",{"type":27,"tag":653,"props":8125,"children":8127},{"className":8126},[],[8128],{"type":32,"value":3225},{"type":32,"value":8130}," can be added. You can read more about timeouts and waiting in ",{"type":27,"tag":172,"props":8132,"children":8134},{"href":8133},"/waiting-in-cypress-and-how-to-avoid-it",[8135],{"type":32,"value":8136},"one of my previous blogposts",{"type":32,"value":256},{"type":27,"tag":45,"props":8139,"children":8141},{"id":8140},"default-timeout",[8142],{"type":32,"value":8143},"Default timeout",{"type":27,"tag":28,"props":8145,"children":8146},{},[8147,8149,8154,8156,8161,8163,8168,8170,8176,8178,8185],{"type":32,"value":8148},"Sometimes, keeping things simple is the best wait. Every ",{"type":27,"tag":653,"props":8150,"children":8152},{"className":8151},[],[8153],{"type":32,"value":6098},{"type":32,"value":8155}," will wait for 60 seconds for the ",{"type":27,"tag":653,"props":8157,"children":8159},{"className":8158},[],[8160],{"type":32,"value":7765},{"type":32,"value":8162}," event to be fired. This is quite a long time, since real users usually don’t have such patience. If the ",{"type":27,"tag":653,"props":8164,"children":8166},{"className":8165},[],[8167],{"type":32,"value":6098},{"type":32,"value":8169}," command fails, you should probably fix the application itself and use this timeout as a performance benchmark. Alternatively, you can lower the timeout by changing the ",{"type":27,"tag":653,"props":8171,"children":8173},{"className":8172},[],[8174],{"type":32,"value":8175},"pageLoadTimeout",{"type":32,"value":8177}," option in your configuration. Read about different ",{"type":27,"tag":172,"props":8179,"children":8182},{"href":8180,"rel":8181},"https://docs.cypress.io/guides/references/configuration#Timeouts",[696],[8183],{"type":32,"value":8184},"timeouts in Cypress docs",{"type":32,"value":256},{"type":27,"tag":8187,"props":8188,"children":8189},"hr",{},[],{"type":27,"tag":28,"props":8191,"children":8192},{},[8193,8195,8200,8201,8206,8208,8215],{"type":32,"value":8194},"If you read this far, you might be interested in following me on ",{"type":27,"tag":172,"props":8196,"children":8198},{"href":5770,"rel":8197},[696],[8199],{"type":32,"value":1589},{"type":32,"value":3372},{"type":27,"tag":172,"props":8202,"children":8204},{"href":5777,"rel":8203},[696],[8205],{"type":32,"value":1598},{"type":32,"value":8207},", or on ",{"type":27,"tag":172,"props":8209,"children":8212},{"href":8210,"rel":8211},"https://www.youtube.com/@filip_hric",[696],[8213],{"type":32,"value":8214},"YouTube",{"type":32,"value":8216},". If you want to be notified about my new blogs and workshops, I recommend subscribing to my newsletter at the bottom of this page.",{"title":5,"searchDepth":320,"depth":320,"links":8218},[8219,8220,8221,8222],{"id":7738,"depth":320,"text":7741},{"id":7926,"depth":320,"text":7929},{"id":8068,"depth":320,"text":8071},{"id":8140,"depth":320,"text":8143},"content:how-to-wait-for-page-to-load-in-cypress:index.md","how-to-wait-for-page-to-load-in-cypress/index.md","how-to-wait-for-page-to-load-in-cypress/index",{"_path":8227,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":8228,"description":8229,"date":8230,"published":10,"slug":8231,"tags":8232,"cypressVersion":5959,"readingTime":8236,"body":8241,"_type":329,"_id":9836,"_source":331,"_file":9837,"_stem":9838,"_extension":334},"/how-to-structure-a-big-project-in-cypress","How to structure a big project in Cypress","Opinion on how a project with +2000 tests should be structured in order to achieve good maintainability, speed and lower the risk of introducing regressions.","2023-02-01","how-to-structure-a-big-project-in-cypress",[5279,8233,8234,8235],"project","structuring","library",{"text":8237,"minutes":8238,"time":8239,"words":8240},"23 min read",22.165,1329900,4433,{"type":24,"children":8242,"toc":9809},[8243,8248,8254,8259,8277,8287,8297,8307,8312,8329,8335,8340,8354],{"type":27,"tag":28,"props":8244,"children":8245},{},[8246],{"type":32,"value":8247},"Cypress will give you a project structure out of the box, but as the project grows, there are different files introduced into it that need their place. Also, there’s an ever-lasting debate on whether use page-objects, and if not, what should be the alternative. In this blogpost I would like to share my view on how a successful project should be created and structured. This is based on my almost 7 years of experience building different projects with Cypress.",{"type":27,"tag":45,"props":8249,"children":8251},{"id":8250},"fundamentals-and-principles",[8252],{"type":32,"value":8253},"Fundamentals and principles",{"type":27,"tag":28,"props":8255,"children":8256},{},[8257],{"type":32,"value":8258},"First of all, let’s talk about some of the principles on which my thoughts are based on. These formed my decisions in my previous projects and that were vital to making the project successful. In other words, not everything mentioned here may apply to every project. It is kind of given, but I still want to mention this. Mostly to avoid \"this would not work for us\" responses. So here they are:",{"type":27,"tag":28,"props":8260,"children":8261},{},[8262,8267,8268,8275],{"type":27,"tag":79,"props":8263,"children":8264},{},[8265],{"type":32,"value":8266},"QA automation should be a part of source code repository.",{"type":32,"value":7660},{"type":27,"tag":172,"props":8269,"children":8272},{"href":8270,"rel":8271},"https://www.linkedin.com/posts/filip-hric_im-really-curious-i-have-an-assumption-activity-7028758343657648128-eKa2",[696],[8273],{"type":32,"value":8274},"I recently made a poll on LinkedIn",{"type":32,"value":8276}," and found out that 45% of responders do not have their test suite in the same repo. In my opinion test automation code, (and especially when using Cypress), should not be detached from source code of the tested application. This keeps all tests and all branches in sync with development, makes it easier to do continuous delivery. This also means that developers are invested in creating and maintaining test automation as well as testers.",{"type":27,"tag":28,"props":8278,"children":8279},{},[8280,8285],{"type":27,"tag":79,"props":8281,"children":8282},{},[8283],{"type":32,"value":8284},"Readability is (the most) important decision maker.",{"type":32,"value":8286}," Having a failed test might not be enough to identify what went wrong. Testers are information providers. This means that if a failed test does not provide enough information on what happened or why it happened, that tester did not do a good job. When writing a test, readability of the test should drive every test design decision.",{"type":27,"tag":28,"props":8288,"children":8289},{},[8290,8295],{"type":27,"tag":79,"props":8291,"children":8292},{},[8293],{"type":32,"value":8294},"Testing should improve speed of delivery.",{"type":32,"value":8296}," Our users don’t care about how fancy our tests are, but what value does the product bring to them. For a successful company it is important to bring that value and to bring it fast. What this means for testing? It should start early and test automation has to be as fast as possible. Slow debugging and slow testing means slow delivery, that’s why test automation should be optimized for speed.",{"type":27,"tag":28,"props":8298,"children":8299},{},[8300,8305],{"type":27,"tag":79,"props":8301,"children":8302},{},[8303],{"type":32,"value":8304},"Human time is more expensive than machine time.",{"type":32,"value":8306}," Saving costs on CI is generally a good thing and writing test automation can speed things up. But there’s a limit to how much time you want to spend on it. It is important to choose your battles and make sure that creating a test automation will actually save you time. Everything you build will need maintenance, and it’s important to think about that, especially when deciding between building a tool vs. paying for a solution.",{"type":27,"tag":28,"props":8308,"children":8309},{},[8310],{"type":32,"value":8311},"If you find yourself disagreeing with these principles, it’s OK. This does not mean that I think you are making bad decisions with your tests. I’m sure you are doing good, but maybe working with a different context. As a result, this forms different principles and drives different decisions. This will always be a reality in tech, especially given how diverse are applications of tech.",{"type":27,"tag":1029,"props":8313,"children":8314},{},[8315],{"type":27,"tag":28,"props":8316,"children":8317},{},[8318,8320,8327],{"type":32,"value":8319},"IMPORTANT NOTE: Having said all this, I’m not 100% set on everything. As time goes by and I gain more experience, I adjust my views and apply different principles. Feel free to ",{"type":27,"tag":172,"props":8321,"children":8324},{"href":8322,"rel":8323},"https://discord.com/invite/3MdvPfT",[696],[8325],{"type":32,"value":8326},"discuss this with me on Discord",{"type":32,"value":8328},", I will be happy to learn about your insights to this.",{"type":27,"tag":45,"props":8330,"children":8332},{"id":8331},"bdd-without-cucumber",[8333],{"type":32,"value":8334},"BDD without Cucumber",{"type":27,"tag":28,"props":8336,"children":8337},{},[8338],{"type":32,"value":8339},"Tests I write often reflect a certain behavior or describe how a certain feature is used. When I started creating test automation, we had 15 most important scenarios written down with my colleague and we would race each other into who completes them faster (she - manually, or me running test automation).",{"type":27,"tag":28,"props":8341,"children":8342},{},[8343,8345,8352],{"type":32,"value":8344},"This has basically made our test automation behavior driven, although we have never decided to go with Gherkin syntax or the Cucumber framework. The simplicity of Cypress commands was good enough solution for us and made it quite apparent what the test is doing. Early enough, I have found this tweet by ",{"type":27,"tag":172,"props":8346,"children":8349},{"href":8347,"rel":8348},"https://twitter.com/kentcdodds",[696],[8350],{"type":32,"value":8351},"Kent C. Dodds",{"type":32,"value":8353}," that we decided to live by:",{"type":27,"tag":5872,"props":8355,"children":8357},{"id":8356},"977018512689455106",[8358,8363,8372,8385,8390,8395,8404,8410,8415,8424,8443,8448,8453,8459,8471,8481,8491,8504,8512,8535,8572,8580,8586,8591,8631,8649,8655,8677,8683,8688,8697,8710,8719,8732,8751,8757,8762,8781,8791,8797,8816,8830,8844,8858,8864,8869,8887,8893,8905,8911,8946,8952,8957,8966,8972,8977,8987,8999,9004,9013,9033,9038,9058,9067,9095,9105,9110,9115,9120,9129,9149,9158,9170,9179,9184,9193,9214,9219,9230,9239,9244,9253,9257,9269,9274,9287,9310,9316,9344,9353,9359,9371,9381,9394,9400,9413,9425,9434,9477,9482,9491,9497,9545,9550,9559,9564,9573,9578,9590,9610,9619,9641,9653,9663,9669,9688,9697,9710,9716,9721,9726,9744,9749,9754,9759,9768,9772,9777,9786],{"type":27,"tag":28,"props":8359,"children":8360},{},[8361],{"type":32,"value":8362},"This meant we wanted our tests to follow a certain scenario and then grouped scenarios into features. A result of such test structure looked something like this:",{"type":27,"tag":793,"props":8364,"children":8367},{"className":8365,"code":8366,"language":2042,"meta":5},[2040],"cypress/\n|-- e2e/\n|    |-- board/\n|    `-- list/\n|-- fixtures/\n`-- support/\n",[8368],{"type":27,"tag":653,"props":8369,"children":8370},{"__ignoreMap":5},[8371],{"type":32,"value":8366},{"type":27,"tag":28,"props":8373,"children":8374},{},[8375,8377,8383],{"type":32,"value":8376},"Folders represent a certain feature and spec files inside those folders would represent a behavior or a user story that this scenario would cover. As mentioned, these scenarios represent real user behavior, but are not written in Gherkin syntax (Given, When, Then) or using Cucumber framework. Although open source community around Cypress ",{"type":27,"tag":172,"props":8378,"children":8380},{"href":6580,"rel":8379},[696],[8381],{"type":32,"value":8382},"has created a Cucumber preprocessor",{"type":32,"value":8384}," that allows you to write your test like this, I generally lean away from this solution.",{"type":27,"tag":28,"props":8386,"children":8387},{},[8388],{"type":32,"value":8389},"In my opinion it adds an unnecessary layer inbetween test code and the application. This slows down test creation and adds unnecessary maintenance. Before you can write a complete test, every step definition needs to be created first. Every new step needs at least one new step definition unless we are creating just a different combination of existing steps. But in that case, there’s no real value in adding such test to existing test suite as all steps have already been covered.",{"type":27,"tag":28,"props":8391,"children":8392},{},[8393],{"type":32,"value":8394},"That being said, BDD approach has brought a focus on user behavior, which I definitely agree with. In my opinion, Cypress commands are already behavior focused, as most of the commands read like a sentence:",{"type":27,"tag":793,"props":8396,"children":8399},{"className":8397,"code":8398,"language":1513,"meta":5},[1510],"  cy.visit('/my-page')\n  cy.get('#element').click()\n",[8400],{"type":27,"tag":653,"props":8401,"children":8402},{"__ignoreMap":5},[8403],{"type":32,"value":8398},{"type":27,"tag":45,"props":8405,"children":8407},{"id":8406},"arrange-act-assert",[8408],{"type":32,"value":8409},"Arrange, Act, Assert",{"type":27,"tag":28,"props":8411,"children":8412},{},[8413],{"type":32,"value":8414},"Rather than going with \"Given-When-Then\" approach, I like to go with Arrange-Act-Assert. They are very similar in their fundamentals, but I feel like the latter approach defines the testing goal more clearly. \"When\" keyword in the Gherkin-style syntax seems a little bit ambiguous as it is not always clear if it refers to an action or to a state. The \"Arrange-Act-Assert\" pattern makes it clearly.",{"type":27,"tag":793,"props":8416,"children":8419},{"className":8417,"code":8418,"language":3520,"meta":5},[3517],"before( () => {\n  // arrange\n  cy.request('POST', '/api/lists', { name: 'new list' })\n})\nit('creates an item', () => {\n  // act\n  cy.visit('/')\n  cy.get('#create').type('list item{enter}')\n  // assert\n  cy.get('[data-cy=item]').should('be.visible')\n})\n",[8420],{"type":27,"tag":653,"props":8421,"children":8422},{"__ignoreMap":5},[8423],{"type":32,"value":8418},{"type":27,"tag":28,"props":8425,"children":8426},{},[8427,8429,8435,8436,8441],{"type":32,"value":8428},"Usually, the \"Arrange\" part happens via API calls, or database setup and rarely via UI. More often than not, this step takes place in ",{"type":27,"tag":653,"props":8430,"children":8432},{"className":8431},[],[8433],{"type":32,"value":8434},"before()",{"type":32,"value":1591},{"type":27,"tag":653,"props":8437,"children":8439},{"className":8438},[],[8440],{"type":32,"value":7243},{"type":32,"value":8442}," hook.",{"type":27,"tag":28,"props":8444,"children":8445},{},[8446],{"type":32,"value":8447},"When it is hard to decide whether a part of test should be done via UI or API, the Arrange-Act-Assert pattern helps on deciding. Everything that is done via UI is part of \"Act\" step. Everything before that goes into \"Arrange\" part and is not done via UI.",{"type":27,"tag":28,"props":8449,"children":8450},{},[8451],{"type":32,"value":8452},"\"Act\" and \"Assert\" steps can happen multiple times during end-to-end test.",{"type":27,"tag":45,"props":8454,"children":8456},{"id":8455},"test-annotation",[8457],{"type":32,"value":8458},"Test annotation",{"type":27,"tag":28,"props":8460,"children":8461},{},[8462,8464,8469],{"type":32,"value":8463},"As mentioned at the beginning, readability is really important. This helps navigation through tests simple. While it may seem like a small detail, it is actually really valuable when you need to debug a test. When writing an ",{"type":27,"tag":653,"props":8465,"children":8467},{"className":8466},[],[8468],{"type":32,"value":7187},{"type":32,"value":8470}," block, the name of the test should give you enough information about what is the test scenario. Some good and bad examples:",{"type":27,"tag":793,"props":8472,"children":8476},{"className":8473,"code":8474,"filename":8475,"language":1513,"meta":5},[1510],"it('board is visible', () => {})\nit('works in edge cases', () => {})\nit('handles input', () => {})\n","❌ don’t do it like this",[8477],{"type":27,"tag":653,"props":8478,"children":8479},{"__ignoreMap":5},[8480],{"type":32,"value":8474},{"type":27,"tag":793,"props":8482,"children":8486},{"className":8483,"code":8484,"filename":8485,"language":1513,"meta":5},[1510],"it('creates a board and navigates to board detail', () => {})\nit('throws error when trying to access private board', () => {})\nit('shows a warning message when input is empty', () => {})\n","✅ much better",[8487],{"type":27,"tag":653,"props":8488,"children":8489},{"__ignoreMap":5},[8490],{"type":32,"value":8484},{"type":27,"tag":28,"props":8492,"children":8493},{},[8494,8496,8502],{"type":32,"value":8495},"Ideally you should write your test title in such a way that you can imagine what the test is doing without looking into its content. Once my colleague recommended that the ",{"type":27,"tag":653,"props":8497,"children":8499},{"className":8498},[],[8500],{"type":32,"value":8501},"it",{"type":32,"value":8503}," and the name of the test should read like a sentence. I really like this approach although I have found cases where that was counterproductive and would push me into weird test namings.",{"type":27,"tag":1029,"props":8505,"children":8506},{},[8507],{"type":27,"tag":28,"props":8508,"children":8509},{},[8510],{"type":32,"value":8511},"Don’t overcomplicate stuff and if a rule needs to be broken, break it.",{"type":27,"tag":28,"props":8513,"children":8514},{},[8515,8517,8524,8526,8533],{"type":32,"value":8516},"Another useful way of making tests more readable is to add your own custom logs. Gleb Bahmutov has a ",{"type":27,"tag":172,"props":8518,"children":8521},{"href":8519,"rel":8520},"https://github.com/bahmutov/cypress-log-to-term",[696],[8522],{"type":32,"value":8523},"useful plugin for logging into terminal",{"type":32,"value":8525},", which can be definitely help with test annotation. I personally like to add steps into my tests to annotate the test scenario. ",{"type":27,"tag":172,"props":8527,"children":8530},{"href":8528,"rel":8529},"https://www.npmjs.com/package/cypress-plugin-steps",[696],[8531],{"type":32,"value":8532},"I have created a plugin",{"type":32,"value":8534}," which does the following:",{"type":27,"tag":105,"props":8536,"children":8537},{},[8538,8551,8562,8567],{"type":27,"tag":109,"props":8539,"children":8540},{},[8541,8543,8549],{"type":32,"value":8542},"every ",{"type":27,"tag":653,"props":8544,"children":8546},{"className":8545},[],[8547],{"type":32,"value":8548},"cy.step()",{"type":32,"value":8550}," command describes a step in a test",{"type":27,"tag":109,"props":8552,"children":8553},{},[8554,8555,8560],{"type":32,"value":8542},{"type":27,"tag":653,"props":8556,"children":8558},{"className":8557},[],[8559],{"type":32,"value":8548},{"type":32,"value":8561}," command is automatically numbered",{"type":27,"tag":109,"props":8563,"children":8564},{},[8565],{"type":32,"value":8566},"whenever a test fails a numbered list is appended to the error message",{"type":27,"tag":109,"props":8568,"children":8569},{},[8570],{"type":32,"value":8571},"the error message is printed to the terminal and the failure screenshot",{"type":27,"tag":28,"props":8573,"children":8574},{},[8575],{"type":27,"tag":959,"props":8576,"children":8579},{"alt":8577,"src":8578},"Cypress timeline with annotated steps and error output","steps_annotation_cp54tm.png",[],{"type":27,"tag":45,"props":8581,"children":8583},{"id":8582},"spec-files",[8584],{"type":32,"value":8585},"Spec files",{"type":27,"tag":28,"props":8587,"children":8588},{},[8589],{"type":32,"value":8590},"Every spec file should contain just a handful of tests. End to end tests tend to be longer, which means dealing with more lines of code. Once you have multiple longer scenarios in your spec file, it may quickly become harder to navigate through.",{"type":27,"tag":28,"props":8592,"children":8593},{},[8594,8596,8601,8603,8608,8610,8615,8617,8622,8624,8629],{"type":32,"value":8595},"I also rarely decide to use ",{"type":27,"tag":653,"props":8597,"children":8599},{"className":8598},[],[8600],{"type":32,"value":7173},{"type":32,"value":8602}," blocks, as the real value of these blocks comes in when you have at least two in a single spec file. ",{"type":27,"tag":653,"props":8604,"children":8606},{"className":8605},[],[8607],{"type":32,"value":7173},{"type":32,"value":8609}," blocks help group together tests that have something in common. Most of the time it’s ",{"type":27,"tag":653,"props":8611,"children":8613},{"className":8612},[],[8614],{"type":32,"value":8434},{"type":32,"value":8616},"or ",{"type":27,"tag":653,"props":8618,"children":8620},{"className":8619},[],[8621],{"type":32,"value":7243},{"type":32,"value":8623}," hooks. This is why in cases when I need to split the groups, I usually split them with a new spec instead of new ",{"type":27,"tag":653,"props":8625,"children":8627},{"className":8626},[],[8628],{"type":32,"value":7173},{"type":32,"value":8630}," block.",{"type":27,"tag":28,"props":8632,"children":8633},{},[8634,8636,8641,8642,8647],{"type":32,"value":8635},"However, there is a problem with this approach when you try to use \"Run all specs\" button in Cypress open mode. This mode basically creates a single spec out of multiple files, which means that all your ",{"type":27,"tag":653,"props":8637,"children":8639},{"className":8638},[],[8640],{"type":32,"value":8434},{"type":32,"value":4164},{"type":27,"tag":653,"props":8643,"children":8645},{"className":8644},[],[8646],{"type":32,"value":7243},{"type":32,"value":8648}," hooks get concatenated causing unexpected results. This is something to be aware of. When working on a big project however, I practically never run all specs through open mode.",{"type":27,"tag":45,"props":8650,"children":8652},{"id":8651},"selectors",[8653],{"type":32,"value":8654},"Selectors",{"type":27,"tag":28,"props":8656,"children":8657},{},[8658,8660,8667,8669,8675],{"type":32,"value":8659},"I have tried different approaches in the past, but I ended up going with ",{"type":27,"tag":172,"props":8661,"children":8664},{"href":8662,"rel":8663},"https://docs.cypress.io/guides/references/best-practices#Selecting-Elements",[696],[8665],{"type":32,"value":8666},"Cypress’ recommendation",{"type":32,"value":8668}," and add ",{"type":27,"tag":653,"props":8670,"children":8672},{"className":8671},[],[8673],{"type":32,"value":8674},"data-cy",{"type":32,"value":8676}," selectors into the application. This has proven to be the most stable approach. Relying on class names has always led to random failures. This was especially true these days as developers rely on UI libraries such as material design or bootstrap. Updating these can often cause a change in classes, which breaks our tests.",{"type":27,"tag":1033,"props":8678,"children":8680},{"id":8679},"knowing-what-to-select",[8681],{"type":32,"value":8682},"Knowing what to select",{"type":27,"tag":28,"props":8684,"children":8685},{},[8686],{"type":32,"value":8687},"Adding your own data attributes for selectors can help you learn more about the application you are testing. Let me demonstrate this by a simple example. Imagine that you have a button that looks like this:",{"type":27,"tag":793,"props":8689,"children":8692},{"className":8690,"code":8691,"language":7826,"meta":5},[7824],"\u003Cbutton disabled>\n  \u003Cspan>Click me!\u003C/span>\n\u003C/button>\n",[8693],{"type":27,"tag":653,"props":8694,"children":8695},{"__ignoreMap":5},[8696],{"type":32,"value":8691},{"type":27,"tag":28,"props":8698,"children":8699},{},[8700,8702,8708],{"type":32,"value":8701},"In your test you want to click in this element. Notice that the button has the ",{"type":27,"tag":653,"props":8703,"children":8705},{"className":8704},[],[8706],{"type":32,"value":8707},"disabled",{"type":32,"value":8709}," property, which means a real user would not be able to click on it. This means that targetting the correct element becomes super important:",{"type":27,"tag":793,"props":8711,"children":8714},{"className":8712,"code":8713,"language":1513,"meta":5},[1510],"// this will pass, but click will do nothing\ncy.get('span').click()\n\n// this will fail, because button is disabled\ncy.get('button').click()\n",[8715],{"type":27,"tag":653,"props":8716,"children":8717},{"__ignoreMap":5},[8718],{"type":32,"value":8713},{"type":27,"tag":28,"props":8720,"children":8721},{},[8722,8724,8730],{"type":32,"value":8723},"This is why choosing the right selector is important. When adding your own ",{"type":27,"tag":653,"props":8725,"children":8727},{"className":8726},[],[8728],{"type":32,"value":8729},"data-*",{"type":32,"value":8731}," attribute can help you understand where does the interaction in your application happen.",{"type":27,"tag":28,"props":8733,"children":8734},{},[8735,8737,8742,8744,8749],{"type":32,"value":8736},"I would also advise to add your ",{"type":27,"tag":653,"props":8738,"children":8740},{"className":8739},[],[8741],{"type":32,"value":8729},{"type":32,"value":8743}," attributes yourself even if you are a tester or not the person developing the app. You will get a better understanding of the structure of the app and also get yourself familiar with different frameworks. Also, I don’t find it particulary useful when the addition of ",{"type":27,"tag":653,"props":8745,"children":8747},{"className":8746},[],[8748],{"type":32,"value":8729},{"type":32,"value":8750}," selector is outsourced to developers. This prolongs the feedback loop and adds unnecessary overhead.",{"type":27,"tag":1033,"props":8752,"children":8754},{"id":8753},"removing-duplicity-improving-readability",[8755],{"type":32,"value":8756},"Removing duplicity, improving readability",{"type":27,"tag":28,"props":8758,"children":8759},{},[8760],{"type":32,"value":8761},"Another advantage of adding your own selector attributes to the source code is that it removes the duplicity that is created when using page objects. Same goes for storing your elements in a separate file. It is another concern to be taken care of and a de-sync between the reality of the application and tests can happen easily.",{"type":27,"tag":28,"props":8763,"children":8764},{},[8765,8767,8772,8774,8779],{"type":32,"value":8766},"Adding ",{"type":27,"tag":653,"props":8768,"children":8770},{"className":8769},[],[8771],{"type":32,"value":8729},{"type":32,"value":8773}," selectors can also help you improve readability of your tests, as you can add anything that makes sense for the test you want to write. Some people I talk to are concerned about a naming convention, but I would not worry about it. Having two of the same ",{"type":27,"tag":653,"props":8775,"children":8777},{"className":8776},[],[8778],{"type":32,"value":8729},{"type":32,"value":8780}," attributes is not a problem until they are found within the same test or within the same screen. I’d definitely advice for using selectors that represent what user sees, instead of trying to come up with a naming convention that would embrace every selector in tested app.",{"type":27,"tag":793,"props":8782,"children":8786},{"className":8783,"code":8784,"filename":8785,"language":1513,"meta":5},[1510],"// ❌ too complicated in my opinion\ncy.get('[data-cy=account-screen-sidemenu-settings-modal]')\n// ✅ much better\ncy.get('[data-cy=settings-modal]')\n","my recommendation",[8787],{"type":27,"tag":653,"props":8788,"children":8789},{"__ignoreMap":5},[8790],{"type":32,"value":8784},{"type":27,"tag":1033,"props":8792,"children":8794},{"id":8793},"selector-strategies-that-make-sense",[8795],{"type":32,"value":8796},"Selector strategies that make sense",{"type":27,"tag":28,"props":8798,"children":8799},{},[8800,8802,8807,8809,8814],{"type":32,"value":8801},"A valid concern for using ",{"type":27,"tag":653,"props":8803,"children":8805},{"className":8804},[],[8806],{"type":32,"value":8729},{"type":32,"value":8808}," attributes is a situation when this attribute gets changed or deleted. In my experience, this happens less often with this approach. This is mostly because these attributes never get changed or deleted by accident, due to a framework update or some other unrelated change. If an attribute gets deleted, it usually happens with the element as well. And in that case your test actually ",{"type":27,"tag":79,"props":8810,"children":8811},{},[8812],{"type":32,"value":8813},"should",{"type":32,"value":8815}," fail. In case of an attribute renaming, a simple \"find and replace\" action does the job.",{"type":27,"tag":28,"props":8817,"children":8818},{},[8819,8821,8828],{"type":32,"value":8820},"Another question that I see raised more often is the usage of ",{"type":27,"tag":172,"props":8822,"children":8825},{"href":8823,"rel":8824},"https://testing-library.com/docs/cypress-testing-library/intro/",[696],[8826],{"type":32,"value":8827},"Cypress testing library",{"type":32,"value":8829},". Personally I haven’t been using it a lot, but I see the value in using it. This library often pushes you to rely on accessibility attributes, which means you test two things at once. Not only you check functionality of your app, but you make sure that it’s accessible as well.",{"type":27,"tag":28,"props":8831,"children":8832},{},[8833,8835,8842],{"type":32,"value":8834},"Most importantly, you can use multiple selector strategies that complement one another. ",{"type":27,"tag":172,"props":8836,"children":8839},{"href":8837,"rel":8838},"https://css-tricks.com/front-end-test-element-locators/",[696],[8840],{"type":32,"value":8841},"This amazing blogpost by Mark Noonan",{"type":32,"value":8843}," demonstrates how different levels of testing strategies can work together to create a very stable test suite.",{"type":27,"tag":28,"props":8845,"children":8846},{},[8847,8849,8856],{"type":32,"value":8848},"I have experimented with ",{"type":27,"tag":172,"props":8850,"children":8853},{"href":8851,"rel":8852},"https://filiphric.com/autocompleting-selectors-in-cypress-with-typescript",[696],[8854],{"type":32,"value":8855},"mapping all of the selectors and making them autocomplete",{"type":32,"value":8857},", but this strategy does in fact rely on a single type definition file which stores all selectors. In theory I can imagine that this might work, but but currently I’m more leaning to this strategy being a dead end. But you be the judge.",{"type":27,"tag":45,"props":8859,"children":8861},{"id":8860},"custom-commands",[8862],{"type":32,"value":8863},"Custom commands",{"type":27,"tag":28,"props":8865,"children":8866},{},[8867],{"type":32,"value":8868},"Custom commands are one of the most powerful features of Cypress. The fact that you can expand your library makes Cypress ecosystem exceptionaly versatile. I usually use three categories of custom commands:",{"type":27,"tag":105,"props":8870,"children":8871},{},[8872,8877,8882],{"type":27,"tag":109,"props":8873,"children":8874},{},[8875],{"type":32,"value":8876},"utility commands",{"type":27,"tag":109,"props":8878,"children":8879},{},[8880],{"type":32,"value":8881},"API calls",{"type":27,"tag":109,"props":8883,"children":8884},{},[8885],{"type":32,"value":8886},"action sequences",{"type":27,"tag":1033,"props":8888,"children":8890},{"id":8889},"custom-api-actions",[8891],{"type":32,"value":8892},"Custom API actions",{"type":27,"tag":28,"props":8894,"children":8895},{},[8896,8898,8903],{"type":32,"value":8897},"With Cypress, you will often times find yourself calling an API to either set up your data or to do some action in your app. Since you may not always want to call ",{"type":27,"tag":653,"props":8899,"children":8901},{"className":8900},[],[8902],{"type":32,"value":6105},{"type":32,"value":8904}," and provide needed authorization, headers or request body, creating custom command seems like a good idea. You can create a function that will take care of default values, or you can pass different arguments to alter the behavior. Data used from API call can be later used in test or can be processed within the test.",{"type":27,"tag":1033,"props":8906,"children":8908},{"id":8907},"utility-commands",[8909],{"type":32,"value":8910},"Utility commands",{"type":27,"tag":28,"props":8912,"children":8913},{},[8914,8916,8921,8923,8929,8931,8937,8938,8944],{"type":32,"value":8915},"If you use ",{"type":27,"tag":653,"props":8917,"children":8919},{"className":8918},[],[8920],{"type":32,"value":8729},{"type":32,"value":8922}," selectors, creating a ",{"type":27,"tag":653,"props":8924,"children":8926},{"className":8925},[],[8927],{"type":32,"value":8928},"cy.getByDataCy()",{"type":32,"value":8930}," command might be useful. Utility commands usually take care of some niche case within an application. Some examples include ",{"type":27,"tag":653,"props":8932,"children":8934},{"className":8933},[],[8935],{"type":32,"value":8936},"cy.getClipboard()",{"type":32,"value":3372},{"type":27,"tag":653,"props":8939,"children":8941},{"className":8940},[],[8942],{"type":32,"value":8943},"cy.getTooltip()",{"type":32,"value":8945}," and so on.",{"type":27,"tag":1033,"props":8947,"children":8949},{"id":8948},"action-sequences",[8950],{"type":32,"value":8951},"Action sequences",{"type":27,"tag":28,"props":8953,"children":8954},{},[8955],{"type":32,"value":8956},"Action sequences resemble the traditional page object model the most. These are series of UI steps that cannot really be avoided by calling an API. Most of the time they deal with situations that take multiple steps and are essential for the test flow. An example might looks something like this:",{"type":27,"tag":793,"props":8958,"children":8961},{"className":8959,"code":8960,"language":3520,"meta":5},[3517],"Cypress.Commands.add('pickSidebarItem', (item: 'Settings' | 'Account' | 'My profile' | 'Log out') => {\n\n  cy.get('[data-cy=hamburger-menu]')\n    .click()\n\n  cy.contains('[data-cy=side-menu]', item)\n    .click()\n\n})\n",[8962],{"type":27,"tag":653,"props":8963,"children":8964},{"__ignoreMap":5},[8965],{"type":32,"value":8960},{"type":27,"tag":1033,"props":8967,"children":8969},{"id":8968},"organizing-custom-commands",[8970],{"type":32,"value":8971},"Organizing custom commands",{"type":27,"tag":28,"props":8973,"children":8974},{},[8975],{"type":32,"value":8976},"My rule of thumb is to put every custom command into it’s own file and add them to their own folder in the Cypress project:",{"type":27,"tag":793,"props":8978,"children":8982},{"className":8979,"code":8980,"highlights":8981,"language":2042,"meta":5},[2040],"big-project/\n|-- cypress/\n|   |-- commands/\n|   |-- e2e/\n|   |-- fixtures/\n|   `-- support/\n|-- .gitignore\n`-- cypress.config.ts\n",[1606],[8983],{"type":27,"tag":653,"props":8984,"children":8985},{"__ignoreMap":5},[8986],{"type":32,"value":8980},{"type":27,"tag":28,"props":8988,"children":8989},{},[8990,8991,8997],{"type":32,"value":3349},{"type":27,"tag":653,"props":8992,"children":8994},{"className":8993},[],[8995],{"type":32,"value":8996},"commands",{"type":32,"value":8998}," folder can contain categories of custom commands, but I am not strict on following this rule. Since custom commands usually have their own unique name there’s not really a big benefit for creating subfolder.",{"type":27,"tag":28,"props":9000,"children":9001},{},[9002],{"type":32,"value":9003},"Every command is in its own file and contains the command as well as the TypeScript definition. An example of such command looks like this:",{"type":27,"tag":793,"props":9005,"children":9008},{"className":9006,"code":9007,"language":3520,"meta":5},[3517],"declare global {\n  namespace Cypress {\n    interface Chainable {\n      addBoardApi: typeof addBoardApi;\n    }\n  }\n}\n\n/**\n * Creates a new board using the API\n * @param name name of the board\n * @example\n * cy.addBoardApi('new board')\n *\n */\nexport const addBoardApi = function(this: any, name: string): Cypress.Chainable\u003Cany> {\n\n  return cy\n    .request('POST', '/api/boards', { name })\n    .its('body', { log: false }).as('board');\n    \n};\n",[9009],{"type":27,"tag":653,"props":9010,"children":9011},{"__ignoreMap":5},[9012],{"type":32,"value":9007},{"type":27,"tag":28,"props":9014,"children":9015},{},[9016,9023,9025,9031],{"type":27,"tag":172,"props":9017,"children":9020},{"href":9018,"rel":9019},"https://docs.cypress.io/guides/tooling/typescript-support#Types-for-Custom-Commands",[696],[9021],{"type":32,"value":9022},"Cypress documentation recommends",{"type":32,"value":9024}," creating a central ",{"type":27,"tag":653,"props":9026,"children":9028},{"className":9027},[],[9029],{"type":32,"value":9030},"index.d.ts",{"type":32,"value":9032}," file which contains type definitions for all commands. I personally lean more to the approached shown above, as this way the type definition is contained in the same file as the command itself. This creates less confusions and it’s much easier to maintain.",{"type":27,"tag":28,"props":9034,"children":9035},{},[9036],{"type":32,"value":9037},"Every command has its own JSDoc comment that provides additional information into what the command does. This is incredibly useful for anyone new who joins the team. It also keeps the code self-documented and can point to useful links e.g. to internal wiki.",{"type":27,"tag":28,"props":9039,"children":9040},{},[9041,9043,9049,9051,9057],{"type":32,"value":9042},"Instead of using ",{"type":27,"tag":653,"props":9044,"children":9046},{"className":9045},[],[9047],{"type":32,"value":9048},"Cypress.Commands",{"type":32,"value":9050}," API, each command is written as a function, and then imported into ",{"type":27,"tag":653,"props":9052,"children":9054},{"className":9053},[],[9055],{"type":32,"value":9056},"cypress/support/e2e.ts",{"type":32,"value":786},{"type":27,"tag":793,"props":9059,"children":9062},{"className":9060,"code":9061,"filename":9056,"language":3520,"meta":5},[3517],"import { addBoardApi } from '../commands/addBoardApi'\n\nCypress.Commands.addAll({ addBoardApi })\n",[9063],{"type":27,"tag":653,"props":9064,"children":9065},{"__ignoreMap":5},[9066],{"type":32,"value":9061},{"type":27,"tag":28,"props":9068,"children":9069},{},[9070,9072,9078,9080,9086,9088,9093],{"type":32,"value":9071},"Another approach that I tend to use is to have an ",{"type":27,"tag":653,"props":9073,"children":9075},{"className":9074},[],[9076],{"type":32,"value":9077},"index.ts",{"type":32,"value":9079}," file that adds all the imports from ",{"type":27,"tag":653,"props":9081,"children":9083},{"className":9082},[],[9084],{"type":32,"value":9085},"cypress/commands",{"type":32,"value":9087}," folder and import that to ",{"type":27,"tag":653,"props":9089,"children":9091},{"className":9090},[],[9092],{"type":32,"value":9056},{"type":32,"value":9094}," instead. This is useful if you decide to move your app into monorepo and add your custom commands into separate library so that it can be reused across your projects.",{"type":27,"tag":793,"props":9096,"children":9100},{"className":9097,"code":9098,"highlights":9099,"language":2042,"meta":5},[2040],"monorepo-project/\n|-- node_modules/\n|-- packages/\n|   |-- commands/       // library\n|   |-- trelloapp/      // app\n|   `-- trelloapp-e2e/  // tests\n|-- tools/\n|-- .editorconfig\n|-- .eslintrc.json\n|-- .gitignore\n|-- .prettierignore\n|-- .prettierrc\n|-- nx.json\n|-- package-lock.json\n`-- package.jso\n",[3877,3667,3809],[9101],{"type":27,"tag":653,"props":9102,"children":9103},{"__ignoreMap":5},[9104],{"type":32,"value":9098},{"type":27,"tag":45,"props":9106,"children":9107},{"id":2608},[9108],{"type":32,"value":9109},"TypeScript",{"type":27,"tag":28,"props":9111,"children":9112},{},[9113],{"type":32,"value":9114},"All my projects use TypeScript. The implementation of TypeScript into an existing JS project is super easy as it can be done gradually. TypeScript errors don’t actually affect your tests, but can help you find errors. TypeScript guides you while you are writing tests by providing autocompletion, checking of the parameters that you pass into your commands and much more.",{"type":27,"tag":28,"props":9116,"children":9117},{},[9118],{"type":32,"value":9119},"TypeScript also works really well with Custom Commands. One way of how you can use TypeScript in your custom commands is to reuse types from your source code in your tests:",{"type":27,"tag":793,"props":9121,"children":9124},{"className":9122,"code":9123,"language":3520,"meta":5},[3517],"import Board from '@/src/models'\n\ncy.request\u003CBoard>('POST', '/api/boards', { name: 'new board' })\n",[9125],{"type":27,"tag":653,"props":9126,"children":9127},{"__ignoreMap":5},[9128],{"type":32,"value":9123},{"type":27,"tag":28,"props":9130,"children":9131},{},[9132,9134,9139,9141,9147],{"type":32,"value":9133},"The code example above shows ",{"type":27,"tag":653,"props":9135,"children":9137},{"className":9136},[],[9138],{"type":32,"value":6105},{"type":32,"value":9140}," command that will return types from ",{"type":27,"tag":653,"props":9142,"children":9144},{"className":9143},[],[9145],{"type":32,"value":9146},"Board",{"type":32,"value":9148}," interface imported from source code. This means that if you have an interface like this:",{"type":27,"tag":793,"props":9150,"children":9153},{"className":9151,"code":9152,"language":3520,"meta":5},[3517],"interface Board {\n  id: number;\n  starred: boolean;\n  name: string;\n  created: string;\n  user: number;\n}\n\nexport default Board;\n",[9154],{"type":27,"tag":653,"props":9155,"children":9156},{"__ignoreMap":5},[9157],{"type":32,"value":9152},{"type":27,"tag":28,"props":9159,"children":9160},{},[9161,9163,9168],{"type":32,"value":9162},"You will be able to spot a TypeScript error if you decide to write a test for something that is not part of the ",{"type":27,"tag":653,"props":9164,"children":9166},{"className":9165},[],[9167],{"type":32,"value":9146},{"type":32,"value":9169}," interface.",{"type":27,"tag":793,"props":9171,"children":9174},{"className":9172,"code":9173,"language":3520,"meta":5},[3517],"import Board from '@/src/models'\n\ncy.request\u003CBoard>('POST', '/api/boards', { name: 'new board' })\n  .then(({ body }) => {\n    // the \"key\" will be underlined in editor\n    expect(body.key).to.be.a('number')\n  })\n",[9175],{"type":27,"tag":653,"props":9176,"children":9177},{"__ignoreMap":5},[9178],{"type":32,"value":9173},{"type":27,"tag":28,"props":9180,"children":9181},{},[9182],{"type":32,"value":9183},"In addition to checking your code in your editor, you can set up a lint check that will make sure you don’t have any TypeScript errors in your codebase:",{"type":27,"tag":793,"props":9185,"children":9188},{"className":9186,"code":9187,"filename":734,"language":1004,"meta":5},[1002],"\"scripts\": {\n  \"lint\": \"tsc --noEmit\"\n}\n",[9189],{"type":27,"tag":653,"props":9190,"children":9191},{"__ignoreMap":5},[9192],{"type":32,"value":9187},{"type":27,"tag":28,"props":9194,"children":9195},{},[9196,9198,9204,9206,9212],{"type":32,"value":9197},"Running ",{"type":27,"tag":653,"props":9199,"children":9201},{"className":9200},[],[9202],{"type":32,"value":9203},"npm run lint",{"type":32,"value":9205}," command will ensure that any TypeScript error introduced by latest changes will be caught early. You can make this ",{"type":27,"tag":653,"props":9207,"children":9209},{"className":9208},[],[9210],{"type":32,"value":9211},"lint",{"type":32,"value":9213}," step run as a pre-commit hook and even prevent commiting such code. This check will take just a couple of seconds.",{"type":27,"tag":28,"props":9215,"children":9216},{},[9217],{"type":32,"value":9218},"The biggest advantage though, is that it creates a two-way sync between source code and your tests. Not only it will make sure that your tests are typed correctly, but whenever a change is introduced on the source code side that changes types, it will affect the tests.",{"type":27,"tag":28,"props":9220,"children":9221},{},[9222,9224],{"type":32,"value":9223},"A nice bonus in the TypeScript world is the ability to define paths. This remove the headache of resolving relative paths in your project. Let’s say you have a path defined in your ",{"type":27,"tag":653,"props":9225,"children":9227},{"className":9226},[],[9228],{"type":32,"value":9229},"tsconfig.json",{"type":27,"tag":793,"props":9231,"children":9234},{"className":9232,"code":9233,"filename":9229,"language":1004,"meta":5},[1002],"{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"es5\", \"dom\"],\n    \"types\": [\"cypress\",\"node\"],\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@fixtures/*\": [\n        \"cypress/fixtures/*\"\n      ]\n    },\n    \"resolveJsonModule\": true,\n  }  \n}\n",[9235],{"type":27,"tag":653,"props":9236,"children":9237},{"__ignoreMap":5},[9238],{"type":32,"value":9233},{"type":27,"tag":28,"props":9240,"children":9241},{},[9242],{"type":32,"value":9243},"You can import a fixture file in your test like this:",{"type":27,"tag":793,"props":9245,"children":9248},{"className":9246,"code":9247,"language":3520,"meta":5},[3517],"import boardSchema from '@fixtures/boardSchema.json'\n\nit('board returns proper JSON schema', () => {\n\n  cy.api({\n    url: `/api/boards/1`\n  }).its('body')\n  .should('jsonSchema', boardSchema)\n  \n})\n",[9249],{"type":27,"tag":653,"props":9250,"children":9251},{"__ignoreMap":5},[9252],{"type":32,"value":9247},{"type":27,"tag":45,"props":9254,"children":9255},{"id":3921},[9256],{"type":32,"value":3924},{"type":27,"tag":28,"props":9258,"children":9259},{},[9260,9262,9267],{"type":32,"value":9261},"I’ve explained how ",{"type":27,"tag":172,"props":9263,"children":9264},{"href":3932},[9265],{"type":32,"value":9266},"code coverage works in the past",{"type":32,"value":9268},". It is a really powerful tool. While you may find some teams that aim for 100% code coverage, I don’t find this particularly useful. Code coverage report can serve as a map of of the applications \"landscape\" and can navigate you to unexplored areas.",{"type":27,"tag":28,"props":9270,"children":9271},{},[9272],{"type":32,"value":9273},"Usually business cases are covered first, but there are many edge cases which often get forgotten and can cause our users’ disappointment. Code coverage helps finding those areas.",{"type":27,"tag":28,"props":9275,"children":9276},{},[9277,9279,9285],{"type":32,"value":9278},"Code coverage requires an instrumented version of the app, which requires a separate build. This can often be solved by creating a custom step in the pipeline and I find ",{"type":27,"tag":172,"props":9280,"children":9282},{"href":9281},"/cypress-and-git-hub-actions-step-by-step-guide#trigger-test-run-manually",[9283],{"type":32,"value":9284},"worfkflow dispatch pipeline",{"type":32,"value":9286}," to be an ideal use case for that.",{"type":27,"tag":28,"props":9288,"children":9289},{},[9290,9292,9299,9301,9308],{"type":32,"value":9291},"The coverage reports can be saved as artifacts, or you can use ",{"type":27,"tag":172,"props":9293,"children":9296},{"href":9294,"rel":9295},"https://about.codecov.io/",[696],[9297],{"type":32,"value":9298},"a service like Codecov",{"type":32,"value":9300}," that provides really beautiful insights into your code coverage. If you want to take a look at a living example of such report, you can take a look at ",{"type":27,"tag":172,"props":9302,"children":9305},{"href":9303,"rel":9304},"https://github.com/filiphric/trelloapp-vue-vite-ts",[696],[9306],{"type":32,"value":9307},"my example repository",{"type":32,"value":9309}," with the Trello-clone app that I made.",{"type":27,"tag":45,"props":9311,"children":9313},{"id":9312},"utilities",[9314],{"type":32,"value":9315},"Utilities",{"type":27,"tag":28,"props":9317,"children":9318},{},[9319,9321,9327,9329,9335,9336,9342],{"type":32,"value":9320},"Every project is specific and comes with a bunch of common-pattern problems that need to be solved. To avoid solving the same problem multiple times, I put all my utilities into ",{"type":27,"tag":653,"props":9322,"children":9324},{"className":9323},[],[9325],{"type":32,"value":9326},"cypress/utils",{"type":32,"value":9328}," folder. This can by stuff like ",{"type":27,"tag":653,"props":9330,"children":9332},{"className":9331},[],[9333],{"type":32,"value":9334},"generateRandomUser()",{"type":32,"value":3372},{"type":27,"tag":653,"props":9337,"children":9339},{"className":9338},[],[9340],{"type":32,"value":9341},"getAuthorization()",{"type":32,"value":9343}," or some others. I usually import this right into my test instead of including them in support file. There’s usually not too many of these as Cypress comes with lodash library bundled in which is full of useful utilities.",{"type":27,"tag":793,"props":9345,"children":9348},{"className":9346,"code":9347,"language":3520,"meta":5},[3517],"// imports lodash from Cypress\nconst { _ } = Cypress\n\n// generates number between 0 and 10\nconst randomNumber = _.random(10)\n",[9349],{"type":27,"tag":653,"props":9350,"children":9351},{"__ignoreMap":5},[9352],{"type":32,"value":9347},{"type":27,"tag":45,"props":9354,"children":9356},{"id":9355},"global-hooks",[9357],{"type":32,"value":9358},"Global hooks",{"type":27,"tag":28,"props":9360,"children":9361},{},[9362,9364,9369],{"type":32,"value":9363},"There are couple of global hooks usually set up in my projects. The usual use case is taking care of cookie consent message. Adding an global ",{"type":27,"tag":653,"props":9365,"children":9367},{"className":9366},[],[9368],{"type":32,"value":7243},{"type":32,"value":9370}," hook can set up all the important cookies and prevent the message from opening in your tests.",{"type":27,"tag":793,"props":9372,"children":9376},{"className":9373,"code":9374,"filename":9375,"language":3520,"meta":5},[3517],"beforeEach(() => {\n  cy.setCookie('user_consents', '{\"marketing\":false,\"essential\":true}')\n})\n","cypress/e2e.ts",[9377],{"type":27,"tag":653,"props":9378,"children":9379},{"__ignoreMap":5},[9380],{"type":32,"value":9374},{"type":27,"tag":28,"props":9382,"children":9383},{},[9384,9386,9392],{"type":32,"value":9385},"You can always use ",{"type":27,"tag":653,"props":9387,"children":9389},{"className":9388},[],[9390],{"type":32,"value":9391},"cy.clearCookies()",{"type":32,"value":9393}," command to remove cookies in the test that tests this consent message.",{"type":27,"tag":45,"props":9395,"children":9397},{"id":9396},"tagging-tests",[9398],{"type":32,"value":9399},"Tagging tests",{"type":27,"tag":28,"props":9401,"children":9402},{},[9403,9405,9411],{"type":32,"value":9404},"As soon as the project grows, it is pretty much impossible to run all tests on every commit. Splitting test into categories can be easily achievedy by using ",{"type":27,"tag":172,"props":9406,"children":9409},{"href":9407,"rel":9408},"https://www.npmjs.com/package/@cypress/grep",[696],[9410],{"type":32,"value":7529},{"type":32,"value":9412}," plugin. It enables you to run a subset of tests based on the test name or based on tags.",{"type":27,"tag":28,"props":9414,"children":9415},{},[9416,9418,9423],{"type":32,"value":9417},"First and foremost, ",{"type":27,"tag":653,"props":9419,"children":9421},{"className":9420},[],[9422],{"type":32,"value":7416},{"type":32,"value":9424}," category is created that takes care of the most essential scenarios. The smoke set can sometimes be a separate folder, but I personally prefer for the tests to live in their own feature folders.",{"type":27,"tag":793,"props":9426,"children":9429},{"className":9427,"code":9428,"language":3520,"meta":5},[3517],"it('creates a new board', { tags: ['@smoke'] }, () => {\n  // test\n})\n",[9430],{"type":27,"tag":653,"props":9431,"children":9432},{"__ignoreMap":5},[9433],{"type":32,"value":9428},{"type":27,"tag":28,"props":9435,"children":9436},{},[9437,9439,9445,9447,9453,9455,9461,9463,9468,9470,9475],{"type":32,"value":9438},"A single test can have multiple tags, so that test can be ran based on a certain testing goal. E.g. ",{"type":27,"tag":653,"props":9440,"children":9442},{"className":9441},[],[9443],{"type":32,"value":9444},"@email",{"type":32,"value":9446}," tag to run all tests that use email validations, ",{"type":27,"tag":653,"props":9448,"children":9450},{"className":9449},[],[9451],{"type":32,"value":9452},"@mobile",{"type":32,"value":9454}," for all mobile tests, or ",{"type":27,"tag":653,"props":9456,"children":9458},{"className":9457},[],[9459],{"type":32,"value":9460},"@visual",{"type":32,"value":9462}," for all tests containing visual validations. I like to think about different situations in which we want to target a certain area of the application. For example, if CSS has changed, we might want to run all ",{"type":27,"tag":653,"props":9464,"children":9466},{"className":9465},[],[9467],{"type":32,"value":9460},{"type":32,"value":9469}," tests, or if our email testing service is not working currently, we may want to temoporarily omit ",{"type":27,"tag":653,"props":9471,"children":9473},{"className":9472},[],[9474],{"type":32,"value":9444},{"type":32,"value":9476}," test subset.",{"type":27,"tag":28,"props":9478,"children":9479},{},[9480],{"type":32,"value":9481},"In CLI, these can be ran by following command:",{"type":27,"tag":793,"props":9483,"children":9486},{"className":9484,"code":9485,"language":6276,"meta":5},[6274],"npx cypress run --env grepTags='@smoke'\n",[9487],{"type":27,"tag":653,"props":9488,"children":9489},{"__ignoreMap":5},[9490],{"type":32,"value":9485},{"type":27,"tag":45,"props":9492,"children":9494},{"id":9493},"configuration-switching",[9495],{"type":32,"value":9496},"Configuration switching",{"type":27,"tag":28,"props":9498,"children":9499},{},[9500,9502,9507,9509,9515,9517,9522,9524,9530,9532,9537,9539],{"type":32,"value":9501},"It is important that test work on multiple different environments. To make things easy, I usually create a ",{"type":27,"tag":653,"props":9503,"children":9505},{"className":9504},[],[9506],{"type":32,"value":5957},{"type":32,"value":9508}," folder that contains ",{"type":27,"tag":653,"props":9510,"children":9512},{"className":9511},[],[9513],{"type":32,"value":9514},".json",{"type":32,"value":9516}," files with all the environment-specific variables such as ",{"type":27,"tag":653,"props":9518,"children":9520},{"className":9519},[],[9521],{"type":32,"value":5995},{"type":32,"value":9523},", url of the API, or some other information that may be used during the test. These get fed into ",{"type":27,"tag":653,"props":9525,"children":9527},{"className":9526},[],[9528],{"type":32,"value":9529},"env",{"type":32,"value":9531}," object from the ",{"type":27,"tag":653,"props":9533,"children":9535},{"className":9534},[],[9536],{"type":32,"value":9514},{"type":32,"value":9538}," file and can easily be accessed by ",{"type":27,"tag":653,"props":9540,"children":9542},{"className":9541},[],[9543],{"type":32,"value":9544},"Cypress.env()",{"type":27,"tag":28,"props":9546,"children":9547},{},[9548],{"type":32,"value":9549},"The following setup will take care of adding the correct information to the project:",{"type":27,"tag":793,"props":9551,"children":9554},{"className":9552,"code":9553,"filename":5669,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  // other config attributes\n  setupNodeEvents(on, config) {\n    // if version not defined, use local\n    const version = config.env.version || 'local'\n    // load env from json\n    config.env = require(`./cypress/config/${version}.json`);\n    // change baseUrl\n    config.baseUrl = config.env.baseUrl\n\n    return config\n  }\n})\n",[9555],{"type":27,"tag":653,"props":9556,"children":9557},{"__ignoreMap":5},[9558],{"type":32,"value":9553},{"type":27,"tag":28,"props":9560,"children":9561},{},[9562],{"type":32,"value":9563},"When running a test with a different configuration, all that’s needed is to run a test like this:",{"type":27,"tag":793,"props":9565,"children":9568},{"className":9566,"code":9567,"language":6276,"meta":5},[6274],"npx cypress open --env version=\"production\"\n",[9569],{"type":27,"tag":653,"props":9570,"children":9571},{"__ignoreMap":5},[9572],{"type":32,"value":9567},{"type":27,"tag":28,"props":9574,"children":9575},{},[9576],{"type":32,"value":9577},"and Cypress will load all the variables needed.",{"type":27,"tag":28,"props":9579,"children":9580},{},[9581,9583,9588],{"type":32,"value":9582},"Besides having the configuration set up in separate ",{"type":27,"tag":653,"props":9584,"children":9586},{"className":9585},[],[9587],{"type":32,"value":9514},{"type":32,"value":9589},", there is information that should not be commited to the repositories, like passwords, api keys, etc. These are usually part of environment and are passed through CLI.",{"type":27,"tag":28,"props":9591,"children":9592},{},[9593,9595,9602,9604,9609],{"type":32,"value":9594},"To make things easier, I use ",{"type":27,"tag":172,"props":9596,"children":9599},{"href":9597,"rel":9598},"https://www.npmjs.com/package/dotenv",[696],[9600],{"type":32,"value":9601},"dotenv package",{"type":32,"value":9603}," that takes care of management of env variables by using ",{"type":27,"tag":653,"props":9605,"children":9607},{"className":9606},[],[9608],{"type":32,"value":943},{"type":32,"value":786},{"type":27,"tag":793,"props":9611,"children":9614},{"className":9612,"code":9613,"filename":943,"language":2250,"meta":5},[2248],"ADMIN_KEY=\"1234-5678-abcd-efgh\"\n",[9615],{"type":27,"tag":653,"props":9616,"children":9617},{"__ignoreMap":5},[9618],{"type":32,"value":9613},{"type":27,"tag":1029,"props":9620,"children":9621},{},[9622],{"type":27,"tag":28,"props":9623,"children":9624},{},[9625,9627,9632,9634,9639],{"type":32,"value":9626},"⚠️ Always make sure that ",{"type":27,"tag":653,"props":9628,"children":9630},{"className":9629},[],[9631],{"type":32,"value":943},{"type":32,"value":9633}," file is added to ",{"type":27,"tag":653,"props":9635,"children":9637},{"className":9636},[],[9638],{"type":32,"value":4244},{"type":32,"value":9640}," otherwise you risk commiting sensitive information out in the public",{"type":27,"tag":28,"props":9642,"children":9643},{},[9644,9646,9651],{"type":32,"value":9645},"To load the keys, dotenv package needs to be imported in ",{"type":27,"tag":653,"props":9647,"children":9649},{"className":9648},[],[9650],{"type":32,"value":5669},{"type":32,"value":9652}," so that env variables are loaded into Cypress and can be used during test.",{"type":27,"tag":793,"props":9654,"children":9658},{"className":9655,"code":9656,"filename":5669,"highlights":9657,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\nimport 'dotenv/config'\n\nexport default defineConfig({\n  // other config attributes\n  setupNodeEvents(on, config) {\n    // read ADMIN_KEY from .env file\n    config.env.ADMIN_KEY = process.env.ADMIN_KEY\n    return config\n  }\n})\n",[320],[9659],{"type":27,"tag":653,"props":9660,"children":9661},{"__ignoreMap":5},[9662],{"type":32,"value":9656},{"type":27,"tag":45,"props":9664,"children":9666},{"id":9665},"node-scripts",[9667],{"type":32,"value":9668},"Node scripts",{"type":27,"tag":28,"props":9670,"children":9671},{},[9672,9673,9678,9680,9686],{"type":32,"value":3349},{"type":27,"tag":653,"props":9674,"children":9676},{"className":9675},[],[9677],{"type":32,"value":5669},{"type":32,"value":9679}," file can get bloated pretty fast, especially when setting up tasks or resolving configurations. This is why I started splitting these into their own files and add them to ",{"type":27,"tag":653,"props":9681,"children":9683},{"className":9682},[],[9684],{"type":32,"value":9685},"scripts",{"type":32,"value":9687}," folder.",{"type":27,"tag":793,"props":9689,"children":9692},{"className":9690,"code":9691,"language":2042,"meta":5},[2040],"scripts/\n|-- codeCoverage.ts\n`-- resolveGoogleVars.ts\n",[9693],{"type":27,"tag":653,"props":9694,"children":9695},{"__ignoreMap":5},[9696],{"type":32,"value":9691},{"type":27,"tag":28,"props":9698,"children":9699},{},[9700,9702,9708],{"type":32,"value":9701},"This keeps the main config file clean and easy to read. It also makes it easier to maintain multiple ",{"type":27,"tag":653,"props":9703,"children":9705},{"className":9704},[],[9706],{"type":32,"value":9707},"cy.task()",{"type":32,"value":9709}," commands.",{"type":27,"tag":45,"props":9711,"children":9713},{"id":9712},"documentation",[9714],{"type":32,"value":9715},"Documentation",{"type":27,"tag":28,"props":9717,"children":9718},{},[9719],{"type":32,"value":9720},"Everytime a new member joins a team, they can either slow down the team, or make it more effective. That’s why having a good documentation is essential to successful onboarding.",{"type":27,"tag":28,"props":9722,"children":9723},{},[9724],{"type":32,"value":9725},"Usually the documentation contains 3 important parts:",{"type":27,"tag":851,"props":9727,"children":9728},{},[9729,9734,9739],{"type":27,"tag":109,"props":9730,"children":9731},{},[9732],{"type":32,"value":9733},"installation of the projects",{"type":27,"tag":109,"props":9735,"children":9736},{},[9737],{"type":32,"value":9738},"explanations, recommendations, examples",{"type":27,"tag":109,"props":9740,"children":9741},{},[9742],{"type":32,"value":9743},"pull request rules (these can be added to the platform you use)",{"type":27,"tag":28,"props":9745,"children":9746},{},[9747],{"type":32,"value":9748},"Installation needs to have all the information one needs in order to install and run project. This will be a living document, since changes introduced to the repository need to be reflected, but also because first version of the document is never sufficient enough. When an important information is missing, it may be useful for the newcomer to add that information to the docs and create their first commit.",{"type":27,"tag":28,"props":9750,"children":9751},{},[9752],{"type":32,"value":9753},"Since every project has its own specifics, it is important to have these explained. What are the conventions in the project? How do you solve common problems. What are the conventions used in this repository? All these questions should be answered in the docs. The main goal of this document is to make your life easier, so ideally it should be easy to read, and if needed, split into multiple files.",{"type":27,"tag":28,"props":9755,"children":9756},{},[9757],{"type":32,"value":9758},"I also find it useful to set some ground rules for pull requests. On many platforms, you can set rules on how many people should approve a pull request, add checklists and other requirements. While these may seem like too much, they are a great help that prevents you from forgetting something important when merging new code.",{"type":27,"tag":793,"props":9760,"children":9763},{"className":9761,"code":9762,"language":2042,"meta":5},[2040],"cypress/\n|-- commands/\n|-- config/\n`-- docs/\n    |-- best-practices.md\n    `-- installation.md\n",[9764],{"type":27,"tag":653,"props":9765,"children":9766},{"__ignoreMap":5},[9767],{"type":32,"value":9762},{"type":27,"tag":45,"props":9769,"children":9770},{"id":7672},[9771],{"type":32,"value":7675},{"type":27,"tag":28,"props":9773,"children":9774},{},[9775],{"type":32,"value":9776},"Big projects are rarely about just Cypress commands and they have much more to do with the test design and project design. While having some thoughts on what is the best way, most of my projects are living organisms that change and evolve as time progresses and needs shift. My current structure looks similar to something like this:",{"type":27,"tag":793,"props":9778,"children":9781},{"className":9779,"code":9780,"language":2042,"meta":5},[2040],"big-project/\n|-- cypress/\n|   |-- commands/\n|   |-- config/\n|   |-- docs/\n|   |-- downloads/\n|   |-- e2e/\n|   |-- fixtures/\n|   |-- screenshots/\n|   |-- scripts/\n|   |-- support/\n|   |-- utils/\n|   `-- videos/\n`-- cypress.config.ts\n",[9782],{"type":27,"tag":653,"props":9783,"children":9784},{"__ignoreMap":5},[9785],{"type":32,"value":9780},{"type":27,"tag":28,"props":9787,"children":9788},{},[9789,9791,9796,9797,9802,9803,9808],{"type":32,"value":9790},"I hope you were able to get some inspiration from this. I share tips like this more often so consider subscribing to the newsletter, and following me on ",{"type":27,"tag":172,"props":9792,"children":9794},{"href":5770,"rel":9793},[696],[9795],{"type":32,"value":1589},{"type":32,"value":3372},{"type":27,"tag":172,"props":9798,"children":9800},{"href":5777,"rel":9799},[696],[9801],{"type":32,"value":1598},{"type":32,"value":4164},{"type":27,"tag":172,"props":9804,"children":9806},{"href":8210,"rel":9805},[696],[9807],{"type":32,"value":8214},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":9810},[9811,9812,9813,9814,9815,9816,9821,9827,9828,9829,9830,9831,9832,9833,9834,9835],{"id":8250,"depth":320,"text":8253},{"id":8331,"depth":320,"text":8334},{"id":8406,"depth":320,"text":8409},{"id":8455,"depth":320,"text":8458},{"id":8582,"depth":320,"text":8585},{"id":8651,"depth":320,"text":8654,"children":9817},[9818,9819,9820],{"id":8679,"depth":1606,"text":8682},{"id":8753,"depth":1606,"text":8756},{"id":8793,"depth":1606,"text":8796},{"id":8860,"depth":320,"text":8863,"children":9822},[9823,9824,9825,9826],{"id":8889,"depth":1606,"text":8892},{"id":8907,"depth":1606,"text":8910},{"id":8948,"depth":1606,"text":8951},{"id":8968,"depth":1606,"text":8971},{"id":2608,"depth":320,"text":9109},{"id":3921,"depth":320,"text":3924},{"id":9312,"depth":320,"text":9315},{"id":9355,"depth":320,"text":9358},{"id":9396,"depth":320,"text":9399},{"id":9493,"depth":320,"text":9496},{"id":9665,"depth":320,"text":9668},{"id":9712,"depth":320,"text":9715},{"id":7672,"depth":320,"text":7675},"content:how-to-structure-a-big-project-in-cypress:index.md","how-to-structure-a-big-project-in-cypress/index.md","how-to-structure-a-big-project-in-cypress/index",{"_path":5721,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":9840,"description":9841,"date":9842,"published":10,"slug":9843,"tags":9844,"cypressVersion":9848,"readingTime":9849,"body":9853,"_type":329,"_id":10359,"_source":331,"_file":10360,"_stem":10361,"_extension":334},"Cypress.io and GitHub Actions: A step by step guide","GitHub Actions are a powerful and easy-to-learn tool that can help you quite a lot, In this blogpost we'll take a look into how they can be used to run your Cypress tests.","2023-01-16","cypress-and-git-hub-actions-step-by-step-guide",[5279,9845,9846,9847],"ci","github","actions","v12.3.0",{"text":1933,"minutes":9850,"time":9851,"words":9852},6.44,386400,1288,{"type":24,"children":9854,"toc":10351},[9855,9860,9866,9871,9876,9889,9894,9900,9913,9922,9936,9945,9950,9992,10005,10041,10088,10093,10099,10104,10113,10126,10133,10139,10144,10152,10163,10171,10176,10186,10191,10196,10204,10209,10217,10222,10230,10236,10241,10252,10273,10285,10293,10299,10313,10322],{"type":27,"tag":28,"props":9856,"children":9857},{},[9858],{"type":32,"value":9859},"You might have wondered about GitHub Actions. They seem like an advanced concept, but in reality they are a powerful and easy-to-learn tool that can help you quite a lot. Let’s look at how to use them to run your Cypress tests.",{"type":27,"tag":45,"props":9861,"children":9863},{"id":9862},"understanding-what-to-do",[9864],{"type":32,"value":9865},"Understanding what to do",{"type":27,"tag":28,"props":9867,"children":9868},{},[9869],{"type":32,"value":9870},"Let’s first take a look at what we are trying to achieve. The goal here is to run Cypress tests using GitHub Actions. In order to do that, we need to take a look at the repository we are working with. I will be using my trelloapp project as an example.",{"type":27,"tag":28,"props":9872,"children":9873},{},[9874],{"type":32,"value":9875},"Whenever I test my application locally, I need to do two things:",{"type":27,"tag":851,"props":9877,"children":9878},{},[9879,9884],{"type":27,"tag":109,"props":9880,"children":9881},{},[9882],{"type":32,"value":9883},"run my application on localhost",{"type":27,"tag":109,"props":9885,"children":9886},{},[9887],{"type":32,"value":9888},"run my tests against that localhost",{"type":27,"tag":28,"props":9890,"children":9891},{},[9892],{"type":32,"value":9893},"In order to do this on GitHub action server, I first need to spin up my project, and install everything I need. I also need to define on what occasion I want to run my tests (e.g. run them on demand, or run them whenever new code is pushed). This slowly shapes up the plan for how the GitHub action will look like. These plans are called \"workflows\" in GitHub Actions.",{"type":27,"tag":45,"props":9895,"children":9897},{"id":9896},"creating-a-workflow",[9898],{"type":32,"value":9899},"Creating a workflow",{"type":27,"tag":28,"props":9901,"children":9902},{},[9903,9905,9911],{"type":32,"value":9904},"Let’s now create a workflow file. Its place is in ",{"type":27,"tag":653,"props":9906,"children":9908},{"className":9907},[],[9909],{"type":32,"value":9910},".github/workflows",{"type":32,"value":9912}," folder:",{"type":27,"tag":793,"props":9914,"children":9917},{"className":9915,"code":9916,"language":2042,"meta":5},[2040],".github/\n`-- workflows/\n    `-- tests.yml\n",[9918],{"type":27,"tag":653,"props":9919,"children":9920},{"__ignoreMap":5},[9921],{"type":32,"value":9916},{"type":27,"tag":28,"props":9923,"children":9924},{},[9925,9927,9934],{"type":32,"value":9926},"This is where GitHub Actions will look for your workflow files. These files are in YAML format and you can think of them as recipes for what GitHub actions should do. They tend to be quite linear, although ",{"type":27,"tag":172,"props":9928,"children":9931},{"href":9929,"rel":9930},"https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution",[696],[9932],{"type":32,"value":9933},"there are ways to write conditional logic",{"type":32,"value":9935}," in them.",{"type":27,"tag":793,"props":9937,"children":9940},{"className":9938,"code":9939,"language":5708,"meta":5},[5706],"name: e2e-tests\non: [push]\njobs:\n  cypress-run:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Cypress run\n        uses: cypress-io/github-action@v5\n        with:\n          start: npm start\n",[9941],{"type":27,"tag":653,"props":9942,"children":9943},{"__ignoreMap":5},[9944],{"type":32,"value":9939},{"type":27,"tag":28,"props":9946,"children":9947},{},[9948],{"type":32,"value":9949},"Let’s break down what’s happening in this file. On the first line, we are giving our action a name. It can be anything, but it never hurts to be descriptive.",{"type":27,"tag":28,"props":9951,"children":9952},{},[9953,9955,9961,9962,9968,9969,9975,9976,9982,9984,9991],{"type":32,"value":9954},"Our second line defines an event that the given scenario should run against. There’s a variety of different events like ",{"type":27,"tag":653,"props":9956,"children":9958},{"className":9957},[],[9959],{"type":32,"value":9960},"push",{"type":32,"value":3372},{"type":27,"tag":653,"props":9963,"children":9965},{"className":9964},[],[9966],{"type":32,"value":9967},"pull_request",{"type":32,"value":3372},{"type":27,"tag":653,"props":9970,"children":9972},{"className":9971},[],[9973],{"type":32,"value":9974},"schedule",{"type":32,"value":1591},{"type":27,"tag":653,"props":9977,"children":9979},{"className":9978},[],[9980],{"type":32,"value":9981},"workflow_dispatch",{"type":32,"value":9983}," that allows you to trigger the action manually. You can find the full list in the ",{"type":27,"tag":172,"props":9985,"children":9988},{"href":9986,"rel":9987},"https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on",[696],[9989],{"type":32,"value":9990},"GitHub Actions documentation",{"type":32,"value":256},{"type":27,"tag":28,"props":9993,"children":9994},{},[9995,9997,10003],{"type":32,"value":9996},"The third line defines the job or jobs that need to be ran. This is where we need to define what needs to be done. If we were to start from scratch, this is where we would do ",{"type":27,"tag":653,"props":9998,"children":10000},{"className":9999},[],[10001],{"type":32,"value":10002},"npm install",{"type":32,"value":10004}," to install all dependencies, spin up the application and run our tests against it. But as you can see, we are not starting from scratch, but using pre-defined actions.",{"type":27,"tag":28,"props":10006,"children":10007},{},[10008,10010,10016,10018,10023,10025,10031,10033,10039],{"type":32,"value":10009},"This is one of the superpowers of GitHub actions. Instead of building everything ourselves, we can use previously created macros and have things handled for us. For example ",{"type":27,"tag":653,"props":10011,"children":10013},{"className":10012},[],[10014],{"type":32,"value":10015},"cypress-io/github-action@v5",{"type":32,"value":10017}," will run ",{"type":27,"tag":653,"props":10019,"children":10021},{"className":10020},[],[10022],{"type":32,"value":10002},{"type":32,"value":10024}," for us, properly cache Cypress (so that next time the installation is rapidly faster), run our application using ",{"type":27,"tag":653,"props":10026,"children":10028},{"className":10027},[],[10029],{"type":32,"value":10030},"npm start",{"type":32,"value":10032}," command and run ",{"type":27,"tag":653,"props":10034,"children":10036},{"className":10035},[],[10037],{"type":32,"value":10038},"npx cypress run",{"type":32,"value":10040}," command for us. All this with just four lines in our YAML file.",{"type":27,"tag":28,"props":10042,"children":10043},{},[10044,10046,10052,10054,10060,10062,10069,10071,10077,10079,10086],{"type":32,"value":10045},"We’ll talk about different configurations for the Cypress GitHub Action, but let’s back up for just a moment to mention some details in our ",{"type":27,"tag":653,"props":10047,"children":10049},{"className":10048},[],[10050],{"type":32,"value":10051},"cypress-run",{"type":32,"value":10053}," job. The ",{"type":27,"tag":653,"props":10055,"children":10057},{"className":10056},[],[10058],{"type":32,"value":10059},"runs-on",{"type":32,"value":10061}," parameter defines ",{"type":27,"tag":172,"props":10063,"children":10066},{"href":10064,"rel":10065},"https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-github-hosted-runners",[696],[10067],{"type":32,"value":10068},"what kind of machine",{"type":32,"value":10070}," we want to use for our tests. The ",{"type":27,"tag":653,"props":10072,"children":10074},{"className":10073},[],[10075],{"type":32,"value":10076},"actions/checkout@v3",{"type":32,"value":10078}," might feel a little odd - why checkout the project we are currently working on? The reason is quite simple actually. There are many things we can do with GitHub action that don’t include checking out the repository. For example ",{"type":27,"tag":172,"props":10080,"children":10083},{"href":10081,"rel":10082},"https://github.com/marketplace/actions/slack-notify",[696],[10084],{"type":32,"value":10085},"sending a Slack notification",{"type":32,"value":10087}," whenever new code is pushed to a remote branch.",{"type":27,"tag":28,"props":10089,"children":10090},{},[10091],{"type":32,"value":10092},"Let’s now focus on how we can use GitHub actions to improve our workflow.",{"type":27,"tag":45,"props":10094,"children":10096},{"id":10095},"trigger-test-run-manually",[10097],{"type":32,"value":10098},"Trigger test run manually",{"type":27,"tag":28,"props":10100,"children":10101},{},[10102],{"type":32,"value":10103},"Instead of running your test on every push, you can choose to run your test on demand. The setup file will look like this:",{"type":27,"tag":793,"props":10105,"children":10108},{"className":10106,"code":10107,"language":5708,"meta":5},[5706],"name: e2e-tests\non: [worfklow_dispatch]\njobs:\n  cypress-run:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Cypress run\n        uses: cypress-io/github-action@v5\n        with:\n          start: npm start\n",[10109],{"type":27,"tag":653,"props":10110,"children":10111},{"__ignoreMap":5},[10112],{"type":32,"value":10107},{"type":27,"tag":28,"props":10114,"children":10115},{},[10116,10118,10124],{"type":32,"value":10117},"Once you set up your file like this, you can go to GitHub and under \"Actions\" tab run your ",{"type":27,"tag":653,"props":10119,"children":10121},{"className":10120},[],[10122],{"type":32,"value":10123},"e2e-tests",{"type":32,"value":10125}," workflow manually.",{"type":27,"tag":28,"props":10127,"children":10128},{},[10129],{"type":27,"tag":959,"props":10130,"children":10132},{"alt":10098,"src":10131},"workflow_dispatch.png",[],{"type":27,"tag":45,"props":10134,"children":10136},{"id":10135},"github-actions-and-cypress-cloud",[10137],{"type":32,"value":10138},"GitHub actions and Cypress Cloud",{"type":27,"tag":28,"props":10140,"children":10141},{},[10142],{"type":32,"value":10143},"Cypress Cloud (formerly Cypress Dashboard) is a service for recording your Cypress test results. It offers a bird’s eye view of your tests, so that you can analyze and maintain your tests more effectively. Recording the results to the Cypress Cloud service is quite simple. As a first step, you need to connect your project to the service. This is done right in your Cypress open mode.",{"type":27,"tag":28,"props":10145,"children":10146},{},[10147],{"type":27,"tag":959,"props":10148,"children":10151},{"alt":10149,"src":10150},"Connect your project to Cypress Cloud service","connect_cloud.mp4",[],{"type":27,"tag":28,"props":10153,"children":10154},{},[10155,10157],{"type":32,"value":10156},"This step will generate a unique key, that will be used to authorize GitHub Actions to communicate with Cypress cloud. Copy this key, and add it to environment in your GitHub Project as ",{"type":27,"tag":653,"props":10158,"children":10160},{"className":10159},[],[10161],{"type":32,"value":10162},"CYPRESS_RECORD_KEY",{"type":27,"tag":28,"props":10164,"children":10165},{},[10166],{"type":27,"tag":959,"props":10167,"children":10170},{"alt":10168,"src":10169},"Store Cypress key in GitHub actions","github_secret.mp4",[],{"type":27,"tag":28,"props":10172,"children":10173},{},[10174],{"type":32,"value":10175},"As last step, we need to edit the workflow file:",{"type":27,"tag":793,"props":10177,"children":10181},{"className":10178,"code":10179,"highlights":10180,"language":5708,"meta":5},[5706],"name: e2e-tests\non: [push]\njobs:\n  cypress-run:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Cypress run\n        uses: cypress-io/github-action@v5\n        with:\n          start: npm start\n          record: true\n        env:\n          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n",[3852,3853,3878],[10182],{"type":27,"tag":653,"props":10183,"children":10184},{"__ignoreMap":5},[10185],{"type":32,"value":10179},{"type":27,"tag":28,"props":10187,"children":10188},{},[10189],{"type":32,"value":10190},"This will enable recording results to Cypress Cloud and add the Cypress key into the environment so our Cypress run can use it.",{"type":27,"tag":28,"props":10192,"children":10193},{},[10194],{"type":32,"value":10195},"Having results recorded to Cypress Cloud gives you some amazing capabilities. You can see screenshots and videos from Cypress run that can help you debug your tests, or you can look at long term analytics and check the health of your test suite. Additionaly, there are some useful integrations. If you don’t want to switch between Cypress Cloud and GitHub all the time, you can have your test results sent right into GitHub.",{"type":27,"tag":28,"props":10197,"children":10198},{},[10199],{"type":27,"tag":959,"props":10200,"children":10203},{"alt":10201,"src":10202},"GitHub integration","github-integration.png",[],{"type":27,"tag":28,"props":10205,"children":10206},{},[10207],{"type":32,"value":10208},"After setting up your integration, your test results will get sent right into your pull request conversation and to your email.",{"type":27,"tag":28,"props":10210,"children":10211},{},[10212],{"type":27,"tag":959,"props":10213,"children":10216},{"alt":10214,"src":10215},"Integration summary","integration-summary.png",[],{"type":27,"tag":28,"props":10218,"children":10219},{},[10220],{"type":32,"value":10221},"GitHub integration will also become part of pull request checks so that you can prevent a pull request from merging if tests failed.",{"type":27,"tag":28,"props":10223,"children":10224},{},[10225],{"type":27,"tag":959,"props":10226,"children":10229},{"alt":10227,"src":10228},"Pull request checks","checks.png",[],{"type":27,"tag":45,"props":10231,"children":10233},{"id":10232},"running-tests-in-parallel",[10234],{"type":32,"value":10235},"Running tests in parallel",{"type":27,"tag":28,"props":10237,"children":10238},{},[10239],{"type":32,"value":10240},"Cypress Cloud allows you to run your tests in parallel and once you have the project set up, it’s relatively easy to start splitting your tests across multiple machines. The configuration file will look like this:",{"type":27,"tag":793,"props":10242,"children":10247},{"className":10243,"code":10244,"highlights":10245,"language":5708,"meta":5},[5706],"name: e2e-tests\non: [push]\njobs:\n  cypress-run:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        containers: [1, 2, 3]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v2\n      - name: Cypress run\n        uses: cypress-io/github-action@v4\n        with:\n          start: npm start\n          record: true\n          parallel: true\n        env:\n          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n",[3809,3668,3723,3746,10246],18,[10248],{"type":27,"tag":653,"props":10249,"children":10250},{"__ignoreMap":5},[10251],{"type":32,"value":10244},{"type":27,"tag":28,"props":10253,"children":10254},{},[10255,10257,10263,10265,10271],{"type":32,"value":10256},"This setup will spin up three machines and run everything in our ",{"type":27,"tag":653,"props":10258,"children":10260},{"className":10259},[],[10261],{"type":32,"value":10262},"steps",{"type":32,"value":10264}," in parallel. The ",{"type":27,"tag":653,"props":10266,"children":10268},{"className":10267},[],[10269],{"type":32,"value":10270},"parallel: true",{"type":32,"value":10272},"flag will make sure that our test run will communicate with the Cypress Cloud and split all our tests in between machines. Cypress Cloud will also take care of automatic load balancing of your tests, so that your test run will take as little time as possible.",{"type":27,"tag":28,"props":10274,"children":10275},{},[10276,10277,10283],{"type":32,"value":3349},{"type":27,"tag":653,"props":10278,"children":10280},{"className":10279},[],[10281],{"type":32,"value":10282},"fail-fast",{"type":32,"value":10284}," flag will make sure that GitHub will not terminate our job if there’s a failed test. If set this to true, you run the risk of leaving your test run hanging on Cypress Cloud. If you want to stop your test from continuing when there’s a failure, you can set this up in your project settings in Cypress Cloud. It even allows you to set up the number of tests that should trigger test run cancellation.",{"type":27,"tag":28,"props":10286,"children":10287},{},[10288],{"type":27,"tag":959,"props":10289,"children":10292},{"alt":10290,"src":10291},"Smart orchestration","smart-orchestration.png",[],{"type":27,"tag":45,"props":10294,"children":10296},{"id":10295},"running-in-parallel-without-cypress-cloud",[10297],{"type":32,"value":10298},"Running in parallel without Cypress Cloud",{"type":27,"tag":28,"props":10300,"children":10301},{},[10302,10304,10311],{"type":32,"value":10303},"Just as I was writing this blogpost, ",{"type":27,"tag":172,"props":10305,"children":10308},{"href":10306,"rel":10307},"https://github.com/bahmutov/cypress-split",[696],[10309],{"type":32,"value":10310},"Gleb Bahmutov has released a plugin",{"type":32,"value":10312}," that allows you to run your tests in parallel without using Cypress Cloud service. It will split your specs across parallel machines just as Cypress Cloud would. The configuration is quite simple.",{"type":27,"tag":793,"props":10314,"children":10317},{"className":10315,"code":10316,"language":5708,"meta":5},[5706],"name: e2e-tests\non: [push]\njobs:\n  cypress-run:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        containers: [1, 2, 3]\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: Cypress run\n        uses: cypress-io/github-action@v5\n        with:\n          start: npm start\n        env:\n          SPLIT: ${{ strategy.job-total }}\n          SPLIT_INDEX: ${{ strategy.job-index }}\n",[10318],{"type":27,"tag":653,"props":10319,"children":10320},{"__ignoreMap":5},[10321],{"type":32,"value":10316},{"type":27,"tag":28,"props":10323,"children":10324},{},[10325,10327,10333,10334,10340,10342,10349],{"type":32,"value":10326},"To give GitHub proper information, you need to set up ",{"type":27,"tag":653,"props":10328,"children":10330},{"className":10329},[],[10331],{"type":32,"value":10332},"SPLIT",{"type":32,"value":4164},{"type":27,"tag":653,"props":10335,"children":10337},{"className":10336},[],[10338],{"type":32,"value":10339},"SPLIT_INDEX",{"type":32,"value":10341}," env variables. This way each process is assigned the specs it needs. It will make the decision automatically, but there are ways to configure this ",{"type":27,"tag":172,"props":10343,"children":10346},{"href":10344,"rel":10345},"https://github.com/bahmutov/cypress-split#list-of-specs",[696],[10347],{"type":32,"value":10348},"mentioned in the README file",{"type":32,"value":10350},". This is a very nice solution for situations where you don’t need Cypress Cloud or have your own reporting dashboard already set up.",{"title":5,"searchDepth":320,"depth":320,"links":10352},[10353,10354,10355,10356,10357,10358],{"id":9862,"depth":320,"text":9865},{"id":9896,"depth":320,"text":9899},{"id":10095,"depth":320,"text":10098},{"id":10135,"depth":320,"text":10138},{"id":10232,"depth":320,"text":10235},{"id":10295,"depth":320,"text":10298},"content:cypress-and-git-hub-actions-step-by-step-guide:index.md","cypress-and-git-hub-actions-step-by-step-guide/index.md","cypress-and-git-hub-actions-step-by-step-guide/index",{"_path":10363,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":10364,"description":10365,"date":10366,"published":10,"slug":10367,"tags":10368,"cypressVersion":10372,"readingTime":10373,"body":10377,"_type":329,"_id":10982,"_source":331,"_file":10983,"_stem":10984,"_extension":334},"/use-session-instead-of-login-page-object-in-cypress","Use cy.session() instead of login page object in Cypress","Short guide to using cy.session() command an save minutes from your test run. Creating a session with Cypress can not only save you time, but can be a great substitution for a log page object","2022-11-16","use-session-instead-of-login-page-object-in-cypress",[5279,4129,10369,10370,10371],"session","performance","caching","v12.0.0",{"text":19,"minutes":10374,"time":10375,"words":10376},8.145,488700,1629,{"type":24,"children":10378,"toc":10973},[10379,10384,10389,10398,10418,10424,10429,10438,10450,10459,10465,10478,10487,10513,10522,10528,10533,10551,10581,10603,10609,10621,10631,10640,10666,10674,10693,10698,10752,10781,10787,10799,10809,10822,10828,10867,10878,10890,10896,10901,10913,10932,10941,10946],{"type":27,"tag":28,"props":10380,"children":10381},{},[10382],{"type":32,"value":10383},"With many test automation efforts, dealing with login is the first hurdle to overcome. This can in itself be quite the challenge.",{"type":27,"tag":28,"props":10385,"children":10386},{},[10387],{"type":32,"value":10388},"A most common way to solve a login flow is to simply go through this as a normal user would. Let’s see how this will look in our app:",{"type":27,"tag":793,"props":10390,"children":10393},{"className":10391,"code":10392,"language":3520,"meta":5},[3517],"cy.visit('/login')\ncy.get('[data-cy=login-email]').type('filip@example.com')\ncy.get('[data-cy=login-password]').type('i\u003C3slovakia!')\ncy.get('[data-cy=login-submit]').click()\ncy.location('pathname').should('eq', '/')\n",[10394],{"type":27,"tag":653,"props":10395,"children":10396},{"__ignoreMap":5},[10397],{"type":32,"value":10392},{"type":27,"tag":28,"props":10399,"children":10400},{},[10401,10403,10408,10410,10416],{"type":32,"value":10402},"Cypress tries to clear browser data in between tests, which leads to the need of logging in before every rest. We can either do this by using ",{"type":27,"tag":653,"props":10404,"children":10406},{"className":10405},[],[10407],{"type":32,"value":7243},{"type":32,"value":10409}," hook or ",{"type":27,"tag":172,"props":10411,"children":10413},{"href":10412},"/cypress-basics-where-did-my-cookies-disappear",[10414],{"type":32,"value":10415},"by using cookies api",{"type":32,"value":10417}," to ignore deleting certain cookies from our app.",{"type":27,"tag":45,"props":10419,"children":10421},{"id":10420},"abstracting-login-into-page-object",[10422],{"type":32,"value":10423},"Abstracting login into page object",{"type":27,"tag":28,"props":10425,"children":10426},{},[10427],{"type":32,"value":10428},"It makes sense to abstract the login sequence into a separate entity. A go to solution is a page object. In many cases it might look something like this:",{"type":27,"tag":793,"props":10430,"children":10433},{"className":10431,"code":10432,"language":3520,"meta":5},[3517],"export class LoginPage {\n\n  username: string\n  password: string\n  log_in: string\n\n  constructor() {\n    this.username = '[data-cy=login-email]'\n    this.password = '[data-cy=login-password]'\n    this.log_in = '[data-cy=login-submit]'\n  }\n\n  /**\n   * opens a login page\n   */\n  load() {\n    cy.visit('/login')\n  }\n\n  /**\n   * fills in username, password and submits form\n   * @param username username of user to log in\n   * @param pass password of the user\n   */\n  login(username: string, pass: string) {\n\n    cy.get(this.username).type(username)\n    cy.get(this.password).type(pass)\n    cy.get(this.log_in).click()\n    cy.location('pathname').should('eq', '/')\n  }\n\n}\n",[10434],{"type":27,"tag":653,"props":10435,"children":10436},{"__ignoreMap":5},[10437],{"type":32,"value":10432},{"type":27,"tag":28,"props":10439,"children":10440},{},[10441,10443,10448],{"type":32,"value":10442},"This way we can easily login before every test and arrange our test before making actions in our test scenario. If you want to do this globally, you can just add a global ",{"type":27,"tag":653,"props":10444,"children":10446},{"className":10445},[],[10447],{"type":32,"value":7243},{"type":32,"value":10449}," hook into your support file:",{"type":27,"tag":793,"props":10451,"children":10454},{"className":10452,"code":10453,"filename":5661,"language":3520,"meta":5},[3517],"import { LoginPage } from '../support/models/LoginPage'\n\nbeforeEach( () => {\n\n  loginPage.load()\n  loginPage.login('filip@example.com', 'i\u003C3slovakia!')\n\n})\n",[10455],{"type":27,"tag":653,"props":10456,"children":10457},{"__ignoreMap":5},[10458],{"type":32,"value":10453},{"type":27,"tag":45,"props":10460,"children":10462},{"id":10461},"using-a-custom-command",[10463],{"type":32,"value":10464},"Using a custom command",{"type":27,"tag":28,"props":10466,"children":10467},{},[10468,10470,10476],{"type":32,"value":10469},"I personally prefer using custom commands for widely used functions like this. The big advantage of custom commands is that they become part of your Cypress library. Because of this, they are easy to find, and also play well with Cypress’ chaining syntax. You can also create DOM snapshots for debugging and many more. I write about these in more detail in ",{"type":27,"tag":172,"props":10471,"children":10473},{"href":10472},"/improve-your-custom-command-logs-in-cypress",[10474],{"type":32,"value":10475},"my previous blogpost",{"type":32,"value":10477},". A custom command for a login might look like this:",{"type":27,"tag":793,"props":10479,"children":10482},{"className":10480,"code":10481,"language":3520,"meta":5},[3517],"declare global {\n  namespace Cypress {\n    interface Chainable {\n      /**\n       * Logs in with a given user\n       * @param email email of the user you want to log in\n       * @param password user passwird\n       * @example\n       * cy.login('filip@example.com', 'i\u003C3slovakia!')\n       *\n       */\n      login: typeof login\n    }\n  }\n}\n\nconst login = (email: string, password: string) => {\n\n  cy.visit('/login')\n  cy.get('[data-cy=login-email]').type(email)\n  cy.get('[data-cy=login-password]').type(`${password}`)\n  cy.get('[data-cy=login-submit]').click()\n  cy.location('pathname').should('eq', '/')\n\n};\n\nCypress.Commands.addAll({ login })\n\n",[10483],{"type":27,"tag":653,"props":10484,"children":10485},{"__ignoreMap":5},[10486],{"type":32,"value":10481},{"type":27,"tag":28,"props":10488,"children":10489},{},[10490,10492,10498,10500,10505,10507,10512],{"type":32,"value":10491},"Lines 1 - 15 are adding our custom command to the Cypress library by expanding TypeScript definitions. Lines 17 - 24 is a function definition that contains the login sequence. Finally, we add our function on line 26. Similarly as with our page object example, we can add our newly created ",{"type":27,"tag":653,"props":10493,"children":10495},{"className":10494},[],[10496],{"type":32,"value":10497},"cy.login()",{"type":32,"value":10499}," command to a global ",{"type":27,"tag":653,"props":10501,"children":10503},{"className":10502},[],[10504],{"type":32,"value":7243},{"type":32,"value":10506}," hook and make our test log in before every ",{"type":27,"tag":653,"props":10508,"children":10510},{"className":10509},[],[10511],{"type":32,"value":7187},{"type":32,"value":8630},{"type":27,"tag":793,"props":10514,"children":10517},{"className":10515,"code":10516,"filename":5661,"language":3520,"meta":5},[3517],"beforeEach( () => {\n  cy.login('filip@example.com', 'i\u003C3slovakia!')\n})\n",[10518],{"type":27,"tag":653,"props":10519,"children":10520},{"__ignoreMap":5},[10521],{"type":32,"value":10516},{"type":27,"tag":45,"props":10523,"children":10525},{"id":10524},"programmatic-login",[10526],{"type":32,"value":10527},"Programmatic login",{"type":27,"tag":28,"props":10529,"children":10530},{},[10531],{"type":32,"value":10532},"However, using UI is not the most effective way and will definitely take a performance toll. The second option is to log in programatically. But of course, this is often easier said than done. With programmatic login (or any kind of login for that matter), there are 3 parts present:",{"type":27,"tag":851,"props":10534,"children":10535},{},[10536,10541,10546],{"type":27,"tag":109,"props":10537,"children":10538},{},[10539],{"type":32,"value":10540},"server",{"type":27,"tag":109,"props":10542,"children":10543},{},[10544],{"type":32,"value":10545},"frontend",{"type":27,"tag":109,"props":10547,"children":10548},{},[10549],{"type":32,"value":10550},"browser",{"type":27,"tag":28,"props":10552,"children":10553},{},[10554,10556,10560,10562,10566,10568,10572,10574,10579],{"type":32,"value":10555},"On log in, ",{"type":27,"tag":79,"props":10557,"children":10558},{},[10559],{"type":32,"value":10540},{"type":32,"value":10561}," provides data (usually in form of token), and then ",{"type":27,"tag":79,"props":10563,"children":10564},{},[10565],{"type":32,"value":10545},{"type":32,"value":10567}," saves it to ",{"type":27,"tag":79,"props":10569,"children":10570},{},[10571],{"type":32,"value":10550},{"type":32,"value":10573}," (usually in form of cookies). To be able to log in programmatically, you need to understand how your app is sending data to the server and how it handles the response. Basically, you need to know ",{"type":27,"tag":302,"props":10575,"children":10576},{},[10577],{"type":32,"value":10578},"exactly",{"type":32,"value":10580}," what happens when user fills in the information and hits \"Log In\" button.",{"type":27,"tag":28,"props":10582,"children":10583},{},[10584,10586,10593,10595,10602],{"type":32,"value":10585},"In other words, you will be re-creating that login in your test. It’s pretty darn fast once you figure it out, but not easy. Especially if you need to deal with ",{"type":27,"tag":172,"props":10587,"children":10590},{"href":10588,"rel":10589},"https://filiphric.com/google-sign-in-with-cypress",[696],[10591],{"type":32,"value":10592},"3rd party login",{"type":32,"value":10594},", OAuth flows and other more advanced login methods. There are many useful guides in ",{"type":27,"tag":172,"props":10596,"children":10599},{"href":10597,"rel":10598},"https://docs.cypress.io/guides/end-to-end-testing/google-authentication",[696],[10600],{"type":32,"value":10601},"Cypress docs for this",{"type":32,"value":256},{"type":27,"tag":45,"props":10604,"children":10606},{"id":10605},"using-cysession",[10607],{"type":32,"value":10608},"Using cy.session()",{"type":27,"tag":28,"props":10610,"children":10611},{},[10612,10614,10619],{"type":32,"value":10613},"But there’s actually no need to figure out login and be effective. With ",{"type":27,"tag":653,"props":10615,"children":10617},{"className":10616},[],[10618],{"type":32,"value":6509},{"type":32,"value":10620}," command, you can use your UI just once for your whole test suite.",{"type":27,"tag":28,"props":10622,"children":10623},{},[10624,10629],{"type":27,"tag":653,"props":10625,"children":10627},{"className":10626},[],[10628],{"type":32,"value":6509},{"type":32,"value":10630}," command came out with version 8.2.0. It was one of the releases that did not get as much attention as they should in my opinion. It’s one of the most effective things you can do in terms of logging in. Let me give you a simple example of the usage and show you how it works. We will use the custom command shown above for this.",{"type":27,"tag":793,"props":10632,"children":10635},{"className":10633,"code":10634,"language":3520,"meta":5},[3517],"it('logs in the user', () => {\n\n  cy.session('performLoginSequence', () => {\n    cy.login('filip@example.com', 'i\u003C3slovakia!')\n  })\n\n  cy.visit('/')\n})\n",[10636],{"type":27,"tag":653,"props":10637,"children":10638},{"__ignoreMap":5},[10639],{"type":32,"value":10634},{"type":27,"tag":28,"props":10641,"children":10642},{},[10643,10645,10650,10652,10657,10659,10664],{"type":32,"value":10644},"In our test, we have wrapped our ",{"type":27,"tag":653,"props":10646,"children":10648},{"className":10647},[],[10649],{"type":32,"value":10497},{"type":32,"value":10651}," command inside ",{"type":27,"tag":653,"props":10653,"children":10655},{"className":10654},[],[10656],{"type":32,"value":6509},{"type":32,"value":10658}," command. This is giving Cypress the information, that whatever runs inside the ",{"type":27,"tag":653,"props":10660,"children":10662},{"className":10661},[],[10663],{"type":32,"value":6509},{"type":32,"value":10665}," callback (in other words, between line 3 and 5) should be remembered as a session. When we run this test again, instead of going through the login, our session will be restored. Notice the following image that shows first run (image above) and second run (image below).",{"type":27,"tag":28,"props":10667,"children":10668},{},[10669],{"type":27,"tag":959,"props":10670,"children":10673},{"alt":10671,"src":10672},"Session in action","session.png",[],{"type":27,"tag":28,"props":10675,"children":10676},{},[10677,10679,10684,10686,10691],{"type":32,"value":10678},"As you can see, the second run is going to ",{"type":27,"tag":79,"props":10680,"children":10681},{},[10682],{"type":32,"value":10683},"restore",{"type":32,"value":10685}," the session, while the first one is ",{"type":27,"tag":79,"props":10687,"children":10688},{},[10689],{"type":32,"value":10690},"creating",{"type":32,"value":10692}," it. You can also notice how the second test takes just a fragment of the time compared to first test.",{"type":27,"tag":28,"props":10694,"children":10695},{},[10696],{"type":32,"value":10697},"The way it works goes something like this:",{"type":27,"tag":851,"props":10699,"children":10700},{},[10701,10706],{"type":27,"tag":109,"props":10702,"children":10703},{},[10704],{"type":32,"value":10705},"Cypress runs a test",{"type":27,"tag":109,"props":10707,"children":10708},{},[10709,10711,10716,10718],{"type":32,"value":10710},"when it stumbles upon ",{"type":27,"tag":653,"props":10712,"children":10714},{"className":10713},[],[10715],{"type":32,"value":6509},{"type":32,"value":10717}," it will make a decision\n",{"type":27,"tag":851,"props":10719,"children":10720},{},[10721,10741],{"type":27,"tag":109,"props":10722,"children":10723},{},[10724,10726,10732,10734,10739],{"type":32,"value":10725},"Session with name ",{"type":27,"tag":653,"props":10727,"children":10729},{"className":10728},[],[10730],{"type":32,"value":10731},"performLoginSequence",{"type":32,"value":10733}," does not exist, I will run the code inside ",{"type":27,"tag":653,"props":10735,"children":10737},{"className":10736},[],[10738],{"type":32,"value":6509},{"type":32,"value":10740}," callback",{"type":27,"tag":109,"props":10742,"children":10743},{},[10744,10745,10750],{"type":32,"value":10725},{"type":27,"tag":653,"props":10746,"children":10748},{"className":10747},[],[10749],{"type":32,"value":10731},{"type":32,"value":10751}," exists, I will restore the session",{"type":27,"tag":28,"props":10753,"children":10754},{},[10755,10757,10762,10764,10768,10770,10774,10775,10779],{"type":32,"value":10756},"What restoring a session essentially means is recovering all of the browser cookies, local storage and session storage that was present in browser after user was logged in. Remember how we talked about programmatic login and three parts of the login flow? Well, ",{"type":27,"tag":653,"props":10758,"children":10760},{"className":10759},[],[10761],{"type":32,"value":6509},{"type":32,"value":10763}," takes care of the ",{"type":27,"tag":79,"props":10765,"children":10766},{},[10767],{"type":32,"value":10550},{"type":32,"value":10769}," part, which means you don’t need to worry about the ",{"type":27,"tag":79,"props":10771,"children":10772},{},[10773],{"type":32,"value":10545},{"type":32,"value":4164},{"type":27,"tag":79,"props":10776,"children":10777},{},[10778],{"type":32,"value":10540},{"type":32,"value":10780}," part.",{"type":27,"tag":45,"props":10782,"children":10784},{"id":10783},"applying-cysession-to-the-whole-project",[10785],{"type":32,"value":10786},"Applying cy.session() to the whole project",{"type":27,"tag":28,"props":10788,"children":10789},{},[10790,10792,10797],{"type":32,"value":10791},"We can now take the session to the whole project by inserting the ",{"type":27,"tag":653,"props":10793,"children":10795},{"className":10794},[],[10796],{"type":32,"value":6509},{"type":32,"value":10798}," sequence into the support file:",{"type":27,"tag":793,"props":10800,"children":10804},{"className":10801,"code":10802,"filename":5661,"highlights":10803,"language":3520,"meta":5},[3517],"beforeEach( () => {\n  cy.session('loginTestingUser', () => {\n    cy.login('filip@example.com', 'i\u003C3slovakia!')\n    }, { \n    cacheAcrossSpecs: true\n  })\n  \n}) \n",[3667],[10805],{"type":27,"tag":653,"props":10806,"children":10807},{"__ignoreMap":5},[10808],{"type":32,"value":10802},{"type":27,"tag":28,"props":10810,"children":10811},{},[10812,10814,10820],{"type":32,"value":10813},"Notice the ",{"type":27,"tag":653,"props":10815,"children":10817},{"className":10816},[],[10818],{"type":32,"value":10819},"cacheAcrossSpecs",{"type":32,"value":10821}," option on line 5. This will make login just once for the whole test run. Imagine every login sequence takes 2 second to finish. If you had 100 tests that log in, you just reduced more than 3 minutes from your test run!",{"type":27,"tag":45,"props":10823,"children":10825},{"id":10824},"creating-multiple-sessions",[10826],{"type":32,"value":10827},"Creating multiple sessions",{"type":27,"tag":28,"props":10829,"children":10830},{},[10831,10833,10838,10840,10845,10846,10851,10853,10858,10860,10865],{"type":32,"value":10832},"Let’s say you have multiple users you want to test your UI against. The first argument of ",{"type":27,"tag":653,"props":10834,"children":10836},{"className":10835},[],[10837],{"type":32,"value":6509},{"type":32,"value":10839}," command is actually an alias for the session. This means we can create multiple sessions and give them different names. The easiest way to achieve this is to flip the order of how we write our commands. Instead of wrapping out ",{"type":27,"tag":653,"props":10841,"children":10843},{"className":10842},[],[10844],{"type":32,"value":10497},{"type":32,"value":10651},{"type":27,"tag":653,"props":10847,"children":10849},{"className":10848},[],[10850],{"type":32,"value":6509},{"type":32,"value":10852},", let’s make ",{"type":27,"tag":653,"props":10854,"children":10856},{"className":10855},[],[10857],{"type":32,"value":6509},{"type":32,"value":10859}," a part of our ",{"type":27,"tag":653,"props":10861,"children":10863},{"className":10862},[],[10864],{"type":32,"value":10497},{"type":32,"value":10866}," command, like this:",{"type":27,"tag":793,"props":10868,"children":10873},{"className":10869,"code":10870,"highlights":10871,"language":3520,"meta":5},[3517],"declare global {\n  namespace Cypress {\n    interface Chainable {\n      /**\n       * Logs in with a given user\n       * @param email email of the user you want to log in\n       * @param password user passwird\n       * @example\n       * cy.login('filip@example.com', 'i\u003C3slovakia!')\n       *\n       */\n      login: typeof login\n    }\n  }\n}\n\nconst login = (email: string, password: string) => {\n\n  cy.session(email, () => {\n    cy.visit('/login')\n    cy.get('[data-cy=login-email]').type(email)\n    cy.get('[data-cy=login-password]').type(`${password}`)\n    cy.get('[data-cy=login-submit]').click()\n    cy.location('pathname').should('eq', '/')\n  }, { \n    cacheAcrossSpecs: true\n  })\n\n};\n\nCypress.Commands.addAll({ login })\n\n",[10872],17,[10874],{"type":27,"tag":653,"props":10875,"children":10876},{"__ignoreMap":5},[10877],{"type":32,"value":10870},{"type":27,"tag":28,"props":10879,"children":10880},{},[10881,10883,10888],{"type":32,"value":10882},"Notice on line 17, we are giving our session the same name as the email we pass into the ",{"type":27,"tag":653,"props":10884,"children":10886},{"className":10885},[],[10887],{"type":32,"value":10497},{"type":32,"value":10889}," function. This means that every user we log in with is going to create their own session. In other words, no matter how many logins we do with different users, each of them will get logged in just once.",{"type":27,"tag":45,"props":10891,"children":10893},{"id":10892},"why-use-a-custom-command-instead-of-page-object-opinion",[10894],{"type":32,"value":10895},"Why use a custom command instead of page object - opinion",{"type":27,"tag":28,"props":10897,"children":10898},{},[10899],{"type":32,"value":10900},"You may have heard me saying contradicting things on page object model in the past. I critisize page object model for how it’s being used, but also say that it is not an antipattern. While it makes sense to make abstractions, it really matters how you do them. There’s a big push for using page object model in order to create abstractions and DRY code (DRY = don’t repeat yourself). While I’m all for DRY code, I think as testers we should aim for DRY test execution as well.",{"type":27,"tag":28,"props":10902,"children":10903},{},[10904,10906,10911],{"type":32,"value":10905},"Page objects are often used to set up a state of your application. Basically, if you want to test \"B\", you need to get there by doing \"A\". If that \"A\" is a login, you might need to perform it in all of your tests. ",{"type":27,"tag":653,"props":10907,"children":10909},{"className":10908},[],[10910],{"type":32,"value":6509},{"type":32,"value":10912}," allows you to limit the amount of times you do \"A\". This does not have to be a login, you can actually do your setup by calling a bunch of API endpoints or perform some other setup actions.",{"type":27,"tag":28,"props":10914,"children":10915},{},[10916,10918,10923,10925,10930],{"type":32,"value":10917},"Using custom commands or custom functions makes more sense than by using a UI page object. But even if you decide to do the setup via UI, you can save a lot of time by using ",{"type":27,"tag":653,"props":10919,"children":10921},{"className":10920},[],[10922],{"type":32,"value":6509},{"type":32,"value":10924},". You would have a hard time using ",{"type":27,"tag":653,"props":10926,"children":10928},{"className":10927},[],[10929],{"type":32,"value":6509},{"type":32,"value":10931}," with a page object. It’s not impossible, but essentially you might need to put your whole login action into a single function, where you visit, fill and submit login form. That page object would look something like this:",{"type":27,"tag":793,"props":10933,"children":10936},{"className":10934,"code":10935,"language":3520,"meta":5},[3517],"export class LoginPage {\n\n  username: string\n  password: string\n  log_in: string\n\n  constructor() {\n    this.username = '[data-cy=login-email]'\n    this.password = '[data-cy=login-password]'\n    this.log_in = '[data-cy=login-submit]'\n  }\n\n  /**\n   * fills in username, password and submits form\n   * @param username username of user to log in\n   * @param pass password of the user\n   */\n  loginAndFill(username: string, pass: string) {\n\n    cy.session(username, () => {\n      cy.visit('/login')\n      cy.get(this.username).type(username)\n      cy.get(this.password).type(pass)\n      cy.get(this.log_in).click()\n      cy.location('pathname').should('eq', '/')\n    }, { \n      cacheAcrossSpecs: true\n    })\n  }\n\n}\n",[10937],{"type":27,"tag":653,"props":10938,"children":10939},{"__ignoreMap":5},[10940],{"type":32,"value":10935},{"type":27,"tag":28,"props":10942,"children":10943},{},[10944],{"type":32,"value":10945},"In this case, our page object contains just a single function and it would not make sense to add any more as this would break the session. Because of this, it makes more sense to choose a simpler pattern, like custom command or a standalone function module that you import to your test.",{"type":27,"tag":28,"props":10947,"children":10948},{},[10949,10951,10957,10958,10963,10965,10971],{"type":32,"value":10950},"Hopefully you enjoyed this blogpost, let me know what you think by reaching out to me on ",{"type":27,"tag":172,"props":10952,"children":10955},{"href":10953,"rel":10954},"https://www.linkedin.com/in/filip-hric-11a5b1126/",[696],[10956],{"type":32,"value":1598},{"type":32,"value":3372},{"type":27,"tag":172,"props":10959,"children":10961},{"href":5770,"rel":10960},[696],[10962],{"type":32,"value":1589},{"type":32,"value":10964},", or on my ",{"type":27,"tag":172,"props":10966,"children":10968},{"href":8322,"rel":10967},[696],[10969],{"type":32,"value":10970},"Discord",{"type":32,"value":10972}," server.",{"title":5,"searchDepth":320,"depth":320,"links":10974},[10975,10976,10977,10978,10979,10980,10981],{"id":10420,"depth":320,"text":10423},{"id":10461,"depth":320,"text":10464},{"id":10524,"depth":320,"text":10527},{"id":10605,"depth":320,"text":10608},{"id":10783,"depth":320,"text":10786},{"id":10824,"depth":320,"text":10827},{"id":10892,"depth":320,"text":10895},"content:use-session-instead-of-login-page-object-in-cypress:index.md","use-session-instead-of-login-page-object-in-cypress/index.md","use-session-instead-of-login-page-object-in-cypress/index",{"_path":10986,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":10987,"description":10988,"date":10989,"published":10,"slug":10990,"tags":10991,"cypressVersion":10994,"readingTime":10995,"body":10999,"_type":329,"_id":11443,"_source":331,"_file":11444,"_stem":11445,"_extension":334},"/testing-pdf-file-with-cypress","Testing a PDF file with Cypress","How to download a PDF file, check the download and parse out the content of the file for further testing","2022-08-08","testing-pdf-file-with-cypress",[5279,10992,10993],"pdf","download","v11.0.0",{"text":927,"minutes":10996,"time":10997,"words":10998},4.84,290400,968,{"type":24,"children":11000,"toc":11439},[11001,11006,11020,11028,11034,11039,11048,11075,11088,11108,11134,11154,11160,11172,11180,11192,11215,11224,11266,11276,11319,11324,11332,11344,11355,11375,11382,11394,11403,11424,11434],{"type":27,"tag":28,"props":11002,"children":11003},{},[11004],{"type":32,"value":11005},"I recently got a question on LinkedIn about Cypress’ ability to test contents of PDF file. At first I thought it is not possible as Cypress is made for testing web applications. But after I thought about it a little more I realized, there are actually couple of ways to approach this problem.",{"type":27,"tag":28,"props":11007,"children":11008},{},[11009,11011,11018],{"type":32,"value":11010},"Let’s start with a description of our app. You can clone it ",{"type":27,"tag":172,"props":11012,"children":11015},{"href":11013,"rel":11014},"https://github.com/filiphric/testing-pdf-with-cypress",[696],[11016],{"type":32,"value":11017},"from my GitHub page and see the final solution",{"type":32,"value":11019}," described in this blogpost. Basically it’s just a simple html file containing links to two PDF files. Clicking on a button will download them to your computer.",{"type":27,"tag":28,"props":11021,"children":11022},{},[11023],{"type":27,"tag":959,"props":11024,"children":11027},{"alt":11025,"src":11026},"Page with a download PDF link","download-pdf.png",[],{"type":27,"tag":45,"props":11029,"children":11031},{"id":11030},"verifying-download",[11032],{"type":32,"value":11033},"Verifying download",{"type":27,"tag":28,"props":11035,"children":11036},{},[11037],{"type":32,"value":11038},"To start off, we can write a simple test to download our file. The test code for this will be simple:",{"type":27,"tag":793,"props":11040,"children":11043},{"className":11041,"code":11042,"language":3520,"meta":5},[3517],"cy.visit('/')\ncy.contains('simple.pdf')\n  .click()\n",[11044],{"type":27,"tag":653,"props":11045,"children":11046},{"__ignoreMap":5},[11047],{"type":32,"value":11042},{"type":27,"tag":28,"props":11049,"children":11050},{},[11051,11053,11059,11061,11067,11069,11074],{"type":32,"value":11052},"This test will finish right after we click on our button. But how do we know if anything happened? Well first of all, we can check that manually, by taking a look into ",{"type":27,"tag":653,"props":11054,"children":11056},{"className":11055},[],[11057],{"type":32,"value":11058},"/cypress/downloads",{"type":32,"value":11060}," folder, where all of our downloads from test run end up. The destination of downloads can be set up by changing ",{"type":27,"tag":653,"props":11062,"children":11064},{"className":11063},[],[11065],{"type":32,"value":11066},"downloadsFolder",{"type":32,"value":11068}," attribute in ",{"type":27,"tag":653,"props":11070,"children":11072},{"className":11071},[],[11073],{"type":32,"value":5669},{"type":32,"value":7538},{"type":27,"tag":28,"props":11076,"children":11077},{},[11078,11080,11086],{"type":32,"value":11079},"But how do we actually check whether the file was downloaded? The easiest way of doing so would be to use ",{"type":27,"tag":653,"props":11081,"children":11083},{"className":11082},[],[11084],{"type":32,"value":11085},"cy.readFile()",{"type":32,"value":11087}," command. This command will fail if a file is not found, so it’s perfect for our situation.",{"type":27,"tag":28,"props":11089,"children":11090},{},[11091,11093,11099,11101,11106],{"type":32,"value":11092},"However, it’s important to note that when we run our tests via ",{"type":27,"tag":653,"props":11094,"children":11096},{"className":11095},[],[11097],{"type":32,"value":11098},"npx cypress open",{"type":32,"value":11100},", downloaded files will get overriden. This is also important, because we can get to a false positive situation when we use ",{"type":27,"tag":653,"props":11102,"children":11104},{"className":11103},[],[11105],{"type":32,"value":11085},{"type":32,"value":11107}," command and a file with the same name was present in downloads folder prior to running the test.",{"type":27,"tag":28,"props":11109,"children":11110},{},[11111,11113,11118,11120,11126,11128,11133],{"type":32,"value":11112},"This is not the case with ",{"type":27,"tag":653,"props":11114,"children":11116},{"className":11115},[],[11117],{"type":32,"value":10038},{"type":32,"value":11119}," script as it will automatically delete contents of downloads folder before running. To change this behavior, you can set up ",{"type":27,"tag":653,"props":11121,"children":11123},{"className":11122},[],[11124],{"type":32,"value":11125},"trashAssetsBeforeRuns",{"type":32,"value":11127}," option in you ",{"type":27,"tag":653,"props":11129,"children":11131},{"className":11130},[],[11132],{"type":32,"value":5669},{"type":32,"value":7538},{"type":27,"tag":28,"props":11135,"children":11136},{},[11137,11139,11145,11147,11152],{"type":32,"value":11138},"Also, while writing your tests, I’d recommend adding ",{"type":27,"tag":653,"props":11140,"children":11142},{"className":11141},[],[11143],{"type":32,"value":11144},"cypress/downloads",{"type":32,"value":11146}," folder into your ",{"type":27,"tag":653,"props":11148,"children":11150},{"className":11149},[],[11151],{"type":32,"value":4244},{"type":32,"value":11153}," file so that it does not accidentally end up bloating your repository size.",{"type":27,"tag":45,"props":11155,"children":11157},{"id":11156},"checking-contents-of-the-file",[11158],{"type":32,"value":11159},"Checking contents of the file",{"type":27,"tag":28,"props":11161,"children":11162},{},[11163,11165,11170],{"type":32,"value":11164},"While ",{"type":27,"tag":653,"props":11166,"children":11168},{"className":11167},[],[11169],{"type":32,"value":11085},{"type":32,"value":11171}," works for making sure the file was downloaded, it doesn’t do a good job with our PDF file. Ironically, there’s a problem with the one thing that the file promises to do. Read file. Just take a look into the console output for the command:",{"type":27,"tag":28,"props":11173,"children":11174},{},[11175],{"type":27,"tag":959,"props":11176,"children":11179},{"alt":11177,"src":11178},"PDF file content read by Cypress cy.readFile() command","pdf-content.png",[],{"type":27,"tag":28,"props":11181,"children":11182},{},[11183,11185,11190],{"type":32,"value":11184},"Unfortunately, there is no native way for Cypress to read the contents of our file, so we need to make our own. It’s actually pretty easy using ",{"type":27,"tag":653,"props":11186,"children":11188},{"className":11187},[],[11189],{"type":32,"value":9707},{"type":32,"value":11191}," but there are couple of small gotchas which need to be taken care of.",{"type":27,"tag":28,"props":11193,"children":11194},{},[11195,11197,11204,11206,11213],{"type":32,"value":11196},"First of all, let’s create our script. A quick search for pdf parsing on ",{"type":27,"tag":172,"props":11198,"children":11201},{"href":11199,"rel":11200},"http://npmjs.com/",[696],[11202],{"type":32,"value":11203},"npmjs.com",{"type":32,"value":11205}," will guide us to a neat little package called ",{"type":27,"tag":172,"props":11207,"children":11210},{"href":11208,"rel":11209},"https://www.npmjs.com/package/pdf-parse",[696],[11211],{"type":32,"value":11212},"pdf-parse",{"type":32,"value":11214},". The usage of this package is very nicely explained on its readme page, so let’s no make it our own.",{"type":27,"tag":793,"props":11216,"children":11219},{"className":11217,"code":11218,"language":3520,"meta":5},[3517],"const fs = require(\"fs\");\nconst path = require('path')\nconst pdf = require('pdf-parse');\n\nexport const readPdf = (pathToPdf: string) => {\n\n  const resolvedPath = path.resolve(pathToPdf)\n  let dataBuffer = fs.readFileSync(resolvedPath);\n  pdf(dataBuffer).then(function ({ text }) {\n\n    return text\n\n  });\n\n}\n",[11220],{"type":27,"tag":653,"props":11221,"children":11222},{"__ignoreMap":5},[11223],{"type":32,"value":11218},{"type":27,"tag":28,"props":11225,"children":11226},{},[11227,11229,11235,11237,11243,11245,11250,11252,11258,11260,11265],{"type":32,"value":11228},"We now have a ",{"type":27,"tag":653,"props":11230,"children":11232},{"className":11231},[],[11233],{"type":32,"value":11234},"readPdf",{"type":32,"value":11236}," function which will take a ",{"type":27,"tag":653,"props":11238,"children":11240},{"className":11239},[],[11241],{"type":32,"value":11242},"pathToPdf",{"type":32,"value":11244}," argument. This will represent a path to our downloads folder. We can now call using ",{"type":27,"tag":653,"props":11246,"children":11248},{"className":11247},[],[11249],{"type":32,"value":9707},{"type":32,"value":11251}," command. But before we are able to do that, we need to add it into our ",{"type":27,"tag":653,"props":11253,"children":11255},{"className":11254},[],[11256],{"type":32,"value":11257},"setupNodeEvents",{"type":32,"value":11259}," function in ",{"type":27,"tag":653,"props":11261,"children":11263},{"className":11262},[],[11264],{"type":32,"value":5669},{"type":32,"value":7538},{"type":27,"tag":793,"props":11267,"children":11271},{"className":11268,"code":11269,"filename":5669,"highlights":11270,"language":3520,"meta":5},[3517],"import { defineConfig } from 'cypress'\nimport { readPdf } from 'cypress/scripts/readPdf'\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents(on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) {\n      on('task', {\n        readPdf\n      })\n    },\n    baseUrl: 'http://localhost:3000'\n  },\n});\n",[320,3668,3723,3746],[11272],{"type":27,"tag":653,"props":11273,"children":11274},{"__ignoreMap":5},[11275],{"type":32,"value":11269},{"type":27,"tag":28,"props":11277,"children":11278},{},[11279,11281,11287,11289,11294,11296,11301,11303,11309,11311,11317],{"type":32,"value":11280},"In the config, we are importing our script, which I saved in the ",{"type":27,"tag":653,"props":11282,"children":11284},{"className":11283},[],[11285],{"type":32,"value":11286},"cypress/scripts",{"type":32,"value":11288}," folder that I created for myself. In ",{"type":27,"tag":653,"props":11290,"children":11292},{"className":11291},[],[11293],{"type":32,"value":11257},{"type":32,"value":11295}," I’m passing this ",{"type":27,"tag":653,"props":11297,"children":11299},{"className":11298},[],[11300],{"type":32,"value":11234},{"type":32,"value":11302}," script. This means that whenever I call ",{"type":27,"tag":653,"props":11304,"children":11306},{"className":11305},[],[11307],{"type":32,"value":11308},"cy.task('readPdf')",{"type":32,"value":11310}," my ",{"type":27,"tag":653,"props":11312,"children":11314},{"className":11313},[],[11315],{"type":32,"value":11316},"cypress/scripts/readPdf",{"type":32,"value":11318}," will be called and will return the contents of my PDF file.",{"type":27,"tag":28,"props":11320,"children":11321},{},[11322],{"type":32,"value":11323},"This now works almost perfectly. There’s a small gotcha here. For some reason we are getting this error:",{"type":27,"tag":28,"props":11325,"children":11326},{},[11327],{"type":27,"tag":959,"props":11328,"children":11331},{"alt":11329,"src":11330},"PDF file content read by Cypress cy.task() command","pdf-task-fail.png",[],{"type":27,"tag":28,"props":11333,"children":11334},{},[11335,11337,11342],{"type":32,"value":11336},"It took me some time before realizing that the reason I’m getting this error is that my function is actually still in the process of working through the PDF file and ",{"type":27,"tag":653,"props":11338,"children":11340},{"className":11339},[],[11341],{"type":32,"value":9707},{"type":32,"value":11343}," is not waiting for it to finish. In order to make sure the function actually finishes doing it’s thing, we need to wrap it inside a promise. While promises can be confusing at first (they definitely were for me), in this case the code is pretty simple:",{"type":27,"tag":793,"props":11345,"children":11350},{"className":11346,"code":11347,"filename":11348,"highlights":11349,"language":3520,"meta":5},[3517],"const fs = require(\"fs\");\nconst path = require('path')\nconst pdf = require('pdf-parse');\n\nexport const readPdf = (pathToPdf: string) => {\n\n  return new Promise((resolve) => {\n    const pdfPath = path.resolve(pathToPdf)\n    let dataBuffer = fs.readFileSync(pdfPath);\n    pdf(dataBuffer).then(function ({ text }) {\n\n      resolve(text)\n\n    });\n  })\n\n}\n","cypress/scripts/readPdf.ts",[3668,3812,3878],[11351],{"type":27,"tag":653,"props":11352,"children":11353},{"__ignoreMap":5},[11354],{"type":32,"value":11347},{"type":27,"tag":28,"props":11356,"children":11357},{},[11358,11360,11365,11367,11373],{"type":32,"value":11359},"This way we can ensure that even if the file takes a little while to parse, Cypress will wait for it to finish. In fact, it will wait up to 60 seconds by default. This number can be changed once again, by modifying ",{"type":27,"tag":653,"props":11361,"children":11363},{"className":11362},[],[11364],{"type":32,"value":5669},{"type":32,"value":11366}," and its ",{"type":27,"tag":653,"props":11368,"children":11370},{"className":11369},[],[11371],{"type":32,"value":11372},"taskTimeout",{"type":32,"value":11374}," option.",{"type":27,"tag":28,"props":11376,"children":11377},{},[11378],{"type":27,"tag":959,"props":11379,"children":11381},{"alt":11329,"src":11380},"pdf-task.png",[],{"type":27,"tag":28,"props":11383,"children":11384},{},[11385,11387,11392],{"type":32,"value":11386},"Our ",{"type":27,"tag":653,"props":11388,"children":11390},{"className":11389},[],[11391],{"type":32,"value":9707},{"type":32,"value":11393}," will yield the text of our PDF to the next command, so we can make an assertion right away:",{"type":27,"tag":793,"props":11395,"children":11398},{"className":11396,"code":11397,"language":3520,"meta":5},[3517],"cy.task('readPdf', 'cypress/downloads/simple.pdf')\n  .should('contain', 'Hello darkness my old friend')\n",[11399],{"type":27,"tag":653,"props":11400,"children":11401},{"__ignoreMap":5},[11402],{"type":32,"value":11397},{"type":27,"tag":28,"props":11404,"children":11405},{},[11406,11408,11414,11416,11422],{"type":32,"value":11407},"Instead of ",{"type":27,"tag":653,"props":11409,"children":11411},{"className":11410},[],[11412],{"type":32,"value":11413},"contain",{"type":32,"value":11415}," we can use ",{"type":27,"tag":653,"props":11417,"children":11419},{"className":11418},[],[11420],{"type":32,"value":11421},"eq",{"type":32,"value":11423}," assertion, but in that case we need to be mindful of all the whitespaces and line breaks that our text will contain. Complexity of reading and parsing the file will grow as the complexity of tested PDF file grows, but there are many ways to handle it. For example, you can organize all your strings to an array. Every phrase or sentence that is separated by a line break will be its own item in an array:",{"type":27,"tag":793,"props":11425,"children":11429},{"className":11426,"code":11427,"filename":11348,"highlights":11428,"language":3520,"meta":5},[3517],"export const readPdf = (pathToPdf: string) => {\n\n  return new Promise((resolve) => {\n    const pdfPath = path.resolve(pathToPdf)\n    let dataBuffer = fs.readFileSync(pdfPath);\n    pdf(dataBuffer).then(function ({ text }) {\n\n      const arr = text.split(\"\\n\");\n      resolve(arr)\n\n    });\n  })\n\n}\n",[3723],[11430],{"type":27,"tag":653,"props":11431,"children":11432},{"__ignoreMap":5},[11433],{"type":32,"value":11427},{"type":27,"tag":28,"props":11435,"children":11436},{},[11437],{"type":32,"value":11438},"That’s about it. If you found this useful, share it with a friend or community. Maybe there’s someone who will benefit from it as well. If you read this far, you might be interested in subscribing to my newsletter and get notified when there’s a new blog.",{"title":5,"searchDepth":320,"depth":320,"links":11440},[11441,11442],{"id":11030,"depth":320,"text":11033},{"id":11156,"depth":320,"text":11159},"content:testing-pdf-file-with-cypress:index.md","testing-pdf-file-with-cypress/index.md","testing-pdf-file-with-cypress/index",{"_path":11447,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":11448,"description":11449,"date":11450,"published":10,"slug":11451,"tags":11452,"cypressVersion":5959,"readingTime":11457,"body":11461,"_type":329,"_id":11779,"_source":331,"_file":11780,"_stem":11781,"_extension":334},"/testing-geolocation-with-cypress","Testing geolocation with Cypress","Explanation on how to test a page that can locate its users either via API call or using browser’s Geolocation API capabilities ","2022-08-02","testing-geolocation-with-cypress",[5279,11453,11454,11455,11456],"geolocation","location","gps","api",{"text":927,"minutes":11458,"time":11459,"words":11460},4.17,250200,834,{"type":24,"children":11462,"toc":11772},[11463,11468,11482,11488,11499,11507,11520,11529,11535,11554,11563,11568,11576,11581,11587,11592,11600,11605,11613,11618,11624,11658,11667,11672,11678,11699,11708,11721,11726,11735,11758],{"type":27,"tag":28,"props":11464,"children":11465},{},[11466],{"type":32,"value":11467},"Today’s websites have various ways of determinig user’s location. Before we know how to test them it is vital we understand them.",{"type":27,"tag":28,"props":11469,"children":11470},{},[11471,11473,11480],{"type":32,"value":11472},"Basically, there are two ways of locating a user. First way is by using API call to a server, which can identify user’s location based on where the user is connecting from. The second way is to use browser’s ",{"type":27,"tag":172,"props":11474,"children":11477},{"href":11475,"rel":11476},"https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API",[696],[11478],{"type":32,"value":11479},"Geolocation interface",{"type":32,"value":11481},", that will provide the frontend application with position coordinates.",{"type":27,"tag":45,"props":11483,"children":11485},{"id":11484},"testing-a-pricing-page",[11486],{"type":32,"value":11487},"Testing a pricing page",{"type":27,"tag":28,"props":11489,"children":11490},{},[11491,11497],{"type":27,"tag":172,"props":11492,"children":11494},{"href":9303,"rel":11493},[696],[11495],{"type":32,"value":11496},"In the application under test",{"type":32,"value":11498},", we have a pricing page. This pricing page will show pricing in different currencies based on whether user is connecting from EU, UK or anywhere from rest of the world.",{"type":27,"tag":28,"props":11500,"children":11501},{},[11502],{"type":27,"tag":959,"props":11503,"children":11506},{"alt":11504,"src":11505},"Pricing page","pricing.png",[],{"type":27,"tag":28,"props":11508,"children":11509},{},[11510,11512,11518],{"type":32,"value":11511},"Moreover, pricing page uses Purchasing Power Parity to provide a discount to some of the countries. All of this is handled by a ",{"type":27,"tag":653,"props":11513,"children":11515},{"className":11514},[],[11516],{"type":32,"value":11517},"GET /api/location",{"type":32,"value":11519}," endpoint. This points to a service that will determine user’s location and return the details about the country and some other details. The response from server will look something like this:",{"type":27,"tag":793,"props":11521,"children":11524},{"className":11522,"code":11523,"language":1004,"meta":5},[1002],"{\n  \"location\": \"sk\",\n  \"currency\": \"EUR\",\n  \"discountEligible\": true,\n  \"discountAmount\": 20\n}\n",[11525],{"type":27,"tag":653,"props":11526,"children":11527},{"__ignoreMap":5},[11528],{"type":32,"value":11523},{"type":27,"tag":45,"props":11530,"children":11532},{"id":11531},"intercepting-location-request",[11533],{"type":32,"value":11534},"Intercepting location request",{"type":27,"tag":28,"props":11536,"children":11537},{},[11538,11540,11545,11546,11552],{"type":32,"value":11539},"To determine if our frontend works as intended, we can intercept our API call and provide our own data as response. For example we can change the ",{"type":27,"tag":653,"props":11541,"children":11543},{"className":11542},[],[11544],{"type":32,"value":11454},{"type":32,"value":4164},{"type":27,"tag":653,"props":11547,"children":11549},{"className":11548},[],[11550],{"type":32,"value":11551},"currency",{"type":32,"value":11553}," attributes, by writing our test like this:",{"type":27,"tag":793,"props":11555,"children":11558},{"className":11556,"code":11557,"language":1513,"meta":5},[1510],"cy.intercept('GET', '/api/location', {\n  location: 'UK',\n  currency: 'GBP'\n})\n\ncy.visit('/pricing')\n",[11559],{"type":27,"tag":653,"props":11560,"children":11561},{"__ignoreMap":5},[11562],{"type":32,"value":11557},{"type":27,"tag":28,"props":11564,"children":11565},{},[11566],{"type":32,"value":11567},"This will render our page as if it was opened in Great Britain region, with £ as the main currency. Notice how price changed as well. This is something which usually needs a test, so that we can make sure prices show up as intended.",{"type":27,"tag":28,"props":11569,"children":11570},{},[11571],{"type":27,"tag":959,"props":11572,"children":11575},{"alt":11573,"src":11574},"Intercepting location","intercept-location.png",[],{"type":27,"tag":28,"props":11577,"children":11578},{},[11579],{"type":32,"value":11580},"This opens up a possibility to test different combinations of currencies, locations and discount eligibility. Since our frontend relies on API to determine user’s location, we are isolating the frontend and testing different cases based on mocked API response.",{"type":27,"tag":45,"props":11582,"children":11584},{"id":11583},"browser-geolocation",[11585],{"type":32,"value":11586},"Browser Geolocation",{"type":27,"tag":28,"props":11588,"children":11589},{},[11590],{"type":32,"value":11591},"With browser Geolocation API, users can allow browser to provide position coordinates to the application. Many sites use this, to determine your location and provide you e.g. with suggestions of restaurants near you. If you have ever seen this dialog, you’ve seen the Geolocation API in action:",{"type":27,"tag":28,"props":11593,"children":11594},{},[11595],{"type":27,"tag":959,"props":11596,"children":11599},{"alt":11597,"src":11598},"Geolocation prompt","geolocation-dialog.png",[],{"type":27,"tag":28,"props":11601,"children":11602},{},[11603],{"type":32,"value":11604},"In our application, we have a \"Find My Location\" button, that will reveal a map with your current location when clicked. When trying to automate this flow in Cypress, we immediately stumble upon a problem:",{"type":27,"tag":28,"props":11606,"children":11607},{},[11608],{"type":27,"tag":959,"props":11609,"children":11612},{"alt":11610,"src":11611},"Geolocation prompt in Cypress","geolocation-cypress.png",[],{"type":27,"tag":28,"props":11614,"children":11615},{},[11616],{"type":32,"value":11617},"In the top left corner you can see that our location prompt is appearing in Cypress window. Since Cypress is running inside the browser, there’s no way of confirming this dialog. However, there are two ways we can approach this. Using a plugin to allow Geolocation automatically, or stubbing it.",{"type":27,"tag":45,"props":11619,"children":11621},{"id":11620},"browser-permissions-plugin",[11622],{"type":32,"value":11623},"Browser permissions plugin",{"type":27,"tag":28,"props":11625,"children":11626},{},[11627,11634,11636,11641,11643,11648,11650,11656],{"type":27,"tag":172,"props":11628,"children":11631},{"href":11629,"rel":11630},"https://github.com/kamranayub/cypress-browser-permissions",[696],[11632],{"type":32,"value":11633},"With this neat plugin",{"type":32,"value":11635},", we can enable or disable certain browser permissions right within our test suite. There are couple of steps we need to follow in order to make this plugin work. After we install the plugin, we need to register a function within ",{"type":27,"tag":653,"props":11637,"children":11639},{"className":11638},[],[11640],{"type":32,"value":11257},{"type":32,"value":11642}," function. Finally, we need to provide out ",{"type":27,"tag":653,"props":11644,"children":11646},{"className":11645},[],[11647],{"type":32,"value":9529},{"type":32,"value":11649}," with ",{"type":27,"tag":653,"props":11651,"children":11653},{"className":11652},[],[11654],{"type":32,"value":11655},"browserPermissions",{"type":32,"value":11657}," object, where we define rules for different permission. All of this is explained in plugin’s README.md file.",{"type":27,"tag":793,"props":11659,"children":11662},{"className":11660,"code":11661,"filename":6028,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\nimport { cypressBrowserPermissionsPlugin } from 'cypress-browser-permissions'\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n      config = cypressBrowserPermissionsPlugin(on, config)\n    },\n    env: {\n      'browserPermissions': {\n        'geolocation': 'allow',\n      }\n    }\n  }\n})\n",[11663],{"type":27,"tag":653,"props":11664,"children":11665},{"__ignoreMap":5},[11666],{"type":32,"value":11661},{"type":27,"tag":28,"props":11668,"children":11669},{},[11670],{"type":32,"value":11671},"Setting up this plugin will automatically allow Geolocation. You can imagine this as automatically clicking that \"allow\" button in the mentioned dialog.",{"type":27,"tag":45,"props":11673,"children":11675},{"id":11674},"stubbing-the-location",[11676],{"type":32,"value":11677},"Stubbing the location",{"type":27,"tag":28,"props":11679,"children":11680},{},[11681,11683,11689,11691,11697],{"type":32,"value":11682},"When using Geolocation API, our frontend will call this API within browser. The Geolocation API is available in ",{"type":27,"tag":653,"props":11684,"children":11686},{"className":11685},[],[11687],{"type":32,"value":11688},"window",{"type":32,"value":11690}," object under ",{"type":27,"tag":653,"props":11692,"children":11694},{"className":11693},[],[11695],{"type":32,"value":11696},"navigator",{"type":32,"value":11698},". In other words, when an application wants to get user’s location from browser, it will call a function that looks something like this:",{"type":27,"tag":793,"props":11700,"children":11703},{"className":11701,"code":11702,"language":1513,"meta":5},[1510],"window.navigator.geolocation.getCurrentPosition()\n",[11704],{"type":27,"tag":653,"props":11705,"children":11706},{"__ignoreMap":5},[11707],{"type":32,"value":11702},{"type":27,"tag":28,"props":11709,"children":11710},{},[11711,11713,11719],{"type":32,"value":11712},"This function call will either return the position coordinates or throw an error. There are multiple ways of handling this and you can read about them in the ",{"type":27,"tag":172,"props":11714,"children":11716},{"href":11475,"rel":11715},[696],[11717],{"type":32,"value":11718},"Geolocation API documentation",{"type":32,"value":11720},". In our application, we are taking the coordinates and send them to a map object, which will render our map position based given coordinates.",{"type":27,"tag":28,"props":11722,"children":11723},{},[11724],{"type":32,"value":11725},"With Cypress we can substitute the real coordinates that our browser would provide an instead use our own. This means that our test will display the map the same way whether it is running locally on your computer or on a pipeline somewhere on the other side of the world.",{"type":27,"tag":793,"props":11727,"children":11730},{"className":11728,"code":11729,"language":1513,"meta":5},[1510],"cy.visit('/pricing', {\n    onBeforeLoad ({ navigator }) {\n      // Košice city\n      const latitude = 48.71597183246423\n      const longitude = 21.255670821215418\n     cy.stub(navigator.geolocation, 'getCurrentPosition')\n       .callsArgWith(0, { coords: { latitude, longitude } })\n    }\n  })\n",[11731],{"type":27,"tag":653,"props":11732,"children":11733},{"__ignoreMap":5},[11734],{"type":32,"value":11729},{"type":27,"tag":28,"props":11736,"children":11737},{},[11738,11740,11747,11749,11756],{"type":32,"value":11739},"I found this solution on ",{"type":27,"tag":172,"props":11741,"children":11744},{"href":11742,"rel":11743},"https://stackoverflow.com/questions/62634691/cypress-mock-geolocation",[696],[11745],{"type":32,"value":11746},"stackoverflow",{"type":32,"value":11748},", and it was nicely demonstrated in a ",{"type":27,"tag":172,"props":11750,"children":11753},{"href":11751,"rel":11752},"https://www.youtube.com/watch?v=3aryQnTQrJs&list=PLNQq42pqd-rzeW-w40zJDRtqFA-NQnxPl&index=14",[696],[11754],{"type":32,"value":11755},"neat video by Ioan Solderea",{"type":32,"value":11757}," as well.",{"type":27,"tag":28,"props":11759,"children":11760},{},[11761,11763,11770],{"type":32,"value":11762},"While this may feel like a hack, it’s actually a pretty good solution for our test. Mostly because we are dealing with functionality of the actual browser here. This functionality is not part of our application’s codebase. But we are using data from that API and we need to make sure it is handled properly. That’s why it would be valuable to make sure that our map will actually show up in our DOM. Visual testing tools like ",{"type":27,"tag":172,"props":11764,"children":11767},{"href":11765,"rel":11766},"https://applitools.com/",[696],[11768],{"type":32,"value":11769},"Applitools",{"type":32,"value":11771}," are really useful in these types of scenarios.",{"title":5,"searchDepth":320,"depth":320,"links":11773},[11774,11775,11776,11777,11778],{"id":11484,"depth":320,"text":11487},{"id":11531,"depth":320,"text":11534},{"id":11583,"depth":320,"text":11586},{"id":11620,"depth":320,"text":11623},{"id":11674,"depth":320,"text":11677},"content:testing-geolocation-with-cypress:index.md","testing-geolocation-with-cypress/index.md","testing-geolocation-with-cypress/index",{"_path":11783,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":11784,"description":11785,"date":11786,"published":10,"slug":11787,"tags":11788,"cypressVersion":5959,"readingTime":11792,"body":11796,"_type":329,"_id":12192,"_source":331,"_file":12193,"_stem":12194,"_extension":334},"/cypress-basics-uploading-file","Cypress basics: Uploading a file","Short explanation on how to upload a file with Cypress to a drag and drop element, inputs or directly by calling your API","2022-07-27","cypress-basics-uploading-file",[5279,11789,11790,11791],"upload","dropzone","file",{"text":927,"minutes":11793,"time":11794,"words":11795},4.37,262200,874,{"type":24,"children":11797,"toc":12187},[11798,11808,11813,11819,11839,11848,11861,11866,11886,11892,11897,11905,11931,11944,11952,11972,11981,12000,12006,12011,12020,12069,12096,12117,12122,12157,12162],{"type":27,"tag":1029,"props":11799,"children":11800},{},[11801,11805],{"type":27,"tag":28,"props":11802,"children":11803},{},[11804],{"type":32,"value":5973},{"type":27,"tag":5975,"props":11806,"children":11807},{},[],{"type":27,"tag":28,"props":11809,"children":11810},{},[11811],{"type":32,"value":11812},"File uploading can be done in various ways, but all of them have a couple of things in common. Most notably, when dealing with file upload, we need to have our frontend ready to accept the file, and then we need to have our backend ready to handle the file. Let’s start with frontend and how we can make an upload using Cypress.",{"type":27,"tag":45,"props":11814,"children":11816},{"id":11815},"uploading-a-file-with-cypress",[11817],{"type":32,"value":11818},"Uploading a file with Cypress",{"type":27,"tag":28,"props":11820,"children":11821},{},[11822,11829,11831,11837],{"type":27,"tag":172,"props":11823,"children":11826},{"href":11824,"rel":11825},"https://docs.cypress.io/guides/references/changelog#9-3-0",[696],[11827],{"type":32,"value":11828},"Starting with version 9.3.0",{"type":32,"value":11830},", Cypress has a ",{"type":27,"tag":653,"props":11832,"children":11834},{"className":11833},[],[11835],{"type":32,"value":11836},".selectFile()",{"type":32,"value":11838}," command which can handle all the file uploads you’ll need. The usage is simple:",{"type":27,"tag":793,"props":11840,"children":11843},{"className":11841,"code":11842,"language":1513,"meta":5},[1510],"cy.get('#upload')\n  .selectFile('cypress/fixtures/logo.png')\n",[11844],{"type":27,"tag":653,"props":11845,"children":11846},{"__ignoreMap":5},[11847],{"type":32,"value":11842},{"type":27,"tag":28,"props":11849,"children":11850},{},[11851,11853,11859],{"type":32,"value":11852},"So, which element do we need to select? This is where we get to dive into the code a little. Everytime you do an upload, there is an ",{"type":27,"tag":653,"props":11854,"children":11856},{"className":11855},[],[11857],{"type":32,"value":11858},"\u003Cinput type=file>",{"type":32,"value":11860}," element present on the page. Even if you don’t see it, I assure you it’s there. It’s an HTML5 element that provides your application an API to communicate with your browser and open that \"choose file\" window. This is how this element normally renders on page:",{"type":27,"tag":28,"props":11862,"children":11863},{},[11864],{"type":32,"value":11865},"![Choose a file to upload](choose-file.png w-1/2)",{"type":27,"tag":28,"props":11867,"children":11868},{},[11869,11871,11877,11879,11884],{"type":32,"value":11870},"However, when we want to upload a file with Cypress, we don’t click this button, but select the ",{"type":27,"tag":653,"props":11872,"children":11874},{"className":11873},[],[11875],{"type":32,"value":11876},"\u003Cinput>",{"type":32,"value":11878}," element and use ",{"type":27,"tag":653,"props":11880,"children":11882},{"className":11881},[],[11883],{"type":32,"value":11836},{"type":32,"value":11885}," function on it. This way, instead of interacting with a dialog window to choose a file, we just specify a path to the file we want to to upload. But what if we don’t see the \"Choose file\" button, but instead we have a upload button or a dropzone area?",{"type":27,"tag":45,"props":11887,"children":11889},{"id":11888},"uploading-to-a-dropzone",[11890],{"type":32,"value":11891},"Uploading to a dropzone",{"type":27,"tag":28,"props":11893,"children":11894},{},[11895],{"type":32,"value":11896},"Many pages choose to render a slightly nicer UI, where client can just drag and drop a file or click a nicely styled button. This may look something like this:",{"type":27,"tag":28,"props":11898,"children":11899},{},[11900],{"type":27,"tag":959,"props":11901,"children":11904},{"alt":11902,"src":11903},"Dropzone UI","dropzone.png",[],{"type":27,"tag":28,"props":11906,"children":11907},{},[11908,11910,11915,11917,11922,11924,11929],{"type":32,"value":11909},"In cases like this, the ",{"type":27,"tag":653,"props":11911,"children":11913},{"className":11912},[],[11914],{"type":32,"value":11876},{"type":32,"value":11916}," element is often hidden. The interesting bit about this is that the ",{"type":27,"tag":653,"props":11918,"children":11920},{"className":11919},[],[11921],{"type":32,"value":11876},{"type":32,"value":11923}," element can be found in weird places in the DOM, often away from the dropzone area. This is because the insertion of the file is handled by JavaScript. You can imagine it as if your file gets taken from the dropzone and passed into the ",{"type":27,"tag":653,"props":11925,"children":11927},{"className":11926},[],[11928],{"type":32,"value":11876},{"type":32,"value":11930}," element where it gets handled.",{"type":27,"tag":28,"props":11932,"children":11933},{},[11934,11936,11942],{"type":32,"value":11935},"In my ",{"type":27,"tag":172,"props":11937,"children":11939},{"href":9303,"rel":11938},[696],[11940],{"type":32,"value":11941},"Trelloapp project",{"type":32,"value":11943},", the dropzone looks something like this:",{"type":27,"tag":28,"props":11945,"children":11946},{},[11947],{"type":27,"tag":959,"props":11948,"children":11951},{"alt":11949,"src":11950},"Dropzone DOM","dropzone-input.png",[],{"type":27,"tag":28,"props":11953,"children":11954},{},[11955,11957,11962,11964,11970],{"type":32,"value":11956},"You’ll see that the the ",{"type":27,"tag":653,"props":11958,"children":11960},{"className":11959},[],[11961],{"type":32,"value":11876},{"type":32,"value":11963}," element has a style of ",{"type":27,"tag":653,"props":11965,"children":11967},{"className":11966},[],[11968],{"type":32,"value":11969},"display: none",{"type":32,"value":11971}," and therefore is hidden from user. To upload a file to this dropzone we can choose one of three strategies:",{"type":27,"tag":793,"props":11973,"children":11976},{"className":11974,"code":11975,"language":1513,"meta":5},[1510],"// input is invisible, so we need to skip Cypress UI checks\ncy.get('input[type=file]')\n  .selectFile('cypress/fixtures/logo.png', { force: true })\n\n// make our input visible by invoking a jQuery function to it\ncy.get('input[type=file]')\n  .invoke('show')\n  .selectFile('cypress/fixtures/logo.png')\n\n// use Cypress’ abilty to handle dropzones\ncy.get('[data-cy=upload-image]')\n  .selectFile('cypress/fixtures/logo.png', { action: 'drag-drop' })\n",[11977],{"type":27,"tag":653,"props":11978,"children":11979},{"__ignoreMap":5},[11980],{"type":32,"value":11975},{"type":27,"tag":28,"props":11982,"children":11983},{},[11984,11986,11991,11993,11999],{"type":32,"value":11985},"Notice how in the third example, we are selecting the whole dropzone itself instead of targeting the ",{"type":27,"tag":653,"props":11987,"children":11989},{"className":11988},[],[11990],{"type":32,"value":11876},{"type":32,"value":11992}," element. This is important, because there’s a difference between how these two strategies are used. If we were to select the wrong element, we might end up with a message like: ",{"type":27,"tag":653,"props":11994,"children":11996},{"className":11995},[],[11997],{"type":32,"value":11998},"cy.selectFile() can only be called on an \u003Cinput type=\"file\"> or a \u003Clabel for=\"fileInput\"> pointing to or containing one",{"type":32,"value":256},{"type":27,"tag":45,"props":12001,"children":12003},{"id":12002},"upload-via-api",[12004],{"type":32,"value":12005},"Upload via API",{"type":27,"tag":28,"props":12007,"children":12008},{},[12009],{"type":32,"value":12010},"The second part of this story is when our image is sent to the server. Our frontend can handle the image upload in various ways, so there might be some slight differences in how file uploads are handled in your application, but a general idea goes something like this:",{"type":27,"tag":793,"props":12012,"children":12015},{"className":12013,"code":12014,"language":1513,"meta":5},[1510],"cy.fixture('logo.png', 'binary').then( image => {\n  const blob = Cypress.Blob.binaryStringToBlob(image, 'image/png');\n  const formData = new FormData();\n  formData.append('image', blob, 'logo.png');\n\n    cy.request({\n      method: 'POST', \n      url: '/api/upload',\n      body: formData,\n      headers: {\n        'content-type': 'multipart/form-data'\n      },\n    })\n  })\n",[12016],{"type":27,"tag":653,"props":12017,"children":12018},{"__ignoreMap":5},[12019],{"type":32,"value":12014},{"type":27,"tag":28,"props":12021,"children":12022},{},[12023,12025,12030,12032,12038,12040,12045,12047,12052,12054,12060,12062,12067],{"type":32,"value":12024},"Let me break this down now. Our ",{"type":27,"tag":653,"props":12026,"children":12028},{"className":12027},[],[12029],{"type":32,"value":11858},{"type":32,"value":12031}," element is usually a part of an html ",{"type":27,"tag":653,"props":12033,"children":12035},{"className":12034},[],[12036],{"type":32,"value":12037},"\u003Cform>",{"type":32,"value":12039},". Usually, ",{"type":27,"tag":653,"props":12041,"children":12043},{"className":12042},[],[12044],{"type":32,"value":12037},{"type":32,"value":12046}," element contains multiple ",{"type":27,"tag":653,"props":12048,"children":12050},{"className":12049},[],[12051],{"type":32,"value":11876},{"type":32,"value":12053}," elements. Before they are sent over to API, they are handled by ",{"type":27,"tag":653,"props":12055,"children":12057},{"className":12056},[],[12058],{"type":32,"value":12059},"FormData",{"type":32,"value":12061}," interface. Basically, every piece of data is appended to the ",{"type":27,"tag":653,"props":12063,"children":12065},{"className":12064},[],[12066],{"type":32,"value":12059},{"type":32,"value":12068}," object and then send to the server using API.",{"type":27,"tag":28,"props":12070,"children":12071},{},[12072,12074,12079,12081,12087,12089,12094],{"type":32,"value":12073},"In Cypress, we need to create this ",{"type":27,"tag":653,"props":12075,"children":12077},{"className":12076},[],[12078],{"type":32,"value":12059},{"type":32,"value":12080}," manually and append our image to it. As you may have noticed, before we ",{"type":27,"tag":653,"props":12082,"children":12084},{"className":12083},[],[12085],{"type":32,"value":12086},".append()",{"type":32,"value":12088}," our image to the ",{"type":27,"tag":653,"props":12090,"children":12092},{"className":12091},[],[12093],{"type":32,"value":12059},{"type":32,"value":12095}," object, we are handling the image. This happens in two ways:",{"type":27,"tag":851,"props":12097,"children":12098},{},[12099,12112],{"type":27,"tag":109,"props":12100,"children":12101},{},[12102,12104,12110],{"type":32,"value":12103},"we load our image as a fixture, using ",{"type":27,"tag":653,"props":12105,"children":12107},{"className":12106},[],[12108],{"type":32,"value":12109},"binary",{"type":32,"value":12111}," encoding",{"type":27,"tag":109,"props":12113,"children":12114},{},[12115],{"type":32,"value":12116},"we convert this binary encoded image to Blob",{"type":27,"tag":28,"props":12118,"children":12119},{},[12120],{"type":32,"value":12121},"Blob stands for \"binary large object\" - in other words, we are converting our image to text. This is normally something that is handled by our frontend application. The reason why we need to do this manually in the Cypress test, is because we are avoiding usage of our frontend. In a way, our test is going to behave the same way as our applicatioun would.",{"type":27,"tag":28,"props":12123,"children":12124},{},[12125,12127,12132,12134,12140,12142,12147,12149,12155],{"type":32,"value":12126},"Once we handle our file and fill in our ",{"type":27,"tag":653,"props":12128,"children":12130},{"className":12129},[],[12131],{"type":32,"value":12059},{"type":32,"value":12133}," object, we are ready to call our API. The ",{"type":27,"tag":653,"props":12135,"children":12137},{"className":12136},[],[12138],{"type":32,"value":12139},"body",{"type":32,"value":12141}," of our request will be the ",{"type":27,"tag":653,"props":12143,"children":12145},{"className":12144},[],[12146],{"type":32,"value":12059},{"type":32,"value":12148}," object itself. The only additional detail is to add ",{"type":27,"tag":653,"props":12150,"children":12152},{"className":12151},[],[12153],{"type":32,"value":12154},"'content-type': 'multipart/form-data'",{"type":32,"value":12156}," header, so that the server knows we are sending this type of request.",{"type":27,"tag":28,"props":12158,"children":12159},{},[12160],{"type":32,"value":12161},"As I mentioned, the final solution for uploading a file via API is going to depend on the API and the application you are testing, but the general idea should be pretty similar to the example given above.",{"type":27,"tag":28,"props":12163,"children":12164},{},[12165,12167,12172,12173,12178,12179,12185],{"type":32,"value":12166},"Hope that this helps. If you found this helpful, send it to a friend that might find this helpful. For more tips like this, me on ",{"type":27,"tag":172,"props":12168,"children":12170},{"href":5770,"rel":12169},[696],[12171],{"type":32,"value":1589},{"type":32,"value":3372},{"type":27,"tag":172,"props":12174,"children":12176},{"href":10953,"rel":12175},[696],[12177],{"type":32,"value":1598},{"type":32,"value":3372},{"type":27,"tag":172,"props":12180,"children":12183},{"href":12181,"rel":12182},"https://www.youtube.com/channel/UCDOCAVIhSh5VpJMEfdak1OA",[696],[12184],{"type":32,"value":8214},{"type":32,"value":12186},", or subscribe to my newsletter.",{"title":5,"searchDepth":320,"depth":320,"links":12188},[12189,12190,12191],{"id":11815,"depth":320,"text":11818},{"id":11888,"depth":320,"text":11891},{"id":12002,"depth":320,"text":12005},"content:cypress-basics-uploading-file:index.md","cypress-basics-uploading-file/index.md","cypress-basics-uploading-file/index",{"_path":12196,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":12197,"description":12198,"date":12199,"published":10,"slug":12200,"tags":12201,"cypressVersion":5959,"readingTime":12204,"body":12209,"_type":329,"_id":12950,"_source":331,"_file":12951,"_stem":12952,"_extension":334},"/8-common-mistakes-in-cypress-and-how-to-avoid-them","8 common mistakes in Cypress (and how to avoid them)","In this blogpost I’m sharing a couple of testing antipatterns that can make your test flaky, hard to read or slow.","2022-07-14","8-common-mistakes-in-cypress-and-how-to-avoid-them",[5279,12202,12203],"best practices","flakiness",{"text":12205,"minutes":12206,"time":12207,"words":12208},"10 min read",9.18,550800,1836,{"type":24,"children":12210,"toc":12940},[12211,12224,12229,12235,12247,12256,12261,12270,12289,12295,12308,12326,12331,12351,12361,12371,12377,12382,12390,12395,12404,12409,12429,12451,12461,12467,12472,12481,12502,12507,12512,12521,12527,12532,12541,12546,12653,12673,12683,12703,12713,12719,12724,12729,12738,12759,12768,12774,12779,12788,12809,12814,12827,12836,12842,12854,12863,12868,12903,12913,12927],{"type":27,"tag":28,"props":12212,"children":12213},{},[12214,12216,12223],{"type":32,"value":12215},"This is a blog post made from a talk I gave at Front end test fest, so if you want to watch a video about it, ",{"type":27,"tag":172,"props":12217,"children":12220},{"href":12218,"rel":12219},"http://front-endtestfest.com/6gm",[696],[12221],{"type":32,"value":12222},"feel free to do so on this link",{"type":32,"value":256},{"type":27,"tag":28,"props":12225,"children":12226},{},[12227],{"type":32,"value":12228},"On my Discord server, I sometimes encounter a common pattern when answering questions. There are certain sets of problems that tend to surface repeatedly and for these, I created this blog post. Let’s jump into them!",{"type":27,"tag":45,"props":12230,"children":12232},{"id":12231},"_1-using-explicit-waiting",[12233],{"type":32,"value":12234},"#1: Using explicit waiting",{"type":27,"tag":28,"props":12236,"children":12237},{},[12238,12240,12245],{"type":32,"value":12239},"This first example feels kinda obvious. Whenever you added an explicit wait to your Cypress test, I believe you had an unsettling feeling about this. But what about the cases when our tests fail because the page is too slow? Feels like using ",{"type":27,"tag":653,"props":12241,"children":12243},{"className":12242},[],[12244],{"type":32,"value":7954},{"type":32,"value":12246}," is the way to go.",{"type":27,"tag":793,"props":12248,"children":12251},{"className":12249,"code":12250,"language":1513,"meta":5},[1510],"// ❌ incorrect way, don’t use\n\ncy.visit('/')\ncy.wait(10000)\ncy.get('button') \n  .should('be.visible')\n",[12252],{"type":27,"tag":653,"props":12253,"children":12254},{"__ignoreMap":5},[12255],{"type":32,"value":12250},{"type":27,"tag":28,"props":12257,"children":12258},{},[12259],{"type":32,"value":12260},"But this makes our test just sit there and hope that the page will get loaded before the next command. Instead, we can make use of Cypress’ built-in retryability.",{"type":27,"tag":793,"props":12262,"children":12265},{"className":12263,"code":12264,"language":1513,"meta":5},[1510],"cy.visit('/')\ncy.get('button', { timeout: 10000 })\n  .should('be.visible')\n",[12266],{"type":27,"tag":653,"props":12267,"children":12268},{"__ignoreMap":5},[12269],{"type":32,"value":12264},{"type":27,"tag":28,"props":12271,"children":12272},{},[12273,12275,12281,12283,12288],{"type":32,"value":12274},"So why is this better? Because this way, we will wait maximum 10 seconds for that ",{"type":27,"tag":653,"props":12276,"children":12278},{"className":12277},[],[12279],{"type":32,"value":12280},"button",{"type":32,"value":12282}," to appear. But if the button renders sooner, the test will immediately move on to the next command. This will help you save some time. If you want to read more about this, I recommend ",{"type":27,"tag":172,"props":12284,"children":12285},{"href":8133},[12286],{"type":32,"value":12287},"checking out my blog on this topic",{"type":32,"value":256},{"type":27,"tag":45,"props":12290,"children":12292},{"id":12291},"_2-using-unreadable-selectors",[12293],{"type":32,"value":12294},"#2: Using unreadable selectors",{"type":27,"tag":28,"props":12296,"children":12297},{},[12298,12300,12306],{"type":32,"value":12299},"I could write a whole article just on the topic of selectors (",{"type":27,"tag":172,"props":12301,"children":12303},{"href":12302},"/cypress-basics-selecting-elements",[12304],{"type":32,"value":12305},"in fact, I did",{"type":32,"value":12307},"), since this is one of the most dealt-with topics for testers. Selectors can be the first thing that can give us a clue as to what our test is doing. Because of that, it is worth making them readable.",{"type":27,"tag":28,"props":12309,"children":12310},{},[12311,12317,12319,12324],{"type":27,"tag":172,"props":12312,"children":12314},{"href":8662,"rel":12313},[696],[12315],{"type":32,"value":12316},"Cypress has some recommendations",{"type":32,"value":12318}," as to which selectors should be used. The main purpose of these recommendations is to provide stability for your tests. At the top of the recommendations is to use separate ",{"type":27,"tag":653,"props":12320,"children":12322},{"className":12321},[],[12323],{"type":32,"value":8729},{"type":32,"value":12325}," selectors. You should add these to your application.",{"type":27,"tag":28,"props":12327,"children":12328},{},[12329],{"type":32,"value":12330},"However (and unfortunately IMHO), testers don’t always have the access to the tested application. This makes selecting elements quite a challenge, especially when the element we are searching for is obscure. Many that find themselves in this situation reach for various strategies for selecting elements.",{"type":27,"tag":28,"props":12332,"children":12333},{},[12334,12336,12341,12343,12349],{"type":32,"value":12335},"One of these strategies is using ",{"type":27,"tag":79,"props":12337,"children":12338},{},[12339],{"type":32,"value":12340},"xpath",{"type":32,"value":12342},". ",{"type":27,"tag":172,"props":12344,"children":12346},{"href":12345},"/cypress-basics-xpath-vs-css-selectors",[12347],{"type":32,"value":12348},"The big caveat of xpath is that their syntax is very hard to read",{"type":32,"value":12350},". By merely looking at your xpath selector, you are not really able to tell what element you are selecting. Moreover, they don’t really add anything to the capabilities of your Cypress tests. Anything xpath can do you can do with Cypress commands, and make it more readable.",{"type":27,"tag":793,"props":12352,"children":12356},{"className":12353,"code":12354,"filename":12355,"language":1513,"meta":5},[1510],"// Select an element by text\ncy.xpath('//*[text()[contains(.,\"My Boards\")]]')\n// Select an element containing a specific child element\ncy.xpath('//div[contains(@class, \"list\")][.//div[contains(@class, \"card\")]]')\n// Filter an element by index\ncy.xpath('(//div[contains(@class, \"board\")])[1]')\n// Select an element after a specific element\ncy.xpath('//div[contains(@class, \"card\")][preceding::div[contains(., \"milk\")]]')\n","❌ selecting elements using xpath",[12357],{"type":27,"tag":653,"props":12358,"children":12359},{"__ignoreMap":5},[12360],{"type":32,"value":12354},{"type":27,"tag":793,"props":12362,"children":12366},{"className":12363,"code":12364,"filename":12365,"language":1513,"meta":5},[1510],"// Select an element by text\ncy.contains('h1', 'My Boards')\n// Select an element containing a specific child element\ncy.get('.card').parents('.list')\n// Filter an element by index\ncy.get('.board').eq(0)\n// Select an element after a specific element\ncy.contains('.card', 'milk').next('.card')\n","✅ selecting elements using cypress commands",[12367],{"type":27,"tag":653,"props":12368,"children":12369},{"__ignoreMap":5},[12370],{"type":32,"value":12364},{"type":27,"tag":45,"props":12372,"children":12374},{"id":12373},"_3-selecting-elements-improperly",[12375],{"type":32,"value":12376},"#3: Selecting elements improperly",{"type":27,"tag":28,"props":12378,"children":12379},{},[12380],{"type":32,"value":12381},"Consider the following scenario. You want to select a card (the white element on the page) and assert its text.",{"type":27,"tag":28,"props":12383,"children":12384},{},[12385],{"type":27,"tag":959,"props":12386,"children":12389},{"alt":12387,"src":12388},"Select a proper card","selecting-elements.png",[],{"type":27,"tag":28,"props":12391,"children":12392},{},[12393],{"type":32,"value":12394},"Notice how both of these elements contain the word \"bugs\" inside. Can you tell which card are we going to select when using this code?",{"type":27,"tag":793,"props":12396,"children":12399},{"className":12397,"code":12398,"language":1513,"meta":5},[1510],"cy,visit('/board/1')\ncy.get('[data-cy=card]')\n  .eq(0)\n  .should('contain.text', 'bugs')\n",[12400],{"type":27,"tag":653,"props":12401,"children":12402},{"__ignoreMap":5},[12403],{"type":32,"value":12398},{"type":27,"tag":28,"props":12405,"children":12406},{},[12407],{"type":32,"value":12408},"You might be guessing the first one, with the text \"triage found bugs\". While that may be a good answer it’s not the most precise one. Correctly it is - whichever card will load first.",{"type":27,"tag":28,"props":12410,"children":12411},{},[12412,12414,12420,12421,12427],{"type":32,"value":12413},"It is important to remember that whenever a Cypress command finishes doing its job, it will move on to the next command. So once an element is found by the ",{"type":27,"tag":653,"props":12415,"children":12417},{"className":12416},[],[12418],{"type":32,"value":12419},".get(",{"type":32,"value":5859},{"type":27,"tag":653,"props":12422,"children":12424},{"className":12423},[],[12425],{"type":32,"value":12426},"command, we will move to the",{"type":32,"value":12428},".eq(0)` command. After that, we will move to our assertion that will fail.",{"type":27,"tag":28,"props":12430,"children":12431},{},[12432,12434,12440,12442,12449],{"type":32,"value":12433},"You might wonder why Cypress does not retry at this point, but it actually does. Just not the whole chain. By design, ",{"type":27,"tag":653,"props":12435,"children":12437},{"className":12436},[],[12438],{"type":32,"value":12439},".should()",{"type":32,"value":12441}," command will ",{"type":27,"tag":172,"props":12443,"children":12446},{"href":12444,"rel":12445},"https://twitter.com/filip_hric/status/1493964887336251394",[696],[12447],{"type":32,"value":12448},"retry the previous command, but not the whole chain",{"type":32,"value":12450},". This is why it is vital to implement a better test design here and add a \"guard\" for this test. Before we assert the text of our card, we’ll make sure that all cards are present in DOM:",{"type":27,"tag":793,"props":12452,"children":12456},{"className":12453,"code":12454,"highlights":12455,"language":1513,"meta":5},[1510],"cy,visit('/board/1')\ncy.get('[data-cy=card]')\n  .should('have.length', 2)\n  .eq(0)\n  .should('contain.text', 'bugs')\n",[1606],[12457],{"type":27,"tag":653,"props":12458,"children":12459},{"__ignoreMap":5},[12460],{"type":32,"value":12454},{"type":27,"tag":45,"props":12462,"children":12464},{"id":12463},"_4-ignoring-requests-in-your-app",[12465],{"type":32,"value":12466},"#4: Ignoring requests in your app",{"type":27,"tag":28,"props":12468,"children":12469},{},[12470],{"type":32,"value":12471},"Let’s take a look at this code example:",{"type":27,"tag":793,"props":12473,"children":12476},{"className":12474,"code":12475,"language":1513,"meta":5},[1510],"cy.visit('/board/1')\ncy.get('[data-cy=list]')\n  .should('not.exist')\n",[12477],{"type":27,"tag":653,"props":12478,"children":12479},{"__ignoreMap":5},[12480],{"type":32,"value":12475},{"type":27,"tag":28,"props":12482,"children":12483},{},[12484,12486,12492,12494,12500],{"type":32,"value":12485},"When we open our page, multiple requests get fired. Responses from these requests will get digested by the frontend app and rendered into our page. In this example ",{"type":27,"tag":653,"props":12487,"children":12489},{"className":12488},[],[12490],{"type":32,"value":12491},"[data-cy=list]",{"type":32,"value":12493}," elements get rendered after we get a response from ",{"type":27,"tag":653,"props":12495,"children":12497},{"className":12496},[],[12498],{"type":32,"value":12499},"/api/lists",{"type":32,"value":12501}," endpoint.",{"type":27,"tag":28,"props":12503,"children":12504},{},[12505],{"type":32,"value":12506},"But the problem with this test is, that we are not telling Cypress to wait for these requests. Because of this, our test may give us a false positive and pass even if there are lists present in our application.",{"type":27,"tag":28,"props":12508,"children":12509},{},[12510],{"type":32,"value":12511},"Cypress will not wait for the requests our application does automatically. We need to define this using the intercept command:",{"type":27,"tag":793,"props":12513,"children":12516},{"className":12514,"code":12515,"language":1513,"meta":5},[1510],"cy.intercept('GET', '/api/lists')\n  .as('lists')\ncy.visit('/board/1')\ncy.wait('@lists')\ncy.get('[data-cy=list]')\n  .should('not.exist')\n",[12517],{"type":27,"tag":653,"props":12518,"children":12519},{"__ignoreMap":5},[12520],{"type":32,"value":12515},{"type":27,"tag":45,"props":12522,"children":12524},{"id":12523},"_5-overlooking-dom-re-rendering",[12525],{"type":32,"value":12526},"#5: Overlooking DOM re-rendering",{"type":27,"tag":28,"props":12528,"children":12529},{},[12530],{"type":32,"value":12531},"Modern web applications send requests all the time to get information from the database and then render them in DOM. In our next example, we are testing a search bar, where each keystroke will send a new request. Each response will make the content on our page re-render.\nIn this test, we want to take a search result and confirm that after we type the word \"for\", we will see the first item with the text \"search for critical bugs\". The test code goes like this:",{"type":27,"tag":793,"props":12533,"children":12536},{"className":12534,"code":12535,"language":1513,"meta":5},[1510],"cy.realPress(['Meta', 'k'])\ncy.get('[data-cy=search-input]')\n  .type('for')\ncy.get('[data-cy=result-item]')\n  .eq(0)\n  .should('contain.text', 'search for critical bugs')\n",[12537],{"type":27,"tag":653,"props":12538,"children":12539},{"__ignoreMap":5},[12540],{"type":32,"value":12535},{"type":27,"tag":28,"props":12542,"children":12543},{},[12544],{"type":32,"value":12545},"This test will suffer from \"element detached from DOM\" error. The reason for this is that while still typing, we will first get 2 results, and when we finish, we’ll get just a single result. In a result, our test will go like this:",{"type":27,"tag":851,"props":12547,"children":12548},{},[12549,12554,12559,12564,12569,12574,12578,12583,12588,12593,12614,12619,12630,12648],{"type":27,"tag":109,"props":12550,"children":12551},{},[12552],{"type":32,"value":12553},"\"f\" key is typed in the search box",{"type":27,"tag":109,"props":12555,"children":12556},{},[12557],{"type":32,"value":12558},"request searching for all items with \"f\" will fire",{"type":27,"tag":109,"props":12560,"children":12561},{},[12562],{"type":32,"value":12563},"response comes back and app will render two results",{"type":27,"tag":109,"props":12565,"children":12566},{},[12567],{"type":32,"value":12568},"\"o\" key is typed in the search box",{"type":27,"tag":109,"props":12570,"children":12571},{},[12572],{"type":32,"value":12573},"request searching for all items with \"fo\" will fire",{"type":27,"tag":109,"props":12575,"children":12576},{},[12577],{"type":32,"value":12563},{"type":27,"tag":109,"props":12579,"children":12580},{},[12581],{"type":32,"value":12582},"\"r\" key is typed in the search box",{"type":27,"tag":109,"props":12584,"children":12585},{},[12586],{"type":32,"value":12587},"request searching for all items with \"for\" will fire",{"type":27,"tag":109,"props":12589,"children":12590},{},[12591],{"type":32,"value":12592},"Cypress is done with typing, so it moves to the next command",{"type":27,"tag":109,"props":12594,"children":12595},{},[12596,12598,12604,12606,12612],{"type":32,"value":12597},"Cypress will select ",{"type":27,"tag":653,"props":12599,"children":12601},{"className":12600},[],[12602],{"type":32,"value":12603},"[data-cy=result-item]",{"type":32,"value":12605}," elements and will filter the first one (using ",{"type":27,"tag":653,"props":12607,"children":12609},{"className":12608},[],[12610],{"type":32,"value":12611},".eq(0)",{"type":32,"value":12613},") command",{"type":27,"tag":109,"props":12615,"children":12616},{},[12617],{"type":32,"value":12618},"Cypress will assert that it has the text \"search for critical bugs\"",{"type":27,"tag":109,"props":12620,"children":12621},{},[12622,12624,12629],{"type":32,"value":12623},"since the text is different, it will run the previous command again (",{"type":27,"tag":653,"props":12625,"children":12627},{"className":12626},[],[12628],{"type":32,"value":12611},{"type":32,"value":5859},{"type":27,"tag":109,"props":12631,"children":12632},{},[12633,12635,12640,12641,12646],{"type":32,"value":12634},"while Cypress is retrying and going back and forth between ",{"type":27,"tag":653,"props":12636,"children":12638},{"className":12637},[],[12639],{"type":32,"value":12611},{"type":32,"value":4164},{"type":27,"tag":653,"props":12642,"children":12644},{"className":12643},[],[12645],{"type":32,"value":12439},{"type":32,"value":12647}," a response from our last request comes back and app will re-render to show single result",{"type":27,"tag":109,"props":12649,"children":12650},{},[12651],{"type":32,"value":12652},"the element that we selected in step 10 is no longer present, so we get an error",{"type":27,"tag":28,"props":12654,"children":12655},{},[12656,12658,12663,12665,12671],{"type":32,"value":12657},"Remember, ",{"type":27,"tag":653,"props":12659,"children":12661},{"className":12660},[],[12662],{"type":32,"value":12439},{"type":32,"value":12664}," command will make the previous command retry, but not the full chain. This means that our ",{"type":27,"tag":653,"props":12666,"children":12668},{"className":12667},[],[12669],{"type":32,"value":12670},"cy.get('[data-cy=result-item]')",{"type":32,"value":12672}," does not get called again. To fix this problem we can again add a guarding assertion to our code to first make sure we get the proper number of results, and then assert the text of the result.",{"type":27,"tag":793,"props":12674,"children":12678},{"className":12675,"code":12676,"highlights":12677,"language":1513,"meta":5},[1510],"cy.realPress(['Meta', 'k'])\ncy.get('[data-cy=search-input]')\n  .type('for')\ncy.get('[data-cy=result-item]')\n  .should('have.length', 1)\n  .eq(0)\n  .should('contain.text', 'search for critical bugs')\n",[3667],[12679],{"type":27,"tag":653,"props":12680,"children":12681},{"__ignoreMap":5},[12682],{"type":32,"value":12676},{"type":27,"tag":28,"props":12684,"children":12685},{},[12686,12688,12694,12696,12701],{"type":32,"value":12687},"But what if we cannot assert the number of results? ",{"type":27,"tag":172,"props":12689,"children":12691},{"href":12690},"/testing-lists-of-items",[12692],{"type":32,"value":12693},"I wrote about this in the past",{"type":32,"value":12695},", but in short, the solution is to use ",{"type":27,"tag":653,"props":12697,"children":12699},{"className":12698},[],[12700],{"type":32,"value":12439},{"type":32,"value":12702}," command with a callback, something like this:",{"type":27,"tag":793,"props":12704,"children":12708},{"className":12705,"code":12706,"highlights":12707,"language":1513,"meta":5},[1510],"cy.realPress(['Meta', 'k'])\ncy.get('[data-cy=search-input]')\n  .type('for')\ncy.get('[data-cy=result-item]')\n  .should( items => {\n    expect(items[0].to.have.text('search for critical bugs'))      \n  })\n",[3667,3809,3668],[12709],{"type":27,"tag":653,"props":12710,"children":12711},{"__ignoreMap":5},[12712],{"type":32,"value":12706},{"type":27,"tag":45,"props":12714,"children":12716},{"id":12715},"_6-creating-inefficient-command-chains",[12717],{"type":32,"value":12718},"#6: Creating inefficient command chains",{"type":27,"tag":28,"props":12720,"children":12721},{},[12722],{"type":32,"value":12723},"Cypress has a really cool chaining syntax. Each command passes information to the next one, creating a one-way flow of your test scenario. But even these commands have an internal logic inside them. Cypress commands can be either parent, child or dual. This means, that some of our commands will always start a new chain.",{"type":27,"tag":28,"props":12725,"children":12726},{},[12727],{"type":32,"value":12728},"Consider this command chain:",{"type":27,"tag":793,"props":12730,"children":12733},{"className":12731,"code":12732,"language":1513,"meta":5},[1510],"cy.get('[data-cy=\"create-board\"]')\n  .click()\n  .get('[data-cy=\"new-board-input\"]')\n  .type('new board{enter}')\n  .location('pathname')\n  .should('contain', '/board/')  \n",[12734],{"type":27,"tag":653,"props":12735,"children":12736},{"__ignoreMap":5},[12737],{"type":32,"value":12732},{"type":27,"tag":28,"props":12739,"children":12740},{},[12741,12743,12749,12751,12757],{"type":32,"value":12742},"The problem with writing a chain like this is not only that it is hard to read, but also that it ignores this parent/child command chaining logic. Every ",{"type":27,"tag":653,"props":12744,"children":12746},{"className":12745},[],[12747],{"type":32,"value":12748},".get()",{"type":32,"value":12750}," command is actually starting a new chain. This means that our ",{"type":27,"tag":653,"props":12752,"children":12754},{"className":12753},[],[12755],{"type":32,"value":12756},".click().get()",{"type":32,"value":12758}," chain does not really make sense. Correctly using chains can prevent your Cypress tests from unpredictable behavior and can make them more readable:",{"type":27,"tag":793,"props":12760,"children":12763},{"className":12761,"code":12762,"language":1513,"meta":5},[1510],"cy.get('[data-cy=\"create-board\"]') // parent\n  .click() // child\ncy.get('[data-cy=\"new-board-input\"]') // parent\n  .type('new board{enter}') // child\ncy.location('pathname') // parent\n  .should('contain', '/board/') // child\n",[12764],{"type":27,"tag":653,"props":12765,"children":12766},{"__ignoreMap":5},[12767],{"type":32,"value":12762},{"type":27,"tag":45,"props":12769,"children":12771},{"id":12770},"_7-overusing-ui",[12772],{"type":32,"value":12773},"#7: Overusing UI",{"type":27,"tag":28,"props":12775,"children":12776},{},[12777],{"type":32,"value":12778},"I believe that while writing UI tests, you should use UI as little as possible. This strategy can make your test faster and provide you the same (or bigger) confidence with your app. Let’s say you have a navigation bar with links, that looks like this:",{"type":27,"tag":793,"props":12780,"children":12783},{"className":12781,"code":12782,"language":7826,"meta":5},[7824],"\u003Cnav>\n  \u003Ca href=\"/blog\">Blog\u003C/a>\n  \u003Ca href=\"/about\">About\u003C/a>\n  \u003Ca href=\"/contact\">Contact\u003C/a>\n\u003C/nav>\n",[12784],{"type":27,"tag":653,"props":12785,"children":12786},{"__ignoreMap":5},[12787],{"type":32,"value":12782},{"type":27,"tag":28,"props":12789,"children":12790},{},[12791,12793,12799,12801,12807],{"type":32,"value":12792},"The goal of the test will be to check all the links inside ",{"type":27,"tag":653,"props":12794,"children":12796},{"className":12795},[],[12797],{"type":32,"value":12798},"\u003Cnav>",{"type":32,"value":12800}," element, to make sure they are pointing to a live website. The intuitive approach might be using ",{"type":27,"tag":653,"props":12802,"children":12804},{"className":12803},[],[12805],{"type":32,"value":12806},".click()",{"type":32,"value":12808}," command and then check either location or content of the opened page to see if the page is live.",{"type":27,"tag":28,"props":12810,"children":12811},{},[12812],{"type":32,"value":12813},"However, this approach is slow and in fact, can give you false confidence. As I mentioned in one of my previous blogs, this approach can overlook that one of our pages is not live, but returns a 404 error.",{"type":27,"tag":28,"props":12815,"children":12816},{},[12817,12819,12825],{"type":32,"value":12818},"Instead of checking your links like this, you can use ",{"type":27,"tag":653,"props":12820,"children":12822},{"className":12821},[],[12823],{"type":32,"value":12824},".request()",{"type":32,"value":12826}," command to make sure that the page is live:",{"type":27,"tag":793,"props":12828,"children":12831},{"className":12829,"code":12830,"language":1513,"meta":5},[1510],"cy.get('a').each( link => {\n  cy.request(link.prop('href'))\n})\n",[12832],{"type":27,"tag":653,"props":12833,"children":12834},{"__ignoreMap":5},[12835],{"type":32,"value":12830},{"type":27,"tag":45,"props":12837,"children":12839},{"id":12838},"_8-repeating-the-same-set-of-actions",[12840],{"type":32,"value":12841},"#8: Repeating the same set of actions",{"type":27,"tag":28,"props":12843,"children":12844},{},[12845,12847,12852],{"type":32,"value":12846},"It’s really common to hear that your code should be DRY = don’t repeat yourself. While this is a great principle for your code, it seems like it is slightly ignored during the test run. In an example below, there’s a ",{"type":27,"tag":653,"props":12848,"children":12850},{"className":12849},[],[12851],{"type":32,"value":10497},{"type":32,"value":12853}," command that will go through the login steps and will be used before every test:",{"type":27,"tag":793,"props":12855,"children":12858},{"className":12856,"code":12857,"language":1513,"meta":5},[1510],"Cypress.Commands.add('login', () => {\n\n  cy.visit('/login')\n  \n  cy.get('[type=email]')\n    .type('filip+example@gmail.com')\n  \n  cy.get('[type=password]')\n      .type('i\u003C3slovak1a!')\n\n  cy.get('[data-cy=\"logged-user\"]')\n    .should('be.visible')\n\n})\n",[12859],{"type":27,"tag":653,"props":12860,"children":12861},{"__ignoreMap":5},[12862],{"type":32,"value":12857},{"type":27,"tag":28,"props":12864,"children":12865},{},[12866],{"type":32,"value":12867},"Having this sequence of steps abstracted to a single command is definitely good. It will definitely make our code more \"DRY\". But as we keep using it in our test, our test execution will go through the same steps over and over, essentially repeating the same set of actions.",{"type":27,"tag":28,"props":12869,"children":12870},{},[12871,12873,12878,12880,12886,12888,12893,12895,12901],{"type":32,"value":12872},"With Cypress you can pull up a trick that will help you solve this issue. This set of steps can be cached and reloaded using ",{"type":27,"tag":653,"props":12874,"children":12876},{"className":12875},[],[12877],{"type":32,"value":6509},{"type":32,"value":12879}," command. This is still in an experimental state, but can be enabled using ",{"type":27,"tag":653,"props":12881,"children":12883},{"className":12882},[],[12884],{"type":32,"value":12885},"experimentalSessionAndOrigin: true",{"type":32,"value":12887}," attribute in your ",{"type":27,"tag":653,"props":12889,"children":12891},{"className":12890},[],[12892],{"type":32,"value":6028},{"type":32,"value":12894},". You can wrap the sequence in our custom command into ",{"type":27,"tag":653,"props":12896,"children":12898},{"className":12897},[],[12899],{"type":32,"value":12900},".session()",{"type":32,"value":12902}," function like this:",{"type":27,"tag":793,"props":12904,"children":12908},{"className":12905,"code":12906,"highlights":12907,"language":1513,"meta":5},[1510],"Cypress.Commands.add('login', () => {\n\n  cy.session('login', () => {\n\n    cy.get('[type=email]')\n      .type('filip+example@gmail.com')\n    \n    cy.get('[type=password]')\n      .type('i\u003C3slovak1a!')\n\n    cy.get('[data-cy=\"logged-user\"]')\n      .should('be.visible')\n\n  })\n\n})\n",[1606,3853],[12909],{"type":27,"tag":653,"props":12910,"children":12911},{"__ignoreMap":5},[12912],{"type":32,"value":12906},{"type":27,"tag":28,"props":12914,"children":12915},{},[12916,12918,12925],{"type":32,"value":12917},"This will cause to run the sequence in your custom commands just once per spec. But if you want to cache it throughout your whole test run, you can do that by using ",{"type":27,"tag":172,"props":12919,"children":12922},{"href":12920,"rel":12921},"https://www.npmjs.com/package/cypress-data-session",[696],[12923],{"type":32,"value":12924},"cypress-data-session plugin",{"type":32,"value":12926},". There are a lot more things you can do this, but caching your steps is probably the most valuable one, as it can easily shave off a couple of minutes from the whole test run. This will of course depend on the test itself. In my own tests, where I just ran 4 tests that logged in, I was able to cut the time in half.",{"type":27,"tag":28,"props":12928,"children":12929},{},[12930,12932,12938],{"type":32,"value":12931},"Hopefully, this helped. I’m teaching all this and more in my ",{"type":27,"tag":172,"props":12933,"children":12935},{"href":12934},"/workshop/cypress-core",[12936],{"type":32,"value":12937},"upcoming workshop",{"type":32,"value":12939},". Hope to see you there!",{"title":5,"searchDepth":320,"depth":320,"links":12941},[12942,12943,12944,12945,12946,12947,12948,12949],{"id":12231,"depth":320,"text":12234},{"id":12291,"depth":320,"text":12294},{"id":12373,"depth":320,"text":12376},{"id":12463,"depth":320,"text":12466},{"id":12523,"depth":320,"text":12526},{"id":12715,"depth":320,"text":12718},{"id":12770,"depth":320,"text":12773},{"id":12838,"depth":320,"text":12841},"content:8-common-mistakes-in-cypress-and-how-to-avoid-them:index.md","8-common-mistakes-in-cypress-and-how-to-avoid-them/index.md","8-common-mistakes-in-cypress-and-how-to-avoid-them/index",{"_path":12954,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":12955,"description":12956,"date":12957,"published":10,"slug":12958,"tags":12959,"cypressVersion":5959,"readingTime":12962,"body":12966,"_type":329,"_id":13378,"_source":331,"_file":13379,"_stem":13380,"_extension":334},"/testing-frontend-performance-with-cypress","Testing frontend performance with Cypress","How to measure loading of elements appearing on page using browser performance API and creating a custom command out of it.","2022-05-09","testing-frontend-performance-with-cypress",[5279,10370,12960,12961],"speed","metrics",{"text":927,"minutes":12963,"time":12964,"words":12965},4.905,294300,981,{"type":24,"children":12967,"toc":13372},[12968,12973,12986,12991,13000,13005,13012,13018,13054,13064,13085,13091,13096,13106,13134,13145,13150,13158,13171,13183,13189,13232,13237,13273,13283,13296,13306,13347,13353,13358],{"type":27,"tag":28,"props":12969,"children":12970},{},[12971],{"type":32,"value":12972},"There are many ways to measure performance. In today’s post I want to talk about one of the most simple. Imagine a following scenario:",{"type":27,"tag":851,"props":12974,"children":12975},{},[12976,12981],{"type":27,"tag":109,"props":12977,"children":12978},{},[12979],{"type":32,"value":12980},"user clicks a button",{"type":27,"tag":109,"props":12982,"children":12983},{},[12984],{"type":32,"value":12985},"modal window appears",{"type":27,"tag":28,"props":12987,"children":12988},{},[12989],{"type":32,"value":12990},"Our test may look something like this:",{"type":27,"tag":793,"props":12992,"children":12995},{"className":12993,"code":12994,"language":3520,"meta":5},[3517],"  cy.visit('/board/1')\n\n  // wait for loading to finish\n  cy.getDataCy('loading')\n    .should('not.exist')\n\n  cy.getDataCy('card')\n    .click()\n\n",[12996],{"type":27,"tag":653,"props":12997,"children":12998},{"__ignoreMap":5},[12999],{"type":32,"value":12994},{"type":27,"tag":28,"props":13001,"children":13002},{},[13003],{"type":32,"value":13004},"This modal window may fetch some data from server, reorder or filter it. Additionally it may perform some other actions such as render images etc. All of these actions take some time and as testers, we want to make sure that the result will not take too long.",{"type":27,"tag":28,"props":13006,"children":13007},{},[13008],{"type":27,"tag":959,"props":13009,"children":13011},{"alt":10370,"src":13010},"performance.mp4",[],{"type":27,"tag":45,"props":13013,"children":13015},{"id":13014},"performancemark-api",[13016],{"type":32,"value":13017},"performance.mark() API",{"type":27,"tag":28,"props":13019,"children":13020},{},[13021,13023,13030,13032,13037,13039,13044,13046,13052],{"type":32,"value":13022},"In ",{"type":27,"tag":172,"props":13024,"children":13027},{"href":13025,"rel":13026},"https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark#browser_compatibility",[696],[13028],{"type":32,"value":13029},"all of the current browsers",{"type":32,"value":13031}," a ",{"type":27,"tag":653,"props":13033,"children":13035},{"className":13034},[],[13036],{"type":32,"value":10370},{"type":32,"value":13038}," API is available on ",{"type":27,"tag":653,"props":13040,"children":13042},{"className":13041},[],[13043],{"type":32,"value":11688},{"type":32,"value":13045}," object. We can access this API by using ",{"type":27,"tag":653,"props":13047,"children":13049},{"className":13048},[],[13050],{"type":32,"value":13051},"cy.window()",{"type":32,"value":13053}," function and then calling a method. To start measuring the performance, we can create a mark that will label the start of our measurement.",{"type":27,"tag":793,"props":13055,"children":13059},{"className":13056,"code":13057,"highlights":13058,"language":3520,"meta":5},[3517],"  cy.visit('/board/1')\n\n  // wait for loading to finish\n  cy.getDataCy('loading')\n    .should('not.exist')\n\n  cy.window()\n    .its('performance')\n    .invoke('mark', 'modalOpen')\n\n  cy.getDataCy('card')\n    .click()\n",[3668,3723,3746],[13060],{"type":27,"tag":653,"props":13061,"children":13062},{"__ignoreMap":5},[13063],{"type":32,"value":13057},{"type":27,"tag":28,"props":13065,"children":13066},{},[13067,13069,13075,13077,13083],{"type":32,"value":13068},"The highlighted part of our code does actually the exact same thing as if we were to type ",{"type":27,"tag":653,"props":13070,"children":13072},{"className":13071},[],[13073],{"type":32,"value":13074},"window.performance.mark('modalOpen')",{"type":32,"value":13076}," in our DevTools console. The ",{"type":27,"tag":653,"props":13078,"children":13080},{"className":13079},[],[13081],{"type":32,"value":13082},"modalOpen",{"type":32,"value":13084}," is just a label, and can be named anything.",{"type":27,"tag":45,"props":13086,"children":13088},{"id":13087},"performancemeasure-api",[13089],{"type":32,"value":13090},"performance.measure() API",{"type":27,"tag":28,"props":13092,"children":13093},{},[13094],{"type":32,"value":13095},"Now that we have labeled the start of our metric, let’s perform the next steps. When we click on the card, it opens modal window. First, we want to make sure that we have reached the desired result. We can check that by making an assertion on the modal window visibility:",{"type":27,"tag":793,"props":13097,"children":13101},{"className":13098,"code":13099,"highlights":13100,"language":3520,"meta":5},[3517],"  cy.visit('/board/1')\n\n  // wait for loading to finish\n  cy.getDataCy('loading')\n    .should('not.exist')\n\n  cy.window()\n    .its('performance')\n    .invoke('mark', 'modalOpen')\n\n  cy.getDataCy('card')\n    .click()\n\n  cy.getDataCy('card-detail')\n    .should('be.visible')\n",[3853,3878],[13102],{"type":27,"tag":653,"props":13103,"children":13104},{"__ignoreMap":5},[13105],{"type":32,"value":13099},{"type":27,"tag":28,"props":13107,"children":13108},{},[13109,13111,13117,13119,13125,13127,13132],{"type":32,"value":13110},"After that, we can call ",{"type":27,"tag":653,"props":13112,"children":13114},{"className":13113},[],[13115],{"type":32,"value":13116},"performance.measure()",{"type":32,"value":13118},"function to make our measurement. Basically, we are pressing a button on a stopwatch here. The argument of the ",{"type":27,"tag":653,"props":13120,"children":13122},{"className":13121},[],[13123],{"type":32,"value":13124},"measure",{"type":32,"value":13126}," function will be our ",{"type":27,"tag":653,"props":13128,"children":13130},{"className":13129},[],[13131],{"type":32,"value":13082},{"type":32,"value":13133}," label. The reason for passing this argument is that we can actually add multiple labels into our test and we need to specify which one to measure. To call the measure function we basically perform a very set of Cypress functions as before:",{"type":27,"tag":793,"props":13135,"children":13140},{"className":13136,"code":13137,"highlights":13138,"language":3520,"meta":5},[3517],"  cy.visit('/board/1')\n\n  // wait for loading to finish\n  cy.getDataCy('loading')\n    .should('not.exist')\n\n  cy.window()\n    .its('performance')\n    .invoke('mark', 'modalOpen')\n\n  cy.getDataCy('card')\n    .click()\n\n  cy.getDataCy('card-detail')\n    .should('be.visible')\n\n  cy.window()\n    .its('performance')\n    .invoke('measure', 'modalOpen')\n",[10872,10246,13139],19,[13141],{"type":27,"tag":653,"props":13142,"children":13143},{"__ignoreMap":5},[13144],{"type":32,"value":13137},{"type":27,"tag":28,"props":13146,"children":13147},{},[13148],{"type":32,"value":13149},"The invoke command is going to yield an object with all kinds of results:",{"type":27,"tag":28,"props":13151,"children":13152},{},[13153],{"type":27,"tag":959,"props":13154,"children":13157},{"alt":13155,"src":13156},"Performance measure output","performance_measure.png",[],{"type":27,"tag":28,"props":13159,"children":13160},{},[13161,13163,13169],{"type":32,"value":13162},"Within this command, we can pick a property from this object using ",{"type":27,"tag":653,"props":13164,"children":13166},{"className":13165},[],[13167],{"type":32,"value":13168},".its()",{"type":32,"value":13170}," command. Since we don’t need retryability, we can set timeout to 0 and make our assertion immediately. Let’s make an assertion that the modal should not load longer than 2 seconds (2000 in milliseconds).",{"type":27,"tag":793,"props":13172,"children":13178},{"className":13173,"code":13174,"highlights":13175,"language":3520,"meta":5},[3517],"  cy.visit('/board/1')\n\n  // wait for loading to finish\n  cy.getDataCy('loading')\n    .should('not.exist')\n\n  cy.window()\n    .its('performance')\n    .invoke('mark', 'modalOpen')\n\n  cy.getDataCy('card')\n    .click()\n\n  cy.getDataCy('card-detail')\n    .should('be.visible')\n\n  cy.window()\n    .its('performance')\n    .invoke('measure', 'modalOpen')\n    .its('duration', { timeout: 0 })\n    .should('be.lessThan', 2000)\n",[13176,13177],20,21,[13179],{"type":27,"tag":653,"props":13180,"children":13181},{"__ignoreMap":5},[13182],{"type":32,"value":13174},{"type":27,"tag":45,"props":13184,"children":13186},{"id":13185},"creating-a-custom-command",[13187],{"type":32,"value":13188},"Creating a custom command",{"type":27,"tag":28,"props":13190,"children":13191},{},[13192,13194,13200,13202,13208,13210,13216,13218,13223,13225,13231],{"type":32,"value":13193},"Now that we know what to do, we can create a custom command out of this. There’s a lot of TypeScript going on, so let me break down what’s happening here. Lines 1-9 is a type declaration. This is how we tell TypeScript compiler that we have added a new ",{"type":27,"tag":653,"props":13195,"children":13197},{"className":13196},[],[13198],{"type":32,"value":13199},"cy.mark()",{"type":32,"value":13201}," command to the library of ",{"type":27,"tag":653,"props":13203,"children":13205},{"className":13204},[],[13206],{"type":32,"value":13207},"cy",{"type":32,"value":13209}," commands. The library is called ",{"type":27,"tag":653,"props":13211,"children":13213},{"className":13212},[],[13214],{"type":32,"value":13215},"Chainable",{"type":32,"value":13217},", and contains all ",{"type":27,"tag":653,"props":13219,"children":13221},{"className":13220},[],[13222],{"type":32,"value":13207},{"type":32,"value":13224}," commands. This library is part of a bigger whole - ",{"type":27,"tag":653,"props":13226,"children":13228},{"className":13227},[],[13229],{"type":32,"value":13230},"namespace Cypress",{"type":32,"value":256},{"type":27,"tag":28,"props":13233,"children":13234},{},[13235],{"type":32,"value":13236},"Lines 11 - 29 is a function that contains our chain of commands from previous example. In addition to that, I have hidden the logs of our three commands and added my own log which you can see on lines 15 - 24.",{"type":27,"tag":28,"props":13238,"children":13239},{},[13240,13242,13248,13250,13256,13258,13264,13266,13272],{"type":32,"value":13241},"Finally, on line 31, we are adding this function to the Cypress library. While lines 1-9 add our command to the Cypress namespace that our TypeScript compiler can recognize, ",{"type":27,"tag":653,"props":13243,"children":13245},{"className":13244},[],[13246],{"type":32,"value":13247},"Cypress.Commands.addAll()",{"type":32,"value":13249}," function will add it to the Cypress itself. I usually store my custom commands to ",{"type":27,"tag":653,"props":13251,"children":13253},{"className":13252},[],[13254],{"type":32,"value":13255},"cypress/support/commands/",{"type":32,"value":13257}," folder and do an ",{"type":27,"tag":653,"props":13259,"children":13261},{"className":13260},[],[13262],{"type":32,"value":13263},"import ../commands/mark.ts",{"type":32,"value":13265}," inside ",{"type":27,"tag":653,"props":13267,"children":13269},{"className":13268},[],[13270],{"type":32,"value":13271},"cypress/support/index.ts",{"type":32,"value":786},{"type":27,"tag":793,"props":13274,"children":13278},{"className":13275,"code":13276,"filename":13277,"language":3520,"meta":5},[3517],"declare namespace Cypress {\n  interface Chainable\u003CSubject = any> {\n      /**\n       * Add a measurment marker. Used with cy.measure() command\n       * @example cy.mark('modalWindow')\n       */\n       mark: typeof mark\n  }\n}\n\nconst mark = (markName: string): Cypress.Chainable\u003Cany> => {\n\n  const logFalse = { log: false }\n\n  Cypress.log({\n    name: 'mark',\n    message: markName,\n    consoleProps() {\n      return {\n        command: 'mark',\n        'mark name': markName\n      }\n    }\n  })\n\n  return cy.window(logFalse)\n    .its('performance', logFalse)\n    .invoke(logFalse, 'mark', markName)\n}\n\nCypress.Commands.addAll({ mark })\n","support/commands/mark.ts",[13279],{"type":27,"tag":653,"props":13280,"children":13281},{"__ignoreMap":5},[13282],{"type":32,"value":13276},{"type":27,"tag":28,"props":13284,"children":13285},{},[13286,13288,13294],{"type":32,"value":13287},"Similarly, we can add the ",{"type":27,"tag":653,"props":13289,"children":13291},{"className":13290},[],[13292],{"type":32,"value":13293},"cy.measure()",{"type":32,"value":13295}," command as well:",{"type":27,"tag":793,"props":13297,"children":13301},{"className":13298,"code":13299,"filename":13300,"language":3520,"meta":5},[3517],"declare namespace Cypress {\n  interface Chainable\u003CSubject = any> {\n      /**\n       * Add a measurment marker. Used with cy.measure() command\n       * @example cy.measure('modalWindow')\n       */\n       measure: typeof measure\n  }\n}\n\nconst measure = (markName: string): Cypress.Chainable\u003Cnumber> => {\n\n  const logFalse = { log: false }\n\n  let measuredDuration: number\n  let log = Cypress.log({\n    name: 'measure',\n    message: markName,\n    autoEnd: false,\n    consoleProps() {\n      return {\n        command: 'measure',\n        'mark name': markName,\n        yielded: measuredDuration\n      }\n    }\n  })\n\n  return cy.window(logFalse)\n    .its('performance', logFalse)\n    .invoke(logFalse, 'measure', markName)\n    .then( ({ duration }) => {\n      measuredDuration = duration\n      log.end()\n      return duration\n    })\n}\n\nCypress.Commands.addAll({ measure })\n","support/commands/measure.ts",[13302],{"type":27,"tag":653,"props":13303,"children":13304},{"__ignoreMap":5},[13305],{"type":32,"value":13299},{"type":27,"tag":28,"props":13307,"children":13308},{},[13309,13311,13316,13318,13324,13326,13331,13333,13339,13341,13346],{"type":32,"value":13310},"A small difference from our ",{"type":27,"tag":653,"props":13312,"children":13314},{"className":13313},[],[13315],{"type":32,"value":13199},{"type":32,"value":13317}," is that this time our return type will be ",{"type":27,"tag":653,"props":13319,"children":13321},{"className":13320},[],[13322],{"type":32,"value":13323},"number",{"type":32,"value":13325},", because the our function will return a number. Also, instead of using ",{"type":27,"tag":653,"props":13327,"children":13329},{"className":13328},[],[13330],{"type":32,"value":13168},{"type":32,"value":13332}," function, we are returning it from ",{"type":27,"tag":653,"props":13334,"children":13336},{"className":13335},[],[13337],{"type":32,"value":13338},".then()",{"type":32,"value":13340}," function as we want to use it in our console command detail as well. If this is a lot of new terms, I suggest checking out this post about ",{"type":27,"tag":172,"props":13342,"children":13343},{"href":10472},[13344],{"type":32,"value":13345},"improving custom Cypress command I’ve made earlier",{"type":32,"value":256},{"type":27,"tag":45,"props":13348,"children":13350},{"id":13349},"performance-testing-in-cypress",[13351],{"type":32,"value":13352},"Performance testing in Cypress",{"type":27,"tag":28,"props":13354,"children":13355},{},[13356],{"type":32,"value":13357},"Whenever we do performance testing of any kind, we need to pay close attention to the environment we are testing on. Are we in production? Is it currently under heavy load? If on staging server, is it 1:1 with production or are we testing a scaled down version? Are we using browser for perfomance testing? Which one? Which version? All of this and more questions need to be asked to provide context for the performance metrics.",{"type":27,"tag":28,"props":13359,"children":13360},{},[13361,13363,13370],{"type":32,"value":13362},"In our context, we are running inside a browser that has two iframes opened. One for our application and one for Cypress script. This may have effect on our testing and it is not slight. Cypress docs ",{"type":27,"tag":172,"props":13364,"children":13367},{"href":13365,"rel":13366},"https://docs.cypress.io/faq/questions/using-cypress-faq#Can-Cypress-be-used-for-performance-testing",[696],[13368],{"type":32,"value":13369},"warn about this in their docs",{"type":32,"value":13371},". This doesn’t mean that measuring performance in Cypress is useless. It just means that we need to take the context into account when looking at the metrics.",{"title":5,"searchDepth":320,"depth":320,"links":13373},[13374,13375,13376,13377],{"id":13014,"depth":320,"text":13017},{"id":13087,"depth":320,"text":13090},{"id":13185,"depth":320,"text":13188},{"id":13349,"depth":320,"text":13352},"content:testing-frontend-performance-with-cypress:index.md","testing-frontend-performance-with-cypress/index.md","testing-frontend-performance-with-cypress/index",{"_path":13382,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":13383,"description":13384,"date":13385,"published":10,"slug":13386,"cypressVersion":10994,"tags":13387,"readingTime":13389,"body":13393,"_type":329,"_id":13995,"_source":331,"_file":13996,"_stem":13997,"_extension":334},"/writing-better-command-chains-in-cypress","Writing better command chains in Cypress","Understanding how command chaining in Cypress works is essential for writing stable tests. In this week’s explainer we’ll take a look on how we can make our tests more stable with writing proper command chains","2022-02-15","writing-better-command-chains-in-cypress",[5279,13388,12203],"chaining",{"text":4032,"minutes":13390,"time":13391,"words":13392},5.125,307500,1025,{"type":24,"children":13394,"toc":13989},[13395,13400,13405,13411,13416,13425,13430,13439,13444,13469,13474,13483,13488,13497,13510,13519,13525,13530,13539,13551,13556,13565,13584,13590,13602,13611,13638,13647,13666,13814,13839,13848,13860,13869,13888,13894,13899,13922,13927,13936,13957,13966,13971],{"type":27,"tag":28,"props":13396,"children":13397},{},[13398],{"type":32,"value":13399},"If you have been using Cypress, you are probably familiar with command chains. Or are you? I see many Cypress users be aware of them but sometimes slightly miss the underlying logic.",{"type":27,"tag":28,"props":13401,"children":13402},{},[13403],{"type":32,"value":13404},"In this post I would like to explore some of the core principles of Cypress chains and how understanding them can make you write your tests better.",{"type":27,"tag":45,"props":13406,"children":13408},{"id":13407},"basics",[13409],{"type":32,"value":13410},"Basics",{"type":27,"tag":28,"props":13412,"children":13413},{},[13414],{"type":32,"value":13415},"Cypress commands are written in chains. That’s why when you wan to interact with an element on your page, you need to write two commands:",{"type":27,"tag":793,"props":13417,"children":13420},{"className":13418,"code":13419,"language":1513,"meta":5},[1510],"cy.get('#element').click()\n",[13421],{"type":27,"tag":653,"props":13422,"children":13423},{"__ignoreMap":5},[13424],{"type":32,"value":13419},{"type":27,"tag":28,"props":13426,"children":13427},{},[13428],{"type":32,"value":13429},"There are commands that start a new chain every time they are called. They are often referred to as parent commands. This means that even if they are chained of another command, they would still start a new chain",{"type":27,"tag":793,"props":13431,"children":13434},{"className":13432,"code":13433,"language":1513,"meta":5},[1510],"cy.get('#element') // new chain\n  .click()\n  .get('#modal') // new chain\n  .type('text{enter}')\n",[13435],{"type":27,"tag":653,"props":13436,"children":13437},{"__ignoreMap":5},[13438],{"type":32,"value":13433},{"type":27,"tag":28,"props":13440,"children":13441},{},[13442],{"type":32,"value":13443},"So visually, it seems like we have one long chain here, but in fact, there are two of them.",{"type":27,"tag":28,"props":13445,"children":13446},{},[13447,13449,13455,13456,13461,13462,13467],{"type":32,"value":13448},"Since there are parent commands, you might have guessed that there are also some child commands. A typical example of a child command would be ",{"type":27,"tag":653,"props":13450,"children":13452},{"className":13451},[],[13453],{"type":32,"value":13454},".type()",{"type":32,"value":1591},{"type":27,"tag":653,"props":13457,"children":13459},{"className":13458},[],[13460],{"type":32,"value":12806},{"type":32,"value":1591},{"type":27,"tag":653,"props":13463,"children":13465},{"className":13464},[],[13466],{"type":32,"value":12439},{"type":32,"value":13468}," which need a subject to be applied to. That subject is provided by a previous command - a parent command.",{"type":27,"tag":28,"props":13470,"children":13471},{},[13472],{"type":32,"value":13473},"There’s a third category, and that is a dual command, or a hybrid command. This can be both parent or child command, depending on the position in the chain.",{"type":27,"tag":793,"props":13475,"children":13478},{"className":13476,"code":13477,"language":1513,"meta":5},[1510],"cy.get('.button') // parent\n  .contains('Send') // parent to .click(), but child to .get()\n  .click('#modal') // child()\n",[13479],{"type":27,"tag":653,"props":13480,"children":13481},{"__ignoreMap":5},[13482],{"type":32,"value":13477},{"type":27,"tag":28,"props":13484,"children":13485},{},[13486],{"type":32,"value":13487},"There are some commands that change behavior depending on the position in the chain. For example let’s say we have a following HTML structure with two lists:",{"type":27,"tag":793,"props":13489,"children":13492},{"className":13490,"code":13491,"language":7826,"meta":5},[7824],"\u003Cul id=\"first-list\">\n  \u003Cli>Apples\u003C/li>\n  \u003Cli>Pears\u003C/li>\n\u003C/ul>\n\u003Cul id=\"second-list\">\n  \u003Cli>Grapes\u003C/li>\n  \u003Cli>Apples\u003C/li>\n\u003C/ul>\n",[13493],{"type":27,"tag":653,"props":13494,"children":13495},{"__ignoreMap":5},[13496],{"type":32,"value":13491},{"type":27,"tag":28,"props":13498,"children":13499},{},[13500,13502,13508],{"type":32,"value":13501},"In this scenario, our ",{"type":27,"tag":653,"props":13503,"children":13505},{"className":13504},[],[13506],{"type":32,"value":13507},".contains()",{"type":32,"value":13509}," command will behave differently based on position in the chain:",{"type":27,"tag":793,"props":13511,"children":13514},{"className":13512,"code":13513,"language":1513,"meta":5},[1510],"cy.contains('Apples') // selects apples in first list\n\ncy.get('.second')\n  .contains('Apples') // selects apples in second list\n",[13515],{"type":27,"tag":653,"props":13516,"children":13517},{"__ignoreMap":5},[13518],{"type":32,"value":13513},{"type":27,"tag":45,"props":13520,"children":13522},{"id":13521},"retries-in-cypress",[13523],{"type":32,"value":13524},"Retries in Cypress",{"type":27,"tag":28,"props":13526,"children":13527},{},[13528],{"type":32,"value":13529},"Cypress has retry built in to many of the commands. For example let’s say that we have a list of elements that will take 3 seconds to load. The following command is completely valid and will pass.",{"type":27,"tag":793,"props":13531,"children":13534},{"className":13532,"code":13533,"language":1513,"meta":5},[1510],"cy.get('li')\n",[13535],{"type":27,"tag":653,"props":13536,"children":13537},{"__ignoreMap":5},[13538],{"type":32,"value":13533},{"type":27,"tag":28,"props":13540,"children":13541},{},[13542,13544,13549],{"type":32,"value":13543},"There is a default timeout set to 4000 milliseconds, which you can change in your ",{"type":27,"tag":653,"props":13545,"children":13547},{"className":13546},[],[13548],{"type":32,"value":6028},{"type":32,"value":13550}," file. If you are coming from Selenium, you might know this as fluent wait. It is built-in to most of Cypress commands, so there's nothing extra to be added to our tests to make them wait fluently.",{"type":27,"tag":28,"props":13552,"children":13553},{},[13554],{"type":32,"value":13555},"Let’s now say, that our lists has 5 elements. They don’t render out immediately, but each one takes about 200 milliseconds to appear in our application.",{"type":27,"tag":793,"props":13557,"children":13560},{"className":13558,"code":13559,"language":1513,"meta":5},[1510],"cy.get('li')\n  .should('have.length', 5)\n",[13561],{"type":27,"tag":653,"props":13562,"children":13563},{"__ignoreMap":5},[13564],{"type":32,"value":13559},{"type":27,"tag":28,"props":13566,"children":13567},{},[13568,13570,13575,13577,13582],{"type":32,"value":13569},"This code is also completely valid. That is because ",{"type":27,"tag":653,"props":13571,"children":13573},{"className":13572},[],[13574],{"type":32,"value":12439},{"type":32,"value":13576}," command will not only repeat itself, but it will make the previous command as well. This means that not only we make sure our assertion passes, but Cypress will run our ",{"type":27,"tag":653,"props":13578,"children":13580},{"className":13579},[],[13581],{"type":32,"value":12748},{"type":32,"value":13583}," command as many times needed (within the upper limit) to satisfy our assertion.",{"type":27,"tag":45,"props":13585,"children":13587},{"id":13586},"common-gotcha",[13588],{"type":32,"value":13589},"Common gotcha",{"type":27,"tag":28,"props":13591,"children":13592},{},[13593,13595,13600],{"type":32,"value":13594},"However, when we make a longer chain of commands, this is when it gets a little tricky. our ",{"type":27,"tag":653,"props":13596,"children":13598},{"className":13597},[],[13599],{"type":32,"value":12439},{"type":32,"value":13601}," command will only make our previous command retry. This means that if we have a longer chain like this, we might run into a problem:",{"type":27,"tag":793,"props":13603,"children":13606},{"className":13604,"code":13605,"language":1513,"meta":5},[1510],"cy.get('li')\n  .eq(0)\n  .should('contain.text', 'Apples')\n",[13607],{"type":27,"tag":653,"props":13608,"children":13609},{"__ignoreMap":5},[13610],{"type":32,"value":13605},{"type":27,"tag":28,"props":13612,"children":13613},{},[13614,13616,13622,13623,13629,13631,13636],{"type":32,"value":13615},"While there is nothing entirely wrong with writing our test like this, it opens door to some flakiness. If all of our ",{"type":27,"tag":653,"props":13617,"children":13619},{"className":13618},[],[13620],{"type":32,"value":13621},"\u003Cul>",{"type":32,"value":4164},{"type":27,"tag":653,"props":13624,"children":13626},{"className":13625},[],[13627],{"type":32,"value":13628},"\u003Cli>",{"type":32,"value":13630}," elements will get rendered at the same time, we are safe. But if they load one by one, we run into a problem. Our ",{"type":27,"tag":653,"props":13632,"children":13634},{"className":13633},[],[13635],{"type":32,"value":12748},{"type":32,"value":13637}," command will not select the element we want. Let’s look at the HTML structure again:",{"type":27,"tag":793,"props":13639,"children":13642},{"className":13640,"code":13641,"language":7826,"meta":5},[7824],"\u003Cul id=\"first-list\"> // this list loads second\n  \u003Cli>Apples\u003C/li> \n  \u003Cli>Pears\u003C/li> \n\u003C/ul>\n\u003Cul id=\"second-list\"> // this list loads first\n  \u003Cli>Grapes\u003C/li> \n  \u003Cli>Apples\u003C/li> \n\u003C/ul>\n",[13643],{"type":27,"tag":653,"props":13644,"children":13645},{"__ignoreMap":5},[13646],{"type":32,"value":13641},{"type":27,"tag":28,"props":13648,"children":13649},{},[13650,13651,13656,13658,13664],{"type":32,"value":11386},{"type":27,"tag":653,"props":13652,"children":13654},{"className":13653},[],[13655],{"type":32,"value":12439},{"type":32,"value":13657}," command will try to search for the ",{"type":27,"tag":653,"props":13659,"children":13661},{"className":13660},[],[13662],{"type":32,"value":13663},"Apples",{"type":32,"value":13665}," text on the wrong element. Let’s break down what happens.",{"type":27,"tag":851,"props":13667,"children":13668},{},[13669,13674,13691,13704,13731,13747,13764,13782],{"type":27,"tag":109,"props":13670,"children":13671},{},[13672],{"type":32,"value":13673},"Our app opens and starts loading our lists",{"type":27,"tag":109,"props":13675,"children":13676},{},[13677,13682,13684,13689],{"type":27,"tag":653,"props":13678,"children":13680},{"className":13679},[],[13681],{"type":32,"value":12748},{"type":32,"value":13683}," command is mand for any ",{"type":27,"tag":653,"props":13685,"children":13687},{"className":13686},[],[13688],{"type":32,"value":13628},{"type":32,"value":13690}," elements it can find",{"type":27,"tag":109,"props":13692,"children":13693},{},[13694,13696,13702],{"type":32,"value":13695},"Our app will load ",{"type":27,"tag":653,"props":13697,"children":13699},{"className":13698},[],[13700],{"type":32,"value":13701},"\u003Cul id=\"second-list\"> ",{"type":32,"value":13703}," while our first list is still loading",{"type":27,"tag":109,"props":13705,"children":13706},{},[13707,13709,13714,13716,13721,13723,13729],{"type":32,"value":13708},"Immediately, ",{"type":27,"tag":653,"props":13710,"children":13712},{"className":13711},[],[13713],{"type":32,"value":12748},{"type":32,"value":13715}," command find our ",{"type":27,"tag":653,"props":13717,"children":13719},{"className":13718},[],[13720],{"type":32,"value":13628},{"type":32,"value":13722}," elements in the second list and pass them on to ",{"type":27,"tag":653,"props":13724,"children":13726},{"className":13725},[],[13727],{"type":32,"value":13728},".eq()",{"type":32,"value":13730}," command",{"type":27,"tag":109,"props":13732,"children":13733},{},[13734,13739,13741],{"type":27,"tag":653,"props":13735,"children":13737},{"className":13736},[],[13738],{"type":32,"value":12611},{"type":32,"value":13740}," command will filter out our first element in the list, containing the text ",{"type":27,"tag":653,"props":13742,"children":13744},{"className":13743},[],[13745],{"type":32,"value":13746},"Grapes",{"type":27,"tag":109,"props":13748,"children":13749},{},[13750,13755,13757,13762],{"type":27,"tag":653,"props":13751,"children":13753},{"className":13752},[],[13754],{"type":32,"value":12439},{"type":32,"value":13756}," command will determine whether it contains the text ",{"type":27,"tag":653,"props":13758,"children":13760},{"className":13759},[],[13761],{"type":32,"value":13663},{"type":32,"value":13763},". It does not, so it makes the previous command run again",{"type":27,"tag":109,"props":13765,"children":13766},{},[13767,13769,13774,13776],{"type":32,"value":13768},"But our ",{"type":27,"tag":653,"props":13770,"children":13772},{"className":13771},[],[13773],{"type":32,"value":13728},{"type":32,"value":13775}," command will just perform the exact same action again, because it was passed elements from ",{"type":27,"tag":653,"props":13777,"children":13779},{"className":13778},[],[13780],{"type":32,"value":13781},"\u003Cul id=\"second-list\">",{"type":27,"tag":109,"props":13783,"children":13784},{},[13785,13786,13791,13793,13799,13801,13806,13807,13812],{"type":32,"value":11386},{"type":27,"tag":653,"props":13787,"children":13789},{"className":13788},[],[13790],{"type":32,"value":12748},{"type":32,"value":13792}," command will never get called again, so even when our ",{"type":27,"tag":653,"props":13794,"children":13796},{"className":13795},[],[13797],{"type":32,"value":13798},"\u003Cul id=\"first-list\">",{"type":32,"value":13800}," eventually gets rendered, our ",{"type":27,"tag":653,"props":13802,"children":13804},{"className":13803},[],[13805],{"type":32,"value":13728},{"type":32,"value":4164},{"type":27,"tag":653,"props":13808,"children":13810},{"className":13809},[],[13811],{"type":32,"value":12439},{"type":32,"value":13813}," commands will not reach it",{"type":27,"tag":28,"props":13815,"children":13816},{},[13817,13819,13824,13826,13831,13833,13837],{"type":32,"value":13818},"To improve this, we can leave out the ",{"type":27,"tag":653,"props":13820,"children":13822},{"className":13821},[],[13823],{"type":32,"value":12611},{"type":32,"value":13825}," command, so that our assertion will make the ",{"type":27,"tag":653,"props":13827,"children":13829},{"className":13828},[],[13830],{"type":32,"value":12748},{"type":32,"value":13832}," command try again until there is at least ",{"type":27,"tag":302,"props":13834,"children":13835},{},[13836],{"type":32,"value":4887},{"type":32,"value":13838}," element that contains our text.",{"type":27,"tag":793,"props":13840,"children":13843},{"className":13841,"code":13842,"language":1513,"meta":5},[1510],"cy.get('li')\n  .should('contain.text', 'Apples')\n",[13844],{"type":27,"tag":653,"props":13845,"children":13846},{"__ignoreMap":5},[13847],{"type":32,"value":13842},{"type":27,"tag":28,"props":13849,"children":13850},{},[13851,13853,13858],{"type":32,"value":13852},"If this is not sufficient, we can first make sure that the number of our ",{"type":27,"tag":653,"props":13854,"children":13856},{"className":13855},[],[13857],{"type":32,"value":13628},{"type":32,"value":13859}," elements is correct, and then make our assertion:",{"type":27,"tag":793,"props":13861,"children":13864},{"className":13862,"code":13863,"language":1513,"meta":5},[1510],"cy.get('li')\n  .should('have.length', 4)\n  .eq(0)\n  .should('contain.text', 'Apples')\n",[13865],{"type":27,"tag":653,"props":13866,"children":13867},{"__ignoreMap":5},[13868],{"type":32,"value":13863},{"type":27,"tag":28,"props":13870,"children":13871},{},[13872,13874,13879,13881,13886],{"type":32,"value":13873},"This works, because every Cypress command will wait for the previous command to finish. This way, our ",{"type":27,"tag":653,"props":13875,"children":13877},{"className":13876},[],[13878],{"type":32,"value":12611},{"type":32,"value":13880}," command will run only when we have all of our four ",{"type":27,"tag":653,"props":13882,"children":13884},{"className":13883},[],[13885],{"type":32,"value":13628},{"type":32,"value":13887}," elements present on page and not sooner.",{"type":27,"tag":45,"props":13889,"children":13891},{"id":13890},"writing-better-command-chains",[13892],{"type":32,"value":13893},"Writing better command chains",{"type":27,"tag":28,"props":13895,"children":13896},{},[13897],{"type":32,"value":13898},"Knowing this, you can write your command chains more effectively. Since we know that:",{"type":27,"tag":851,"props":13900,"children":13901},{},[13902,13907,13912],{"type":27,"tag":109,"props":13903,"children":13904},{},[13905],{"type":32,"value":13906},"every commands waits for the previous to finish",{"type":27,"tag":109,"props":13908,"children":13909},{},[13910],{"type":32,"value":13911},"commands pass information from one to another",{"type":27,"tag":109,"props":13913,"children":13914},{},[13915,13920],{"type":27,"tag":653,"props":13916,"children":13918},{"className":13917},[],[13919],{"type":32,"value":12439},{"type":32,"value":13921}," makes previous command retry",{"type":27,"tag":28,"props":13923,"children":13924},{},[13925],{"type":32,"value":13926},"We can prevent flakiness and make our commands stable. Let’s say we have a search box that will render our elements from API. Writing a test like this might be flaky, because as we write our search phrase, API requests are being fired on every letter stroke and response takes a couple of seconds to render in app (this or course depends on the implementation in the app but this practice is not uncommon).",{"type":27,"tag":793,"props":13928,"children":13931},{"className":13929,"code":13930,"language":1513,"meta":5},[1510],"cy.get('#search').type('Apples')\ncy.get('.result').contains('Apples').click()\n",[13932],{"type":27,"tag":653,"props":13933,"children":13934},{"__ignoreMap":5},[13935],{"type":32,"value":13930},{"type":27,"tag":28,"props":13937,"children":13938},{},[13939,13941,13947,13949,13955],{"type":32,"value":13940},"Looking at this example, you know why this may be flaky. Our ",{"type":27,"tag":653,"props":13942,"children":13944},{"className":13943},[],[13945],{"type":32,"value":13946},".result",{"type":32,"value":13948}," item might get re-rendered with new text as our API responses arrive. This may result in ",{"type":27,"tag":653,"props":13950,"children":13952},{"className":13951},[],[13953],{"type":32,"value":13954},"Element detached from DOM",{"type":32,"value":13956}," error that you might have encountered. To make this test more stable, you can write it like this:",{"type":27,"tag":793,"props":13958,"children":13961},{"className":13959,"code":13960,"language":1513,"meta":5},[1510],"cy.get('#search').type('Apples')\ncy.contains('.result', 'Apples').click()\n",[13962],{"type":27,"tag":653,"props":13963,"children":13964},{"__ignoreMap":5},[13965],{"type":32,"value":13960},{"type":27,"tag":28,"props":13967,"children":13968},{},[13969],{"type":32,"value":13970},"This way we click the result that we were searching for even if there’s a delay between entering the search query and showing results.",{"type":27,"tag":28,"props":13972,"children":13973},{},[13974,13976,13981,13982,13987],{"type":32,"value":13975},"Hope this helps. If you feel like this post may help someone or has helped you, feel free to share it on social networks, it really helps. You can also follow me on ",{"type":27,"tag":172,"props":13977,"children":13979},{"href":5770,"rel":13978},[696],[13980],{"type":32,"value":1589},{"type":32,"value":7692},{"type":27,"tag":172,"props":13983,"children":13985},{"href":10953,"rel":13984},[696],[13986],{"type":32,"value":1598},{"type":32,"value":13988},". See ya!",{"title":5,"searchDepth":320,"depth":320,"links":13990},[13991,13992,13993,13994],{"id":13407,"depth":320,"text":13410},{"id":13521,"depth":320,"text":13524},{"id":13586,"depth":320,"text":13589},{"id":13890,"depth":320,"text":13893},"content:writing-better-command-chains-in-cypress:index.md","writing-better-command-chains-in-cypress/index.md","writing-better-command-chains-in-cypress/index",{"_path":13999,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":14000,"description":14001,"date":14002,"published":10,"slug":14003,"tags":14004,"cypressVersion":5959,"readingTime":14008,"body":14012,"_type":329,"_id":14596,"_source":331,"_file":14597,"_stem":14598,"_extension":334},"/google-sign-in-with-cypress","Google Sign in with Cypress","Explanation on how to log in to Google SSO enabled app programmatically and how does the process actually work.","2022-02-07","google-sign-in-with-cypress",[5279,14005,4129,14006,14007],"google","sso","third party",{"text":19,"minutes":14009,"time":14010,"words":14011},8.065,483900,1613,{"type":24,"children":14013,"toc":14589},[14014,14067,14081,14087,14092,14097,14105,14110,14115,14120,14128,14133,14138,14144,14158,14193,14219,14224,14250,14257,14286,14299,14305,14319,14324,14332,14355,14363,14368,14376,14382,14412,14421,14458,14477,14482,14507,14512,14521,14527,14544,14554,14564],{"type":27,"tag":28,"props":14015,"children":14016},{},[14017,14019,14041,14043,14050,14052,14058,14060,14065],{"type":32,"value":14018},"If you ever tried to click on \"Sign in with Google\" button with Cypress, then you know it throws an error. ",{"type":27,"tag":14020,"props":14021,"children":14022},"del",{},[14023,14025,14032,14034],{"type":32,"value":14024},"The reason for that is that Cypress does not support ",{"type":27,"tag":172,"props":14026,"children":14029},{"href":14027,"rel":14028},"https://github.com/cypress-io/cypress/issues/944",[696],[14030],{"type":32,"value":14031},"visiting multiple domains",{"type":32,"value":14033},". This feature is still ",{"type":27,"tag":172,"props":14035,"children":14038},{"href":14036,"rel":14037},"https://github.com/cypress-io/cypress/pull/20022",[696],[14039],{"type":32,"value":14040},"being worked on",{"type":32,"value":14042}," (EDIT: Multi-domain support ",{"type":27,"tag":172,"props":14044,"children":14047},{"href":14045,"rel":14046},"https://docs.cypress.io/guides/references/changelog#9-6-0",[696],[14048],{"type":32,"value":14049},"landed with version 9.6.0",{"type":32,"value":14051},"), however, in order to log in to Google, you don’t need this feature to land. The good news is, that while the ",{"type":27,"tag":653,"props":14053,"children":14055},{"className":14054},[],[14056],{"type":32,"value":14057},".visit()",{"type":32,"value":14059}," command redirects in your app allow you to be on a single domain only, ",{"type":27,"tag":653,"props":14061,"children":14063},{"className":14062},[],[14064],{"type":32,"value":12824},{"type":32,"value":14066}," can go anywhere.",{"type":27,"tag":28,"props":14068,"children":14069},{},[14070,14072,14079],{"type":32,"value":14071},"Instead of clicking through login, you can login programmatically and appear to your app as a logged in user. In this post I will walk you through the process. You can find the explanation in ",{"type":27,"tag":172,"props":14073,"children":14076},{"href":14074,"rel":14075},"https://docs.cypress.io/guides/testing-strategies/google-authentication",[696],[14077],{"type":32,"value":14078},"Cypress docs",{"type":32,"value":14080}," as well, but if you want some more context on the nuts and bolts of this, make sure to continue reading.",{"type":27,"tag":45,"props":14082,"children":14084},{"id":14083},"how-does-google-sso-work",[14085],{"type":32,"value":14086},"How does Google SSO work?",{"type":27,"tag":28,"props":14088,"children":14089},{},[14090],{"type":32,"value":14091},"Google SSO is following a standard OAuth 2.0 flow. You might find some graphs on the internet. However, I have always found them quite overwhelming. Plus they told me nothing about how should I test them in Cypress. So let me provide you with a metaphor.",{"type":27,"tag":28,"props":14093,"children":14094},{},[14095],{"type":32,"value":14096},"Imagine an exclusive club. In front of that club, there’s a guard that let’s guests in. Once he allows you to get in, you’ll get a stamp sign on your hand. Only those that have this stamp sign on, are allow to enter the club, dance, order drinks and stay in the club. In your application that would be some kind of token, usually stored in your cookies, that will authenticate you against your app.",{"type":27,"tag":28,"props":14098,"children":14099},{},[14100],{"type":27,"tag":959,"props":14101,"children":14104},{"alt":14102,"src":14103},"Club metaphor for authentication","club.png",[],{"type":27,"tag":28,"props":14106,"children":14107},{},[14108],{"type":32,"value":14109},"In this metaphor, the club is your application and all the data that is stored in database. In order to access it, you need to be authenticated. In a typical login/password situation you would tell the guard the password and your name and he’ll let you in.",{"type":27,"tag":28,"props":14111,"children":14112},{},[14113],{"type":32,"value":14114},"Of course, there could be some more situations. You’re not on the list (user not registered), or use a wrong password. Or the club is closed due to technical problems (error 500).",{"type":27,"tag":28,"props":14116,"children":14117},{},[14118],{"type":32,"value":14119},"So where does Google SSO come in? In this particular situation, you don’t have a password, but instead you have a good friend that knows the club owner and can get you to that super-exclusive club. The guard knows about this, trusts your friend, and will let you in.",{"type":27,"tag":28,"props":14121,"children":14122},{},[14123],{"type":27,"tag":959,"props":14124,"children":14127},{"alt":14125,"src":14126},"Google authentication","google.png",[],{"type":27,"tag":28,"props":14129,"children":14130},{},[14131],{"type":32,"value":14132},"Notice that no password is needed in this situation, because everything depends on the communication of your friend and the guard. You just need to have that friend that will communicate with the guard. In our application, these will be two servers talking. In general, it actually does not matter all that much whether it is the Google server, or some other OAuth provider.",{"type":27,"tag":28,"props":14134,"children":14135},{},[14136],{"type":32,"value":14137},"It goes without saying, that if you want to sign into an application with Google SSO, you need have an application that uses it. But how does one set up such an application? I want to describe this before we dive in to writing a Cypress test. It’s going to be useful in order to get a good understanding of how to solve the problem of logging into Google with Cypress.",{"type":27,"tag":45,"props":14139,"children":14141},{"id":14140},"how-to-setup-a-google-sso-enabled-app",[14142],{"type":32,"value":14143},"How to setup a Google SSO-enabled app",{"type":27,"tag":28,"props":14145,"children":14146},{},[14147,14149,14156],{"type":32,"value":14148},"Google has made a creation of an SSO-enabled app pretty easy for developers. There are some ",{"type":27,"tag":172,"props":14150,"children":14153},{"href":14151,"rel":14152},"https://egghead.io/lessons/javascript-add-a-google-oauth-2-0-login-button-to-your-site",[696],[14154],{"type":32,"value":14155},"great examples",{"type":32,"value":14157}," on how to set it up for your application. These require you to follow a couple of simple steps. There are a couple of steps to this:",{"type":27,"tag":851,"props":14159,"children":14160},{},[14161,14173,14178,14183,14188],{"type":27,"tag":109,"props":14162,"children":14163},{},[14164,14166],{"type":32,"value":14165},"Create a OAuth project on ",{"type":27,"tag":172,"props":14167,"children":14170},{"href":14168,"rel":14169},"https://console.developers.google.com/",[696],[14171],{"type":32,"value":14172},"Google developer console",{"type":27,"tag":109,"props":14174,"children":14175},{},[14176],{"type":32,"value":14177},"Set up which domains this application can be used on",{"type":27,"tag":109,"props":14179,"children":14180},{},[14181],{"type":32,"value":14182},"Set up where you will be redirected after a user returns from Google login screen",{"type":27,"tag":109,"props":14184,"children":14185},{},[14186],{"type":32,"value":14187},"Add a button to your frontend",{"type":27,"tag":109,"props":14189,"children":14190},{},[14191],{"type":32,"value":14192},"Add a validation to your backend",{"type":27,"tag":28,"props":14194,"children":14195},{},[14196,14198,14203,14205,14210,14212,14217],{"type":32,"value":14197},"Step 1 will create a ",{"type":27,"tag":79,"props":14199,"children":14200},{},[14201],{"type":32,"value":14202},"Client ID",{"type":32,"value":14204}," and a ",{"type":27,"tag":79,"props":14206,"children":14207},{},[14208],{"type":32,"value":14209},"Client secret",{"type":32,"value":14211}," for you. We’ll get to these items shortly. Just remember they exist.  The names might be a little confusing, because ",{"type":27,"tag":302,"props":14213,"children":14214},{},[14215],{"type":32,"value":14216},"client",{"type":32,"value":14218}," in this context points to your application, and not the user that is trying to log in to your application. But these two items will be important, when we’ll attempt to log in.",{"type":27,"tag":28,"props":14220,"children":14221},{},[14222],{"type":32,"value":14223},"The rest of these steps are done by developers. But as a tester, it is good for you to know, that there is such a thing as a Google developer console. And that in order to create that \"Log in with Google\" button, you need to register your app in the console. In our metaphor, this would be the part where we establish the relationship with our influential friend.",{"type":27,"tag":28,"props":14225,"children":14226},{},[14227,14229,14234,14236,14241,14243,14248],{"type":32,"value":14228},"By the way, if you want to try this whole process, I suggest you create your own project in ",{"type":27,"tag":172,"props":14230,"children":14232},{"href":14168,"rel":14231},[696],[14233],{"type":32,"value":14172},{"type":32,"value":14235},". Copy the ",{"type":27,"tag":79,"props":14237,"children":14238},{},[14239],{"type":32,"value":14240},"Client id",{"type":32,"value":14242}," (you will just need that one), and set your origin URLs to ",{"type":27,"tag":653,"props":14244,"children":14246},{"className":14245},[],[14247],{"type":32,"value":6362},{"type":32,"value":14249},". If it looks something like this, you are on the right track:",{"type":27,"tag":28,"props":14251,"children":14252},{},[14253],{"type":27,"tag":959,"props":14254,"children":14256},{"alt":14172,"src":14255},"console.png",[],{"type":27,"tag":28,"props":14258,"children":14259},{},[14260,14262,14268,14270,14276,14278,14285],{"type":32,"value":14261},"You can do this on my ",{"type":27,"tag":172,"props":14263,"children":14265},{"href":9303,"rel":14264},[696],[14266],{"type":32,"value":14267},"Trello app",{"type":32,"value":14269},", or follow the ",{"type":27,"tag":172,"props":14271,"children":14273},{"href":14074,"rel":14272},[696],[14274],{"type":32,"value":14275},"instructions on Cypress docs",{"type":32,"value":14277}," to set this up on ",{"type":27,"tag":172,"props":14279,"children":14282},{"href":14280,"rel":14281},"https://github.com/cypress-io/cypress-realworld-app",[696],[14283],{"type":32,"value":14284},"Cypress Real World app",{"type":32,"value":256},{"type":27,"tag":28,"props":14287,"children":14288},{},[14289,14291,14297],{"type":32,"value":14290},"One thing that you might have noticed on the screenshot, but I haven’t mentioned yet, is the ",{"type":27,"tag":653,"props":14292,"children":14294},{"className":14293},[],[14295],{"type":32,"value":14296},"https://developers.google.com/oauthplayground",{"type":32,"value":14298}," URL. Let’s look into what it is.",{"type":27,"tag":45,"props":14300,"children":14302},{"id":14301},"how-to-set-up-the-user-you-want-to-log-in",[14303],{"type":32,"value":14304},"How to set up the user you want to log in",{"type":27,"tag":28,"props":14306,"children":14307},{},[14308,14310,14317],{"type":32,"value":14309},"You can choose whichever Google user you want. Got one? Good. Now go to ",{"type":27,"tag":172,"props":14311,"children":14314},{"href":14312,"rel":14313},"https://developers.google.com/oauthplayground/",[696],[14315],{"type":32,"value":14316},"OAuth Playground",{"type":32,"value":14318},". This service will allow you to create a \"refresh token\". With this token, you can authenticate against Google API. What this means is, that you can access data from your Google account. In most \"Google sign in enabled\" apps, the data would be things like your profile picture, email, your name, or some other data from your account.",{"type":27,"tag":28,"props":14320,"children":14321},{},[14322],{"type":32,"value":14323},"There are many options on this playground, but since we just want to authenticate in our app, we want to choose \"Google Oauth API v2\".",{"type":27,"tag":28,"props":14325,"children":14326},{},[14327],{"type":27,"tag":959,"props":14328,"children":14331},{"alt":14329,"src":14330},"Google oauth playground scope","playground_api.png",[],{"type":27,"tag":28,"props":14333,"children":14334},{},[14335,14337,14342,14344,14348,14349,14353],{"type":32,"value":14336},"Don’t try to select all of the different APIs, just to be sure. Scoping the authorization to a minimal degree is certainly the better way. Not only we want to scope the data that the refresh token will have access to, but we want to scope ",{"type":27,"tag":79,"props":14338,"children":14339},{},[14340],{"type":32,"value":14341},"where",{"type":32,"value":14343}," can this token be used. To use it only in our project, check the \"Use your own OAuth credentials\" checkbox and enter the ",{"type":27,"tag":79,"props":14345,"children":14346},{},[14347],{"type":32,"value":14202},{"type":32,"value":4164},{"type":27,"tag":79,"props":14350,"children":14351},{},[14352],{"type":32,"value":14209},{"type":32,"value":14354},". Remember how I mentioned those?",{"type":27,"tag":28,"props":14356,"children":14357},{},[14358],{"type":27,"tag":959,"props":14359,"children":14362},{"alt":14360,"src":14361},"Google oauth configuration","playground_oauth.png",[],{"type":27,"tag":28,"props":14364,"children":14365},{},[14366],{"type":32,"value":14367},"After setting this up, click the \"Authorize API\" button and proceed to step 2. You are just click of a button away. Click on \"Exchange authorization code for tokens\" button and copy the refresh token.",{"type":27,"tag":28,"props":14369,"children":14370},{},[14371],{"type":27,"tag":959,"props":14372,"children":14375},{"alt":14373,"src":14374},"Exchange authorization code for tokens","playground_token.png",[],{"type":27,"tag":45,"props":14377,"children":14379},{"id":14378},"login-the-user-in-our-app",[14380],{"type":32,"value":14381},"Login the user in our app",{"type":27,"tag":28,"props":14383,"children":14384},{},[14385,14387,14392,14393,14397,14398,14403,14405,14410],{"type":32,"value":14386},"As you might have noticed, there are two big parts to all this. One is the application (project in the Google developer console) and the other part is the user we want to authenticate (OAuth playground). Once we have ",{"type":27,"tag":79,"props":14388,"children":14389},{},[14390],{"type":32,"value":14391},"Client Id",{"type":32,"value":3372},{"type":27,"tag":79,"props":14394,"children":14395},{},[14396],{"type":32,"value":14209},{"type":32,"value":4164},{"type":27,"tag":79,"props":14399,"children":14400},{},[14401],{"type":32,"value":14402},"refresh token",{"type":32,"value":14404}," ready, we are ready to log in programmatically. To do that, we’ll create a ",{"type":27,"tag":653,"props":14406,"children":14408},{"className":14407},[],[14409],{"type":32,"value":12824},{"type":32,"value":14411}," in our test.",{"type":27,"tag":793,"props":14413,"children":14416},{"className":14414,"code":14415,"language":1513,"meta":5},[1510],"cy.request({\n    method: 'POST',\n    url: 'https://www.googleapis.com/oauth2/v4/token',\n    body: {\n      grant_type: 'refresh_token',\n      client_id: Cypress.env('googleClientId'),\n      client_secret: Cypress.env('googleClientSecret'),\n      refresh_token: Cypress.env('googleRefreshToken'),\n    },\n  }).then(({ body }) => {\n    const { id_token } = body\n  })\n",[14417],{"type":27,"tag":653,"props":14418,"children":14419},{"__ignoreMap":5},[14420],{"type":32,"value":14415},{"type":27,"tag":1029,"props":14422,"children":14423},{},[14424],{"type":27,"tag":28,"props":14425,"children":14426},{},[14427,14429,14434,14436,14441,14443,14449,14451,14457],{"type":32,"value":14428},"Side note: See that I am storing all the important keys in ",{"type":27,"tag":653,"props":14430,"children":14432},{"className":14431},[],[14433],{"type":32,"value":9544},{"type":32,"value":14435},". I don't actually save them in ",{"type":27,"tag":653,"props":14437,"children":14439},{"className":14438},[],[14440],{"type":32,"value":6028},{"type":32,"value":14442}," and neither should you. Never save these in your repository and never commit them. Instead, keep them in your environment and add them to Cypress at runtime. ",{"type":27,"tag":172,"props":14444,"children":14446},{"href":14445},"/create-a-configuration-plugin-in-cypress",[14447],{"type":32,"value":14448},"I wrote a whole article",{"type":32,"value":14450}," on how you can do this and you can find an example in ",{"type":27,"tag":172,"props":14452,"children":14455},{"href":14453,"rel":14454},"https://docs.cypress.io/guides/testing-strategies/google-authentication#Setting-Google-app-credentials-in-Cypress",[696],[14456],{"type":32,"value":14078},{"type":32,"value":11757},{"type":27,"tag":28,"props":14459,"children":14460},{},[14461,14462,14468,14470,14475],{"type":32,"value":3349},{"type":27,"tag":653,"props":14463,"children":14465},{"className":14464},[],[14466],{"type":32,"value":14467},"id_token",{"type":32,"value":14469}," that is returned from the server is the information (among other) that the Google server will usually respond with once it closes the login window. Instead of interacting with UI, we have now achieved retrieving this information via our ",{"type":27,"tag":653,"props":14471,"children":14473},{"className":14472},[],[14474],{"type":32,"value":12824},{"type":32,"value":14476}," command.",{"type":27,"tag":28,"props":14478,"children":14479},{},[14480],{"type":32,"value":14481},"From this moment on, your application takes over and handles that information. Depending on how well you know the application you are testing, it should get easier from this point on.",{"type":27,"tag":28,"props":14483,"children":14484},{},[14485,14487,14492,14494,14499,14501,14506],{"type":32,"value":14486},"Different applications might slightly differ from one another in how they use this information. Your application might fire an http request, set some cookies, do some redirects, etc. Usually it will take the ",{"type":27,"tag":653,"props":14488,"children":14490},{"className":14489},[],[14491],{"type":32,"value":14467},{"type":32,"value":14493}," and validate it on server. This means that you may need to do another ",{"type":27,"tag":653,"props":14495,"children":14497},{"className":14496},[],[14498],{"type":32,"value":12824},{"type":32,"value":14500}," inside the ",{"type":27,"tag":653,"props":14502,"children":14504},{"className":14503},[],[14505],{"type":32,"value":13338},{"type":32,"value":14476},{"type":27,"tag":28,"props":14508,"children":14509},{},[14510],{"type":32,"value":14511},"In my Trello application, I have a slightly simplified workflow, where I send the token to the server, where it is validated. If the validation fails, server will return the error. But if it succeeds, server will respond with an authorization token, and the user will become authorized to perform actions in the app. So my request will look something like this:",{"type":27,"tag":793,"props":14513,"children":14516},{"className":14514,"code":14515,"language":1513,"meta":5},[1510],"cy.request({\n    method: 'POST',\n    url: 'https://www.googleapis.com/oauth2/v4/token',\n    body: {\n      grant_type: 'refresh_token',\n      client_id: Cypress.env('googleClientId'),\n      client_secret: Cypress.env('googleClientSecret'),\n      refresh_token: Cypress.env('googleRefreshToken'),\n    },\n  }).then(({ body }) => {\n    const { id_token } = body\n      cy.request('POST', '/api/login', { jwt: id_token })\n        .then( ({ body: { accessToken } }) => {\n          cy.setCookie('trello_token', accessToken)\n        })\n  })\n",[14517],{"type":27,"tag":653,"props":14518,"children":14519},{"__ignoreMap":5},[14520],{"type":32,"value":14515},{"type":27,"tag":45,"props":14522,"children":14524},{"id":14523},"short-faq",[14525],{"type":32,"value":14526},"Short FAQ",{"type":27,"tag":28,"props":14528,"children":14529},{},[14530,14542],{"type":27,"tag":79,"props":14531,"children":14532},{},[14533,14535,14540],{"type":32,"value":14534},"Do I need to add ",{"type":27,"tag":172,"props":14536,"children":14538},{"href":14296,"rel":14537},[696],[14539],{"type":32,"value":14296},{"type":32,"value":14541}," to the redirect URIs in my project?",{"type":32,"value":14543},"\nYes. Without this the user token generated on OAuth Playground will not work for logging into your app.",{"type":27,"tag":28,"props":14545,"children":14546},{},[14547,14552],{"type":27,"tag":79,"props":14548,"children":14549},{},[14550],{"type":32,"value":14551},"Can I use any Google login for this?",{"type":32,"value":14553},"\nYes, it can be a gmail.com address or a company address that uses Google apps. You will need to generate a refresh token for each of the users you want to log in.",{"type":27,"tag":28,"props":14555,"children":14556},{},[14557,14562],{"type":27,"tag":79,"props":14558,"children":14559},{},[14560],{"type":32,"value":14561},"Can I do this without knowing the Client ID and Client secret?",{"type":32,"value":14563},"\nNo. You need to obtain these if you want to use this method. However, there’s a good chance you already have them available in your environment if you are using Google SSO for your local development.",{"type":27,"tag":28,"props":14565,"children":14566},{},[14567,14569,14575,14577,14582,14583,14588],{"type":32,"value":14568},"Hope this helps. If you feel like this might help someone share the article on your social network. If you want some more articles, good news! They are coming! You can subscribe to a newsletter, check out my ",{"type":27,"tag":172,"props":14570,"children":14572},{"href":12181,"rel":14571},[696],[14573],{"type":32,"value":14574},"YouTube channel",{"type":32,"value":14576},", or follow me on ",{"type":27,"tag":172,"props":14578,"children":14580},{"href":5770,"rel":14579},[696],[14581],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":14584,"children":14586},{"href":10953,"rel":14585},[696],[14587],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":14590},[14591,14592,14593,14594,14595],{"id":14083,"depth":320,"text":14086},{"id":14140,"depth":320,"text":14143},{"id":14301,"depth":320,"text":14304},{"id":14378,"depth":320,"text":14381},{"id":14523,"depth":320,"text":14526},"content:google-sign-in-with-cypress:index.md","google-sign-in-with-cypress/index.md","google-sign-in-with-cypress/index",{"_path":14600,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":14601,"description":14602,"date":14603,"published":10,"slug":14604,"tags":14605,"cypressVersion":14607,"readingTime":14608,"body":14612,"_type":329,"_id":15134,"_source":331,"_file":15135,"_stem":15136,"_extension":334},"/switch-between-environments-in-cypress","Switch between environments in Cypress","There are multiple ways you can switch to different environments in Cypress. In this blogpost, I explain which ones you can use and show some examples.","2022-01-24","switch-between-environments-in-cypress",[5279,14606,5957,7555,5995],"environment","v9.8.0",{"text":585,"minutes":14609,"time":14610,"words":14611},3.55,213000,710,{"type":24,"children":14613,"toc":15125},[14614,14619,14628,14640,14651,14691,14700,14742,14748,14767,14773,14800,14809,14822,14828,14840,14849,14855,14867,14876,14902,14911,14916,14925,14963,14969,14989,14999,15025,15034,15079,15088,15100],{"type":27,"tag":28,"props":14615,"children":14616},{},[14617],{"type":32,"value":14618},"You probably want to run your tests on mulitple environments. Many times I’ve seen people doing something like this:",{"type":27,"tag":793,"props":14620,"children":14623},{"className":14621,"code":14622,"language":1513,"meta":5},[1510],"cy.visit(Cypress.env('localUrl'))\n",[14624],{"type":27,"tag":653,"props":14625,"children":14626},{"__ignoreMap":5},[14627],{"type":32,"value":14622},{"type":27,"tag":28,"props":14629,"children":14630},{},[14631,14633,14638],{"type":32,"value":14632},"While I am a fan of using ",{"type":27,"tag":653,"props":14634,"children":14636},{"className":14635},[],[14637],{"type":32,"value":9544},{"type":32,"value":14639}," for storing values, there are multiple better way you can go around this.",{"type":27,"tag":45,"props":14641,"children":14643},{"id":14642},"using-baseurl",[14644,14646],{"type":32,"value":14645},"Using ",{"type":27,"tag":653,"props":14647,"children":14649},{"className":14648},[],[14650],{"type":32,"value":5995},{"type":27,"tag":28,"props":14652,"children":14653},{},[14654,14656,14661,14663,14668,14670,14675,14677,14682,14684,14689],{"type":32,"value":14655},"For starters, let's look at ",{"type":27,"tag":653,"props":14657,"children":14659},{"className":14658},[],[14660],{"type":32,"value":14057},{"type":32,"value":14662}," command. This command will open browser with a location you define. But it also takes the ",{"type":27,"tag":653,"props":14664,"children":14666},{"className":14665},[],[14667],{"type":32,"value":5995},{"type":32,"value":14669}," attribute from your ",{"type":27,"tag":653,"props":14671,"children":14673},{"className":14672},[],[14674],{"type":32,"value":6028},{"type":32,"value":14676},". This means that when you have your ",{"type":27,"tag":653,"props":14678,"children":14680},{"className":14679},[],[14681],{"type":32,"value":5995},{"type":32,"value":14683}," set to ",{"type":27,"tag":653,"props":14685,"children":14687},{"className":14686},[],[14688],{"type":32,"value":6362},{"type":32,"value":14690},", you can write your urls like this:",{"type":27,"tag":793,"props":14692,"children":14695},{"className":14693,"code":14694,"language":1513,"meta":5},[1510],"cy.visit('/dashboard')\n",[14696],{"type":27,"tag":653,"props":14697,"children":14698},{"__ignoreMap":5},[14699],{"type":32,"value":14694},{"type":27,"tag":28,"props":14701,"children":14702},{},[14703,14705,14711,14713,14718,14720,14725,14726,14732,14734,14740],{"type":32,"value":14704},"and the resolved location will be ",{"type":27,"tag":653,"props":14706,"children":14708},{"className":14707},[],[14709],{"type":32,"value":14710},"http://localhost:3000/dashboard",{"type":32,"value":14712},". The ",{"type":27,"tag":653,"props":14714,"children":14716},{"className":14715},[],[14717],{"type":32,"value":5995},{"type":32,"value":14719}," attribute is used in ",{"type":27,"tag":653,"props":14721,"children":14723},{"className":14722},[],[14724],{"type":32,"value":12824},{"type":32,"value":4164},{"type":27,"tag":653,"props":14727,"children":14729},{"className":14728},[],[14730],{"type":32,"value":14731},".intercept()",{"type":32,"value":14733}," commands as well. This is better than using an env variable. For example, you don't need to deal with renaming everything if you decide one day to change the name of your the ",{"type":27,"tag":653,"props":14735,"children":14737},{"className":14736},[],[14738],{"type":32,"value":14739},"localUrl",{"type":32,"value":14741}," env variable.",{"type":27,"tag":45,"props":14743,"children":14745},{"id":14744},"rewriting-cypressconfigjs",[14746],{"type":32,"value":14747},"Rewriting cypress.config.js",{"type":27,"tag":28,"props":14749,"children":14750},{},[14751,14753,14758,14760,14765],{"type":32,"value":14752},"The easiest way to switch environments is to simply rewrite your ",{"type":27,"tag":653,"props":14754,"children":14756},{"className":14755},[],[14757],{"type":32,"value":6028},{"type":32,"value":14759}," file and set ",{"type":27,"tag":653,"props":14761,"children":14763},{"className":14762},[],[14764],{"type":32,"value":5995},{"type":32,"value":14766}," to a different value each time you want to switch environments. This is of course tedious and takes way too much work if you need to switch often. Also, its not the best way if you use version control and want to run your tests in CI. You need to make a commit every time you want to test against different envrionment and creates mess in your git history.",{"type":27,"tag":45,"props":14768,"children":14770},{"id":14769},"pointing-to-a-different-configuration-file",[14771],{"type":32,"value":14772},"Pointing to a different configuration file",{"type":27,"tag":28,"props":14774,"children":14775},{},[14776,14778,14783,14785,14791,14793,14798],{"type":32,"value":14777},"Instead of using the default ",{"type":27,"tag":653,"props":14779,"children":14781},{"className":14780},[],[14782],{"type":32,"value":6028},{"type":32,"value":14784},", you can point Cypress to a completely different file. Let's say you have a ",{"type":27,"tag":653,"props":14786,"children":14788},{"className":14787},[],[14789],{"type":32,"value":14790},"cypress.production.config.js",{"type":32,"value":14792}," file, where your ",{"type":27,"tag":653,"props":14794,"children":14796},{"className":14795},[],[14797],{"type":32,"value":5995},{"type":32,"value":14799}," attribute is set to your production server. To run Cypress using this file, you can do the following:",{"type":27,"tag":793,"props":14801,"children":14804},{"className":14802,"code":14803,"language":1084,"meta":5},[1082],"npx cypress open --config-file cypress.production.config.js\n",[14805],{"type":27,"tag":653,"props":14806,"children":14807},{"__ignoreMap":5},[14808],{"type":32,"value":14803},{"type":27,"tag":28,"props":14810,"children":14811},{},[14812,14814,14820],{"type":32,"value":14813},"This of course works for ",{"type":27,"tag":653,"props":14815,"children":14817},{"className":14816},[],[14818],{"type":32,"value":14819},"cypress run",{"type":32,"value":14821}," command as well.",{"type":27,"tag":45,"props":14823,"children":14825},{"id":14824},"passing-a-cli-flag",[14826],{"type":32,"value":14827},"Passing a CLI flag",{"type":27,"tag":28,"props":14829,"children":14830},{},[14831,14833,14838],{"type":32,"value":14832},"If you don’t want to change the whole config, you can just change the ",{"type":27,"tag":653,"props":14834,"children":14836},{"className":14835},[],[14837],{"type":32,"value":5995},{"type":32,"value":14839}," attribute by passing it through CLI:",{"type":27,"tag":793,"props":14841,"children":14844},{"className":14842,"code":14843,"language":1084,"meta":5},[1082],"npx cypress open --baseUrl http://localhost:3000\n",[14845],{"type":27,"tag":653,"props":14846,"children":14847},{"__ignoreMap":5},[14848],{"type":32,"value":14843},{"type":27,"tag":45,"props":14850,"children":14852},{"id":14851},"using-setupnodeevents",[14853],{"type":32,"value":14854},"Using setupNodeEvents",{"type":27,"tag":28,"props":14856,"children":14857},{},[14858,14860,14865],{"type":32,"value":14859},"I wrote about this approach in the past, so you can check out a ",{"type":27,"tag":172,"props":14861,"children":14862},{"href":14445},[14863],{"type":32,"value":14864},"more detailed article here.",{"type":32,"value":14866}," Basically, as Cypress opens, you can change the config on the fly and rewrite anything in the config. See the following code:",{"type":27,"tag":793,"props":14868,"children":14871},{"className":14869,"code":14870,"filename":6028,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n      config.baseUrl = 'http://localhost:3000'\n      return config\n    }\n  }\n})\n",[14872],{"type":27,"tag":653,"props":14873,"children":14874},{"__ignoreMap":5},[14875],{"type":32,"value":14870},{"type":27,"tag":28,"props":14877,"children":14878},{},[14879,14881,14886,14887,14893,14895,14900],{"type":32,"value":14880},"This will mean that even if you have ",{"type":27,"tag":653,"props":14882,"children":14884},{"className":14883},[],[14885],{"type":32,"value":5995},{"type":32,"value":14683},{"type":27,"tag":653,"props":14888,"children":14890},{"className":14889},[],[14891],{"type":32,"value":14892},"https://cypress.io",{"type":32,"value":14894},", when you open Cypress, it will be rewritten to ",{"type":27,"tag":653,"props":14896,"children":14898},{"className":14897},[],[14899],{"type":32,"value":6362},{"type":32,"value":14901},". If you want to seamlessly switch between environments, you can pass an env variable via CLI and then read it in your config. So for example you can write a config like this:",{"type":27,"tag":793,"props":14903,"children":14906},{"className":14904,"code":14905,"filename":6028,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n      if (config.env.environment === 'local') {\n        config.baseUrl = 'http://localhost:3000'\n      }\n      return config\n    }\n  }\n})\n",[14907],{"type":27,"tag":653,"props":14908,"children":14909},{"__ignoreMap":5},[14910],{"type":32,"value":14905},{"type":27,"tag":28,"props":14912,"children":14913},{},[14914],{"type":32,"value":14915},"And then pass this to your CLI:",{"type":27,"tag":793,"props":14917,"children":14920},{"className":14918,"code":14919,"language":1084,"meta":5},[1082],"npx cypress open --env environment=local\n",[14921],{"type":27,"tag":653,"props":14922,"children":14923},{"__ignoreMap":5},[14924],{"type":32,"value":14919},{"type":27,"tag":28,"props":14926,"children":14927},{},[14928,14930,14936,14938,14943,14945,14950,14952,14957,14958],{"type":32,"value":14929},"As a result, whenever you pass the ",{"type":27,"tag":653,"props":14931,"children":14933},{"className":14932},[],[14934],{"type":32,"value":14935},"local",{"type":32,"value":14937}," environment flag, Cypress will rewrite your ",{"type":27,"tag":653,"props":14939,"children":14941},{"className":14940},[],[14942],{"type":32,"value":5995},{"type":32,"value":14944}," to ",{"type":27,"tag":653,"props":14946,"children":14948},{"className":14947},[],[14949],{"type":32,"value":6362},{"type":32,"value":14951},". If you pass a different variable, or don't pass anything, Cypress will use the default ",{"type":27,"tag":653,"props":14953,"children":14955},{"className":14954},[],[14956],{"type":32,"value":5995},{"type":32,"value":3103},{"type":27,"tag":653,"props":14959,"children":14961},{"className":14960},[],[14962],{"type":32,"value":6028},{"type":27,"tag":45,"props":14964,"children":14966},{"id":14965},"using-module-api",[14967],{"type":32,"value":14968},"Using Module API",{"type":27,"tag":28,"props":14970,"children":14971},{},[14972,14974,14979,14981,14987],{"type":32,"value":14973},"Module API let’s you be very flexible in how you run your tests. When using it, instead of typing ",{"type":27,"tag":653,"props":14975,"children":14977},{"className":14976},[],[14978],{"type":32,"value":10038},{"type":32,"value":14980},", you will run your own script. This way, you will type e.g. ",{"type":27,"tag":653,"props":14982,"children":14984},{"className":14983},[],[14985],{"type":32,"value":14986},"node cypress-run.js",{"type":32,"value":14988}," to your terminal and create a file that looks something like this:",{"type":27,"tag":793,"props":14990,"children":14994},{"className":14991,"code":14992,"filename":14993,"language":1513,"meta":5},[1510],"const cypress = require('cypress')\ncypress.run()\n","cypress/cypress-run.js",[14995],{"type":27,"tag":653,"props":14996,"children":14997},{"__ignoreMap":5},[14998],{"type":32,"value":14992},{"type":27,"tag":28,"props":15000,"children":15001},{},[15002,15003,15009,15011,15016,15018,15023],{"type":32,"value":3349},{"type":27,"tag":653,"props":15004,"children":15006},{"className":15005},[],[15007],{"type":32,"value":15008},".run()",{"type":32,"value":15010}," function will take an object as an argument. In this object, you can define various properties. The ",{"type":27,"tag":653,"props":15012,"children":15014},{"className":15013},[],[15015],{"type":32,"value":5995},{"type":32,"value":15017}," property will be nested inside ",{"type":27,"tag":653,"props":15019,"children":15021},{"className":15020},[],[15022],{"type":32,"value":5957},{"type":32,"value":15024}," object like this:",{"type":27,"tag":793,"props":15026,"children":15029},{"className":15027,"code":15028,"filename":14993,"language":1513,"meta":5},[1510],"const cypress = require('cypress')\ncypress.run({\n  config: {\n    baseUrl: 'http://localhost:3000'\n  }\n})\n",[15030],{"type":27,"tag":653,"props":15031,"children":15032},{"__ignoreMap":5},[15033],{"type":32,"value":15028},{"type":27,"tag":28,"props":15035,"children":15036},{},[15037,15039,15044,15046,15052,15054,15060,15062,15068,15070,15077],{"type":32,"value":15038},"This enables us to write a function that will resolve our ",{"type":27,"tag":653,"props":15040,"children":15042},{"className":15041},[],[15043],{"type":32,"value":5995},{"type":32,"value":15045}," based on some logic. For example, we can tell Cypress to setup a staging url when running on CI. Most of the CI providers have a ",{"type":27,"tag":653,"props":15047,"children":15049},{"className":15048},[],[15050],{"type":32,"value":15051},"CI",{"type":32,"value":15053}," environment variable, which is set to ",{"type":27,"tag":653,"props":15055,"children":15057},{"className":15056},[],[15058],{"type":32,"value":15059},"true",{"type":32,"value":15061}," and can be accessed in our ",{"type":27,"tag":653,"props":15063,"children":15065},{"className":15064},[],[15066],{"type":32,"value":15067},"cypress-run.js",{"type":32,"value":15069}," file. You can also use an ",{"type":27,"tag":172,"props":15071,"children":15074},{"href":15072,"rel":15073},"https://www.npmjs.com/package/is-ci",[696],[15075],{"type":32,"value":15076},"npm package for this",{"type":32,"value":15078},", but it essentially does the same thing. The module api file will now look like this:",{"type":27,"tag":793,"props":15080,"children":15083},{"className":15081,"code":15082,"filename":14993,"language":1513,"meta":5},[1510],"const cypress = require('cypress')\nlet baseUrl = process.env.CI ? 'http://staging.example.com' : 'http://localhost:3000'\n\ncypress.run({\n  config: { baseUrl } // baseUrl will resolve on line 2\n})\n",[15084],{"type":27,"tag":653,"props":15085,"children":15086},{"__ignoreMap":5},[15087],{"type":32,"value":15082},{"type":27,"tag":28,"props":15089,"children":15090},{},[15091,15093,15098],{"type":32,"value":15092},"After this, things can get even more complex. You can customize a logic that will resolve your ",{"type":27,"tag":653,"props":15094,"children":15096},{"className":15095},[],[15097],{"type":32,"value":5995},{"type":32,"value":15099}," based on multiple conditions.",{"type":27,"tag":28,"props":15101,"children":15102},{},[15103,15105,15110,15111,15116,15118,15124],{"type":32,"value":15104},"Hope you’ve enjoyed this. You can share or retweet ths blogpost if you feel like it might help someone. If you have questions, you can find me on ",{"type":27,"tag":172,"props":15106,"children":15108},{"href":5770,"rel":15107},[696],[15109],{"type":32,"value":1589},{"type":32,"value":3372},{"type":27,"tag":172,"props":15112,"children":15114},{"href":10953,"rel":15113},[696],[15115],{"type":32,"value":1598},{"type":32,"value":15117}," or join the ",{"type":27,"tag":172,"props":15119,"children":15121},{"href":8322,"rel":15120},[696],[15122],{"type":32,"value":15123},"Discord server",{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":15126},[15127,15129,15130,15131,15132,15133],{"id":14642,"depth":320,"text":15128},"Using baseUrl",{"id":14744,"depth":320,"text":14747},{"id":14769,"depth":320,"text":14772},{"id":14824,"depth":320,"text":14827},{"id":14851,"depth":320,"text":14854},{"id":14965,"depth":320,"text":14968},"content:switch-between-environments-in-cypress:index.md","switch-between-environments-in-cypress/index.md","switch-between-environments-in-cypress/index",{"_path":15138,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":15139,"description":15140,"date":15141,"published":10,"slug":15142,"tags":15143,"cypressVersion":5959,"readingTime":15145,"body":15149,"_type":329,"_id":15559,"_source":331,"_file":15560,"_stem":15561,"_extension":334},"/cypress-basics-api-testing","Cypress basics: API testing","Cypress is a great testing tool that can be also very helpful when testing API. In this post, I’ll go over some basics on how to write an API test in Cypress.","2022-01-17","cypress-basics-api-testing",[5279,11456,15144],"request",{"text":927,"minutes":15146,"time":15147,"words":15148},4.315,258900,863,{"type":24,"children":15150,"toc":15552},[15151,15156,15166,15172,15184,15193,15204,15216,15225,15250,15260,15269,15282,15288,15301,15310,15315,15324,15330,15342,15351,15371,15380,15392,15412,15418,15429,15438,15451,15460,15465,15474,15480,15485,15513,15522,15527,15535],{"type":27,"tag":28,"props":15152,"children":15153},{},[15154],{"type":32,"value":15155},"If you have ever tested API via Postman or some other tool, this one will be a piece of cake for you. Cypress is a great testing tool that can be also very helpful when testing API. In today’s post, I’ll go over some basics on how to write an API test in Cypress.",{"type":27,"tag":1029,"props":15157,"children":15158},{},[15159,15163],{"type":27,"tag":28,"props":15160,"children":15161},{},[15162],{"type":32,"value":5973},{"type":27,"tag":5975,"props":15164,"children":15165},{},[],{"type":27,"tag":45,"props":15167,"children":15169},{"id":15168},"request-command",[15170],{"type":32,"value":15171},".request() command",{"type":27,"tag":28,"props":15173,"children":15174},{},[15175,15177,15182],{"type":32,"value":15176},"This command will be the center of it all. To send a simple request with a ",{"type":27,"tag":653,"props":15178,"children":15180},{"className":15179},[],[15181],{"type":32,"value":7797},{"type":32,"value":15183}," method, you can call it like this:",{"type":27,"tag":793,"props":15185,"children":15188},{"className":15186,"code":15187,"language":1513,"meta":5},[1510],"cy.request('/api/boards')\n",[15189],{"type":27,"tag":653,"props":15190,"children":15191},{"__ignoreMap":5},[15192],{"type":32,"value":15187},{"type":27,"tag":28,"props":15194,"children":15195},{},[15196,15198,15203],{"type":32,"value":15197},"Notice you don’t really need to add the method. Cypress optimizes their commands for maximum readability, so if you write a request like this, it will automatically be one with a method of ",{"type":27,"tag":653,"props":15199,"children":15201},{"className":15200},[],[15202],{"type":32,"value":7797},{"type":32,"value":256},{"type":27,"tag":28,"props":15205,"children":15206},{},[15207,15209,15214],{"type":32,"value":15208},"If you pass two arguments into ",{"type":27,"tag":653,"props":15210,"children":15212},{"className":15211},[],[15213],{"type":32,"value":12824},{"type":32,"value":15215}," command, the first argument will be considered a method, and the second one will be a url.",{"type":27,"tag":793,"props":15217,"children":15220},{"className":15218,"code":15219,"language":1513,"meta":5},[1510],"cy.request('DELETE', '/api/boards/9873789121')\n",[15221],{"type":27,"tag":653,"props":15222,"children":15223},{"__ignoreMap":5},[15224],{"type":32,"value":15219},{"type":27,"tag":28,"props":15226,"children":15227},{},[15228,15230,15236,15238,15243,15245],{"type":32,"value":15229},"Also, I haven't specified a full url. That is because the ",{"type":27,"tag":653,"props":15231,"children":15233},{"className":15232},[],[15234],{"type":32,"value":15235},"/api/boards",{"type":32,"value":15237}," will be automatically appended to anything that is defined as ",{"type":27,"tag":653,"props":15239,"children":15241},{"className":15240},[],[15242],{"type":32,"value":5995},{"type":32,"value":15244}," in ",{"type":27,"tag":653,"props":15246,"children":15248},{"className":15247},[],[15249],{"type":32,"value":6028},{"type":27,"tag":28,"props":15251,"children":15252},{},[15253,15258],{"type":27,"tag":653,"props":15254,"children":15256},{"className":15255},[],[15257],{"type":32,"value":12824},{"type":32,"value":15259}," command can take maximum of 3 arguments. The third one will be a request body.",{"type":27,"tag":793,"props":15261,"children":15264},{"className":15262,"code":15263,"language":1513,"meta":5},[1510],"cy.request('POST', '/api/boards', {\n  name: 'space travel plan'\n})\n",[15265],{"type":27,"tag":653,"props":15266,"children":15267},{"__ignoreMap":5},[15268],{"type":32,"value":15263},{"type":27,"tag":28,"props":15270,"children":15271},{},[15272,15274,15281],{"type":32,"value":15273},"This simple syntax is super useful, when you want to send a bunch of requests to your database to quickly setup your data for your UI test. My friend Furbo has written a ",{"type":27,"tag":172,"props":15275,"children":15278},{"href":15276,"rel":15277},"https://code.kiwi.com/articles/skip-the-ui-using-api-calls/",[696],[15279],{"type":32,"value":15280},"great blogpost about this",{"type":32,"value":256},{"type":27,"tag":45,"props":15283,"children":15285},{"id":15284},"passing-multiple-attributes-to-request-command",[15286],{"type":32,"value":15287},"Passing multiple attributes to .request() command",{"type":27,"tag":28,"props":15289,"children":15290},{},[15291,15293,15299],{"type":32,"value":15292},"If you want to pass some more options or just provide your ",{"type":27,"tag":653,"props":15294,"children":15296},{"className":15295},[],[15297],{"type":32,"value":15298},".request",{"type":32,"value":15300}," command a little more context, you can pass a single object. The same request from previous example can be written like this:",{"type":27,"tag":793,"props":15302,"children":15305},{"className":15303,"code":15304,"language":1513,"meta":5},[1510],"cy.request({\n  method: 'POST', \n  url: '/api/boards', \n  body: {\n    name: 'space travel plan'\n  }\n})\n",[15306],{"type":27,"tag":653,"props":15307,"children":15308},{"__ignoreMap":5},[15309],{"type":32,"value":15304},{"type":27,"tag":28,"props":15311,"children":15312},{},[15313],{"type":32,"value":15314},"This also gives you the ability to pass more options, for example headers or query parameters:",{"type":27,"tag":793,"props":15316,"children":15319},{"className":15317,"code":15318,"language":1513,"meta":5},[1510],"cy.request({\n  method: 'GET', \n  url: '/api/boards', \n  qs: {\n    starred: 'true'\n  },\n  headers: {\n    accept: 'application/json'\n  }\n})\n",[15320],{"type":27,"tag":653,"props":15321,"children":15322},{"__ignoreMap":5},[15323],{"type":32,"value":15318},{"type":27,"tag":45,"props":15325,"children":15327},{"id":15326},"getting-data-from-request",[15328],{"type":32,"value":15329},"Getting data from request",{"type":27,"tag":28,"props":15331,"children":15332},{},[15333,15335,15340],{"type":32,"value":15334},"After a request receives a response from server, you can access the information using ",{"type":27,"tag":653,"props":15336,"children":15338},{"className":15337},[],[15339],{"type":32,"value":13338},{"type":32,"value":15341}," command. This will return all kinds of attributes like response body, status code, duration etc.",{"type":27,"tag":793,"props":15343,"children":15346},{"className":15344,"code":15345,"language":1513,"meta":5},[1510],"cy.request({\n  method: 'POST', \n  url: '/api/boards', \n  body: {\n    name: 'space travel plan'\n  }\n}).then( (board) => {\n\n  console.log(board.status) // 201\n  console.log(board.duration) // 11\n  console.log(board.body) \n/* \n  { \n    \"name\": \"new board\",\n    \"id\": 39871447524,\n    \"starred\": false,\n    \"created\": \"2022-01-17\"\n  }\n*/\n})\n",[15347],{"type":27,"tag":653,"props":15348,"children":15349},{"__ignoreMap":5},[15350],{"type":32,"value":15345},{"type":27,"tag":28,"props":15352,"children":15353},{},[15354,15356,15362,15364,15369],{"type":32,"value":15355},"The alias ",{"type":27,"tag":653,"props":15357,"children":15359},{"className":15358},[],[15360],{"type":32,"value":15361},"board",{"type":32,"value":15363}," used as a parameter in our ",{"type":27,"tag":653,"props":15365,"children":15367},{"className":15366},[],[15368],{"type":32,"value":13338},{"type":32,"value":15370}," function can actually be skipped, if you use destructuring.",{"type":27,"tag":793,"props":15372,"children":15375},{"className":15373,"code":15374,"language":1513,"meta":5},[1510],"cy.request({\n  method: 'POST', \n  url: '/api/boards', \n  body: {\n    name: 'space travel plan'\n  }\n}).then( ({ status }) => {\n  console.log(status) // 201\n})\n",[15376],{"type":27,"tag":653,"props":15377,"children":15378},{"__ignoreMap":5},[15379],{"type":32,"value":15374},{"type":27,"tag":28,"props":15381,"children":15382},{},[15383,15385,15391],{"type":32,"value":15384},"This way you don’t have to create a named alias everytime you want to get some data from the request. If you want to learn more about destructuring, you can read ",{"type":27,"tag":172,"props":15386,"children":15388},{"href":15387},"/using-destructuring-in-cypress",[15389],{"type":32,"value":15390},"of my older posts on this topic",{"type":32,"value":256},{"type":27,"tag":28,"props":15393,"children":15394},{},[15395,15397,15403,15405,15411],{"type":32,"value":15396},"If you want to use data from the response elsewhere in the test, you can check out ",{"type":27,"tag":172,"props":15398,"children":15400},{"href":15399},"/working-with-api-response-data-in-cypress",[15401],{"type":32,"value":15402},"this post on working with API data",{"type":32,"value":15404},", or ",{"type":27,"tag":172,"props":15406,"children":15408},{"href":15407},"/cypress-basics-variables",[15409],{"type":32,"value":15410},"this one, on using variables in Cypress",{"type":32,"value":256},{"type":27,"tag":45,"props":15413,"children":15415},{"id":15414},"testing-response-data",[15416],{"type":32,"value":15417},"Testing response data",{"type":27,"tag":28,"props":15419,"children":15420},{},[15421,15423,15428],{"type":32,"value":15422},"Now that we have gotten data from our server, we can proceed with testing them. Cypress has bundled chai library, which you can use inside your ",{"type":27,"tag":653,"props":15424,"children":15426},{"className":15425},[],[15427],{"type":32,"value":13338},{"type":32,"value":14476},{"type":27,"tag":793,"props":15430,"children":15433},{"className":15431,"code":15432,"language":1513,"meta":5},[1510],"cy.request({\n  method: 'POST', \n  url: '/api/boards', \n  body: {\n    name: 'space travel plan'\n  }\n}).then( ({ status }) => {\n  expect(status).to.eq(201)\n})\n",[15434],{"type":27,"tag":653,"props":15435,"children":15436},{"__ignoreMap":5},[15437],{"type":32,"value":15432},{"type":27,"tag":28,"props":15439,"children":15440},{},[15441,15443,15449],{"type":32,"value":15442},"Response body is usually stored in JSON format, which means that if you want to find particular item in the response and test it, you need to find a proper path. ",{"type":27,"tag":172,"props":15444,"children":15446},{"href":15445},"/reading-and-testing-json-object-in-cypress",[15447],{"type":32,"value":15448},"I dive more deeply into this topic in one of my older blogs",{"type":32,"value":15450},", but a simple example would look something like this:",{"type":27,"tag":793,"props":15452,"children":15455},{"className":15453,"code":15454,"language":1513,"meta":5},[1510],"cy.request({\n  method: 'GET', \n  url: '/api/boards', \n}).then( ({ body }) => {\n  \n  expect(body).to.have.length(2) // check number of items \n  expect(body[0].name).to.eq('space travel plan') // check first item in array\n\n})\n",[15456],{"type":27,"tag":653,"props":15457,"children":15458},{"__ignoreMap":5},[15459],{"type":32,"value":15454},{"type":27,"tag":28,"props":15461,"children":15462},{},[15463],{"type":32,"value":15464},"You can test various attributes of the API response as the bundled chai library is pretty versatile. For example, you can check whether returned content has the proper type, contains certain items or you can write your own function to check a value.",{"type":27,"tag":793,"props":15466,"children":15469},{"className":15467,"code":15468,"language":1513,"meta":5},[1510],"cy.request({\n  method: 'GET', \n  url: '/api/boards', \n}).then( ({ body }) => {\n  \n  expect(body.length).to.be.greaterThan(1) // more than 1 item is in list\n  expect(body[0].name).to.be.a('string') // the text 'space travel plan' is a string\n  expect(body[0].id).to.satisfy((num) => { return num > 0 }) // id must be bigger than 0\n\n})\n",[15470],{"type":27,"tag":653,"props":15471,"children":15472},{"__ignoreMap":5},[15473],{"type":32,"value":15468},{"type":27,"tag":45,"props":15475,"children":15477},{"id":15476},"using-cypress-plugin-api-plugin",[15478],{"type":32,"value":15479},"Using cypress-plugin-api plugin",{"type":27,"tag":28,"props":15481,"children":15482},{},[15483],{"type":32,"value":15484},"Cypress will open browser each time you run a test, which is something to have in mind once you decide to use Cypress for API testing. Also, you need to open browser console to look into the details of Cypress response.",{"type":27,"tag":28,"props":15486,"children":15487},{},[15488,15490,15497,15499,15505,15507,15512],{"type":32,"value":15489},"But with ",{"type":27,"tag":172,"props":15491,"children":15494},{"href":15492,"rel":15493},"https://github.com/filiphric/cypress-plugin-api",[696],[15495],{"type":32,"value":15496},"cypress-plugin-api plugin",{"type":32,"value":15498},", the request, as well as response get rendered into browser window, so you can easily observe your API even in GUI mode. This plugin will add ",{"type":27,"tag":653,"props":15500,"children":15502},{"className":15501},[],[15503],{"type":32,"value":15504},".api()",{"type":32,"value":15506}," command to your Cypress library, and the syntax is very similar to ",{"type":27,"tag":653,"props":15508,"children":15510},{"className":15509},[],[15511],{"type":32,"value":12824},{"type":32,"value":14476},{"type":27,"tag":793,"props":15514,"children":15517},{"className":15515,"code":15516,"language":1513,"meta":5},[1510],"cy\n  .api({\n    method: 'POST', \n    url: '/api/boards', \n    body: { name: 'new board' }\n  })\n",[15518],{"type":27,"tag":653,"props":15519,"children":15520},{"__ignoreMap":5},[15521],{"type":32,"value":15516},{"type":27,"tag":28,"props":15523,"children":15524},{},[15525],{"type":32,"value":15526},"This test will then produce this nice render in your test:",{"type":27,"tag":28,"props":15528,"children":15529},{},[15530],{"type":27,"tag":959,"props":15531,"children":15534},{"alt":15532,"src":15533},"cy.api command in action","cypress-plugin-api.png",[],{"type":27,"tag":28,"props":15536,"children":15537},{},[15538,15540,15545,15546,15551],{"type":32,"value":15539},"Hope you liked this. You can help me spread the word and share this post with your friends if you feel like I deserved it. Make sure to follow me on ",{"type":27,"tag":172,"props":15541,"children":15543},{"href":5770,"rel":15542},[696],[15544],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":15547,"children":15549},{"href":10953,"rel":15548},[696],[15550],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":15553},[15554,15555,15556,15557,15558],{"id":15168,"depth":320,"text":15171},{"id":15284,"depth":320,"text":15287},{"id":15326,"depth":320,"text":15329},{"id":15414,"depth":320,"text":15417},{"id":15476,"depth":320,"text":15479},"content:cypress-basics-api-testing:index.md","cypress-basics-api-testing/index.md","cypress-basics-api-testing/index",{"_path":8133,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":15563,"description":15564,"date":15565,"published":10,"slug":15566,"tags":15567,"cypressVersion":10372,"readingTime":15570,"body":15574,"_type":329,"_id":16007,"_source":331,"_file":16008,"_stem":16009,"_extension":334},"Waiting in Cypress and how to avoid it","Adding a wait to your test is something people like to avoid. Luckily, with Cypress, there are several ways of how to avoid waiting for a static period of time and simply move a test forward once the application is in a state we desire.","2022-01-10","waiting-in-cypress-and-how-to-avoid-it",[5279,15568,15569,3225],"waiting","cy.wait",{"text":4032,"minutes":15571,"time":15572,"words":15573},5.835,350100,1167,{"type":24,"children":15575,"toc":15999},[15576,15596,15608,15620,15626,15638,15647,15673,15679,15704,15713,15718,15723,15732,15745,15754,15760,15765,15774,15808,15813,15822,15856,15862,15875,15884,15890,15902,15911,15916,15921,15930,15942,15948,15960,15969,15974,15983],{"type":27,"tag":28,"props":15577,"children":15578},{},[15579,15581,15587,15589,15594],{"type":32,"value":15580},"There are many perfectionists among testers. Almost everyone I have met has this itch when they use the ",{"type":27,"tag":653,"props":15582,"children":15584},{"className":15583},[],[15585],{"type":32,"value":15586},".wait()",{"type":32,"value":15588}," command in Cypress and halt the test for a couple of seconds. If this applies to you as well, then you know well that using ",{"type":27,"tag":653,"props":15590,"children":15592},{"className":15591},[],[15593],{"type":32,"value":15586},{"type":32,"value":15595}," like this is not exactly the best solution and try to look for an alternative. The test simply does nothing for a couple of seconds. Those couple of seconds may be enough. But sometimes, the wait is not long enough.",{"type":27,"tag":28,"props":15597,"children":15598},{},[15599,15601,15606],{"type":32,"value":15600},"Whenever we use ",{"type":27,"tag":653,"props":15602,"children":15604},{"className":15603},[],[15605],{"type":32,"value":15586},{"type":32,"value":15607},", we want our application to reach the desired state. Modal closes, network response comes back in, button changes state, etc. Cypress was built with retrybility in mind - which means that as soon as a command passes, it will move on to the next one. If it’s not passing, Cypress will keep retrying for a couple of seconds.",{"type":27,"tag":28,"props":15609,"children":15610},{},[15611,15613,15618],{"type":32,"value":15612},"This architecture often causes that Cypress often moves ",{"type":27,"tag":302,"props":15614,"children":15615},{},[15616],{"type":32,"value":15617},"too fast",{"type":32,"value":15619}," through our application, and we want to make it wait. Reaching for a hard wait is often a way to tell Cypress to slow down. But it’s not ideal, as I already mentioned. So let’s look at a couple of things you can do when you face the dreaded solution.",{"type":27,"tag":45,"props":15621,"children":15623},{"id":15622},"just-use-the-wait-and-be-done-with-it",[15624],{"type":32,"value":15625},"Just use the wait and be done with it",{"type":27,"tag":28,"props":15627,"children":15628},{},[15629,15631,15636],{"type":32,"value":15630},"I know, I know. The heading of this article promises a guide on how to avoid this, but hear me out. Sometimes, the best solution for you and the rest of the team is just using the hard wait. Don’t spend two days finding the right combination of guards, assertions, intercepts and whatnot to avoid using the ",{"type":27,"tag":653,"props":15632,"children":15634},{"className":15633},[],[15635],{"type":32,"value":15586},{"type":32,"value":15637}," command. Those two days are probably exceeding the total waiting time that the test would create. More importantly, your time is much more valuable than the one on CI/CD pipeline. You could be working on something more useful. Perfectionism is expensive. Just add the wait, move on, and come back later. It’s also a good practice to leave a \"to do\" comment so that anyone that encounters this will get an understanding of why is there a wait in this test.",{"type":27,"tag":793,"props":15639,"children":15642},{"className":15640,"code":15641,"language":1513,"meta":5},[1510],"  // TODO: ugh, have to use wait. button does not get interactive soon enough\n  cy.get('button')\n    .wait(2000) \n    .click()\n",[15643],{"type":27,"tag":653,"props":15644,"children":15645},{"__ignoreMap":5},[15646],{"type":32,"value":15641},{"type":27,"tag":28,"props":15648,"children":15649},{},[15650,15655,15657,15664,15666,15671],{"type":27,"tag":79,"props":15651,"children":15652},{},[15653],{"type":32,"value":15654},"PRO TIP:",{"type":32,"value":15656}," you can use ",{"type":27,"tag":172,"props":15658,"children":15661},{"href":15659,"rel":15660},"https://www.npmjs.com/package/eslint-plugin-cypress",[696],[15662],{"type":32,"value":15663},"eslint-plugin-cypress",{"type":32,"value":15665}," to get lint warning every time you use ",{"type":27,"tag":653,"props":15667,"children":15669},{"className":15668},[],[15670],{"type":32,"value":15586},{"type":32,"value":15672}," in your test.",{"type":27,"tag":45,"props":15674,"children":15676},{"id":15675},"use-defaultcommandtimeout-to-change-default-timeout",[15677],{"type":32,"value":15678},"Use \"defaultCommandTimeout\" to change default timeout",{"type":27,"tag":28,"props":15680,"children":15681},{},[15682,15684,15689,15690,15695,15697,15702],{"type":32,"value":15683},"Every element you query for an element using ",{"type":27,"tag":653,"props":15685,"children":15687},{"className":15686},[],[15688],{"type":32,"value":12748},{"type":32,"value":7660},{"type":27,"tag":653,"props":15691,"children":15693},{"className":15692},[],[15694],{"type":32,"value":13507},{"type":32,"value":15696}," or some other command, it will have a default wait time of 4 seconds. Cypress will wait for the element to appear in DOM and will retry while it can. If 4 seconds are not enough, you can set the time up globally for your project in the ",{"type":27,"tag":653,"props":15698,"children":15700},{"className":15699},[],[15701],{"type":32,"value":6028},{"type":32,"value":15703}," file to make Cypress wait longer:",{"type":27,"tag":793,"props":15705,"children":15708},{"className":15706,"code":15707,"filename":6028,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    defaultCommandTimeout: 5000\n  }\n})\n",[15709],{"type":27,"tag":653,"props":15710,"children":15711},{"__ignoreMap":5},[15712],{"type":32,"value":15707},{"type":27,"tag":28,"props":15714,"children":15715},{},[15716],{"type":32,"value":15717},"Setting this timeout has one important side effect. Your tests will fail slower. This may prolong the feedback loop for you, so you might want to reach for a less harsh solution.",{"type":27,"tag":28,"props":15719,"children":15720},{},[15721],{"type":32,"value":15722},"Let’s say you have a single test where some elements load slightly slower. Instead of applying the longer timeout globally, you can just apply this configuration in a single test.",{"type":27,"tag":793,"props":15724,"children":15727},{"className":15725,"code":15726,"language":1513,"meta":5},[1510],"it('slow test', { defaultCommandTimeout: 5000 },  () => {\n\n  // will wait 5 seconds for element to appear in dom\n  cy.get('slowElement')\n\n})\n",[15728],{"type":27,"tag":653,"props":15729,"children":15730},{"__ignoreMap":5},[15731],{"type":32,"value":15726},{"type":27,"tag":28,"props":15733,"children":15734},{},[15735,15737,15743],{"type":32,"value":15736},"This configuration object works for ",{"type":27,"tag":653,"props":15738,"children":15740},{"className":15739},[],[15741],{"type":32,"value":15742},"describe",{"type":32,"value":15744}," blocks as well:",{"type":27,"tag":793,"props":15746,"children":15749},{"className":15747,"code":15748,"language":1513,"meta":5},[1510],"describe('slow tests', { defaultCommandTimeout: 5000 },  () => {\n\n  it('slow test #1', () => {\n    // will apply 5 timeout\n  })\n\n  it('slow test #2', () => {\n    // here too\n  })\n\n})\n\nit('not so slow test', () => {\n  // but not here\n})\n",[15750],{"type":27,"tag":653,"props":15751,"children":15752},{"__ignoreMap":5},[15753],{"type":32,"value":15748},{"type":27,"tag":45,"props":15755,"children":15757},{"id":15756},"use-timeout-per-command",[15758],{"type":32,"value":15759},"Use timeout per command",{"type":27,"tag":28,"props":15761,"children":15762},{},[15763],{"type":32,"value":15764},"Prolonging the timeout for the whole test might not always be the best way. Sometimes, you simply want to wait until a certain element appears, but everything else on the page is pretty fast. For these cases, you can use the options object and change timeout for a certain command.",{"type":27,"tag":793,"props":15766,"children":15769},{"className":15767,"code":15768,"language":1513,"meta":5},[1510],"cy.get('#myElement', { timeout: 10000 })\n  .should('be.visible')\n",[15770],{"type":27,"tag":653,"props":15771,"children":15772},{"__ignoreMap":5},[15773],{"type":32,"value":15768},{"type":27,"tag":28,"props":15775,"children":15776},{},[15777,15779,15784,15786,15791,15793,15798,15800,15807],{"type":32,"value":15778},"Notice how we are adding the ",{"type":27,"tag":653,"props":15780,"children":15782},{"className":15781},[],[15783],{"type":32,"value":3225},{"type":32,"value":15785}," into our ",{"type":27,"tag":653,"props":15787,"children":15789},{"className":15788},[],[15790],{"type":32,"value":12748},{"type":32,"value":15792}," command, not the ",{"type":27,"tag":653,"props":15794,"children":15796},{"className":15795},[],[15797],{"type":32,"value":12439},{"type":32,"value":15799},". The intuitive approach might be to wait for the element to pass our assertion. But our assertion is tied to the querying of the element. That’s why if an assertion is not fulfilled, it ",{"type":27,"tag":172,"props":15801,"children":15804},{"href":15802,"rel":15803},"https://docs.cypress.io/guides/core-concepts/retry-ability#Only-the-last-command-is-retried",[696],[15805],{"type":32,"value":15806},"will make the whole query as well",{"type":32,"value":256},{"type":27,"tag":28,"props":15809,"children":15810},{},[15811],{"type":32,"value":15812},"This can also be useful if you want to wait for the element to disappear or be removed from the DOM before you move on to the next step of your test.",{"type":27,"tag":793,"props":15814,"children":15817},{"className":15815,"code":15816,"language":1513,"meta":5},[1510],"cy.get('#modal', { timeout: 10000 })\n  .should('not.exist')\n\n// continue with the test\n",[15818],{"type":27,"tag":653,"props":15819,"children":15820},{"__ignoreMap":5},[15821],{"type":32,"value":15816},{"type":27,"tag":28,"props":15823,"children":15824},{},[15825,15827,15833,15834,15840,15842,15847,15849,15854],{"type":32,"value":15826},"Side note: Be mindful of the difference between ",{"type":27,"tag":653,"props":15828,"children":15830},{"className":15829},[],[15831],{"type":32,"value":15832},"not.exist",{"type":32,"value":4164},{"type":27,"tag":653,"props":15835,"children":15837},{"className":15836},[],[15838],{"type":32,"value":15839},"not.be.visible",{"type":32,"value":15841},". I sometimes see people confuse these two and a for good reason. Intuitively, they feel like the same thing. But while ",{"type":27,"tag":653,"props":15843,"children":15845},{"className":15844},[],[15846],{"type":32,"value":15832},{"type":32,"value":15848}," will check for absence of the element in DOM, ",{"type":27,"tag":653,"props":15850,"children":15852},{"className":15851},[],[15853],{"type":32,"value":15839},{"type":32,"value":15855}," will only pass if the element is present in DOM, but it is not visible.",{"type":27,"tag":45,"props":15857,"children":15859},{"id":15858},"wait-for-page-load",[15860],{"type":32,"value":15861},"Wait for page load",{"type":27,"tag":28,"props":15863,"children":15864},{},[15865,15867,15873],{"type":32,"value":15866},"I’ve talked about ",{"type":27,"tag":172,"props":15868,"children":15870},{"href":15869},"/testing-links-with-cypress",[15871],{"type":32,"value":15872},"checking links",{"type":32,"value":15874}," in the past and why clicking individual links might not be the best solution. But if a page redirect is part of your test flow, you might want to wait a second for the test to continue. Instead of using the wait command, you can use the same principle as in the previous example.",{"type":27,"tag":793,"props":15876,"children":15879},{"className":15877,"code":15878,"language":1513,"meta":5},[1510],"cy.get('#redirectLink')\n  .click()\n\ncy.location('pathname', { timeout: 10000 })\n  .should('eq', '/about')\n",[15880],{"type":27,"tag":653,"props":15881,"children":15882},{"__ignoreMap":5},[15883],{"type":32,"value":15878},{"type":27,"tag":45,"props":15885,"children":15887},{"id":15886},"wait-for-api-response",[15888],{"type":32,"value":15889},"Wait for API response",{"type":27,"tag":28,"props":15891,"children":15892},{},[15893,15895,15900],{"type":32,"value":15894},"Cypress works great with http requests. If you are waiting for some resources to be loaded in your app, you can intercept a request and then create an alias for it. That alias will then be used with ",{"type":27,"tag":653,"props":15896,"children":15898},{"className":15897},[],[15899],{"type":32,"value":15586},{"type":32,"value":15901}," command. Test will only continue once that command is finished.",{"type":27,"tag":793,"props":15903,"children":15906},{"className":15904,"code":15905,"language":1513,"meta":5},[1510],"cy.intercept('/api/boards').as('boardList')\ncy.visit('/')\ncy.wait('@boardList')\n// continue with test after response happens\n",[15907],{"type":27,"tag":653,"props":15908,"children":15909},{"__ignoreMap":5},[15910],{"type":32,"value":15905},{"type":27,"tag":28,"props":15912,"children":15913},{},[15914],{"type":32,"value":15915},"Finding the right request to intercept is a great way to make sure that Cypress will wait until page loads with all the right data loaded.",{"type":27,"tag":28,"props":15917,"children":15918},{},[15919],{"type":32,"value":15920},"If you need to wait for multiple requests, you can set up a multiple alias wait in a single command:",{"type":27,"tag":793,"props":15922,"children":15925},{"className":15923,"code":15924,"language":1513,"meta":5},[1510],"cy.intercept('/api/boards').as('boardList')\ncy.intercept('/api/cards').as('cardList')\ncy.visit('/')\ncy.wait(['@boardList', '@cardList'])\n// continue with test after all responses happen\n",[15926],{"type":27,"tag":653,"props":15927,"children":15928},{"__ignoreMap":5},[15929],{"type":32,"value":15924},{"type":27,"tag":28,"props":15931,"children":15932},{},[15933,15935,15940],{"type":32,"value":15934},"One important notice here - if you want to change the default timeout for api responses, you need to work with ",{"type":27,"tag":653,"props":15936,"children":15938},{"className":15937},[],[15939],{"type":32,"value":8026},{"type":32,"value":15941}," config option.",{"type":27,"tag":45,"props":15943,"children":15945},{"id":15944},"wait-for-anything",[15946],{"type":32,"value":15947},"Wait for anything",{"type":27,"tag":28,"props":15949,"children":15950},{},[15951,15953,15958],{"type":32,"value":15952},"You can wait for basically anything by passing a callback function into ",{"type":27,"tag":653,"props":15954,"children":15956},{"className":15955},[],[15957],{"type":32,"value":12439},{"type":32,"value":15959}," command. It will use the built in retry logic and wait for the function to pass. For example, you can wait until all of the elements on page have the proper text. This example shows how we can wait for a list to be reordered instead of waiting for a second.",{"type":27,"tag":793,"props":15961,"children":15964},{"className":15962,"code":15963,"language":1513,"meta":5},[1510],"cy.contains('button', 'Sort alphabetically')\n  .click()\n\n// list is reordering, it will take a while\n\ncy.get('.list')\n  .should( (items) => {\n\n    // but no worries, we will retry until these pass or until timeout\n    expect(items).to.have.length(2)\n    expect(items[0]).to.have.text('Apples')\n    expect(items[1]).to.have.text('Bananas')\n\n  })\n\n// all is good, continue with our test\n",[15965],{"type":27,"tag":653,"props":15966,"children":15967},{"__ignoreMap":5},[15968],{"type":32,"value":15963},{"type":27,"tag":28,"props":15970,"children":15971},{},[15972],{"type":32,"value":15973},"These can be applied for anything, for example here we check if input has a proper value and a class:",{"type":27,"tag":793,"props":15975,"children":15978},{"className":15976,"code":15977,"language":1513,"meta":5},[1510],"cy.get('input')\n  .should( (email) => {\n\n    expect(email).to.have.value('hello@example.com')\n    expect(email).to.have.class('valid')\n\n  })\n",[15979],{"type":27,"tag":653,"props":15980,"children":15981},{"__ignoreMap":5},[15982],{"type":32,"value":15977},{"type":27,"tag":28,"props":15984,"children":15985},{},[15986,15987,15992,15993,15998],{"type":32,"value":15539},{"type":27,"tag":172,"props":15988,"children":15990},{"href":5770,"rel":15989},[696],[15991],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":15994,"children":15996},{"href":10953,"rel":15995},[696],[15997],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":16000},[16001,16002,16003,16004,16005,16006],{"id":15622,"depth":320,"text":15625},{"id":15675,"depth":320,"text":15678},{"id":15756,"depth":320,"text":15759},{"id":15858,"depth":320,"text":15861},{"id":15886,"depth":320,"text":15889},{"id":15944,"depth":320,"text":15947},"content:waiting-in-cypress-and-how-to-avoid-it:index.md","waiting-in-cypress-and-how-to-avoid-it/index.md","waiting-in-cypress-and-how-to-avoid-it/index",{"_path":15407,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":16011,"description":16012,"date":16013,"published":10,"slug":16014,"tags":16015,"readingTime":16017,"body":16021,"_type":329,"_id":16507,"_source":331,"_file":16508,"_stem":16509,"_extension":334},"Cypress basics: Variables","How to store variables in Cypress tests and use variables and aliases inside the test and between tests.","2021-12-07","cypress-basics-variables",[5279,16016,13407],"variables",{"text":1933,"minutes":16018,"time":16019,"words":16020},6.105,366300,1221,{"type":24,"children":16022,"toc":16497},[16023,16033,16038,16047,16059,16107,16119,16129,16134,16143,16155,16161,16171,16180,16185,16201,16206,16216,16235,16255,16260,16269,16274,16279,16285,16311,16320,16326,16366,16375,16381,16399,16408,16414,16419,16428,16434,16478,16487],{"type":27,"tag":1029,"props":16024,"children":16025},{},[16026,16030],{"type":27,"tag":28,"props":16027,"children":16028},{},[16029],{"type":32,"value":5973},{"type":27,"tag":5975,"props":16031,"children":16032},{},[],{"type":27,"tag":28,"props":16034,"children":16035},{},[16036],{"type":32,"value":16037},"If you came here via Google search, you are probably wondering why code like this does not work in Cypress:",{"type":27,"tag":793,"props":16039,"children":16042},{"className":16040,"code":16041,"language":1513,"meta":5},[1510],"it('stores value in variable', () => {\n\n  let id\n\n  cy.request('/api/boards')\n    .then( res => {\n\n      id = res.body[0].id\n    })\n\n  cy.visit('/board/' + id) // \"id\" is undefined?!\n\n})\n",[16043],{"type":27,"tag":653,"props":16044,"children":16045},{"__ignoreMap":5},[16046],{"type":32,"value":16041},{"type":27,"tag":28,"props":16048,"children":16049},{},[16050,16052,16057],{"type":32,"value":16051},"If you are here just for the solution, scroll down to the section named ",{"type":27,"tag":79,"props":16053,"children":16054},{},[16055],{"type":32,"value":16056},"Possible solutions",{"type":32,"value":16058}," If you want to understand what is going on, read on.",{"type":27,"tag":28,"props":16060,"children":16061},{},[16062,16064,16070,16072,16079,16081,16088,16090,16096,16098,16105],{"type":32,"value":16063},"So why is ",{"type":27,"tag":653,"props":16065,"children":16067},{"className":16066},[],[16068],{"type":32,"value":16069},"id",{"type":32,"value":16071}," undefined? When you dig into docs, it might get a little confusing. There’s article about ",{"type":27,"tag":172,"props":16073,"children":16076},{"href":16074,"rel":16075},"https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Commands-Are-Asynchronous",[696],[16077],{"type":32,"value":16078},"how commands in Cypress are asynchronous",{"type":32,"value":16080},", then maybe read a little bit about ",{"type":27,"tag":172,"props":16082,"children":16085},{"href":16083,"rel":16084},"https://docs.cypress.io/guides/core-concepts/variables-and-aliases",[696],[16086],{"type":32,"value":16087},"how you should handle variables",{"type":32,"value":16089},", you’ll try ",{"type":27,"tag":653,"props":16091,"children":16093},{"className":16092},[],[16094],{"type":32,"value":16095},"async/await",{"type":32,"value":16097},", but then find out ",{"type":27,"tag":172,"props":16099,"children":16102},{"href":16100,"rel":16101},"https://docs.cypress.io/faq/questions/using-cypress-faq#Can-I-use-the-new-ES7-async-await-syntax",[696],[16103],{"type":32,"value":16104},"that does not work either",{"type":32,"value":16106},". So what is going on?",{"type":27,"tag":28,"props":16108,"children":16109},{},[16110,16112,16117],{"type":32,"value":16111},"Let’s add a couple of ",{"type":27,"tag":653,"props":16113,"children":16115},{"className":16114},[],[16116],{"type":32,"value":5487},{"type":32,"value":16118}," functions to our test and see how will the test behave. Just by looking at the code, can you guess what will be printed out in browser console?",{"type":27,"tag":793,"props":16120,"children":16124},{"className":16121,"code":16122,"highlights":16123,"language":1513,"meta":5},[1510],"it('stores value in variable', () => {\n  console.log('>>> first log')\n  let id\n\n  cy.request('/api/boards')\n    .then( res => {\n      console.log('>>> second log')\n\n      id = res.body[0].id\n    })\n  console.log('>>> third log')\n\n  cy.visit('/board/' + id)\n})\n",[320,3668,3811],[16125],{"type":27,"tag":653,"props":16126,"children":16127},{"__ignoreMap":5},[16128],{"type":32,"value":16122},{"type":27,"tag":28,"props":16130,"children":16131},{},[16132],{"type":32,"value":16133},"Maybe you guessed it right. But I guess you are curious why is this the answer:",{"type":27,"tag":793,"props":16135,"children":16138},{"className":16136,"code":16137,"language":2250,"meta":5},[2248],">>> first log\n>>> third log\n>>> second log\n",[16139],{"type":27,"tag":653,"props":16140,"children":16141},{"__ignoreMap":5},[16142],{"type":32,"value":16137},{"type":27,"tag":28,"props":16144,"children":16145},{},[16146,16148,16153],{"type":32,"value":16147},"As I said earlier, the answer ",{"type":27,"tag":302,"props":16149,"children":16150},{},[16151],{"type":32,"value":16152},"is",{"type":32,"value":16154}," in the docs, but it might be a little confusing. At least for me it was. So here’s another way to think about it.",{"type":27,"tag":45,"props":16156,"children":16158},{"id":16157},"cypress-chain-vs-everything-else",[16159],{"type":32,"value":16160},"Cypress chain vs. everything else",{"type":27,"tag":28,"props":16162,"children":16163},{},[16164,16169],{"type":27,"tag":79,"props":16165,"children":16166},{},[16167],{"type":32,"value":16168},"Cypress commands run in a chain",{"type":32,"value":16170},". Each chain link ties to the one before and is also tied to the one after. This way Cypress ensures that you don’t run into race conditions and will automatically wait for the previous command to finish. Let me give you an example.",{"type":27,"tag":793,"props":16172,"children":16175},{"className":16173,"code":16174,"language":1513,"meta":5},[1510],"cy\n  .get('li')\n  .should('have.length', 5) // wait until previous command finds elements\n  .last() // wait until previous assertion passes\n  .click() // wait until previous command finishes\n",[16176],{"type":27,"tag":653,"props":16177,"children":16178},{"__ignoreMap":5},[16179],{"type":32,"value":16174},{"type":27,"tag":28,"props":16181,"children":16182},{},[16183],{"type":32,"value":16184},"Again, no command will run until the one before is finished. If any of the commands don’t finish on time, (usually 4 seconds) test fails.",{"type":27,"tag":28,"props":16186,"children":16187},{},[16188,16193,16195,16200],{"type":27,"tag":79,"props":16189,"children":16190},{},[16191],{"type":32,"value":16192},"So what happens with the code that is outside the chain?",{"type":32,"value":16194}," Well, since it’s not part of the chain, there’s nothing that forces it to wait, and gets ",{"type":27,"tag":79,"props":16196,"children":16197},{},[16198],{"type":32,"value":16199},"executed immediately",{"type":32,"value":256},{"type":27,"tag":28,"props":16202,"children":16203},{},[16204],{"type":32,"value":16205},"Let’s now look at the example with a fresh perspective.",{"type":27,"tag":793,"props":16207,"children":16211},{"className":16208,"code":16209,"highlights":16210,"language":1513,"meta":5},[1510],"it('stores value in variable', () => {\n  // outside of chain, run immediately\n  console.log('>>> first log') \n  let id\n\n  cy.request('/api/boards')\n    .then( res => {\n      // inside the chain, wait for .request to finish\n      console.log('>>> second log') \n      id = res.body[0].id\n    })\n\n  // outside of chain, run immediately\n  console.log('>>> third log') \n\n  cy.visit('/board/' + id)\n})\n",[320,1606,3723,3746,3852,3853],[16212],{"type":27,"tag":653,"props":16213,"children":16214},{"__ignoreMap":5},[16215],{"type":32,"value":16209},{"type":27,"tag":28,"props":16217,"children":16218},{},[16219,16221,16226,16228,16233],{"type":32,"value":16220},"Hopefully, the ",{"type":27,"tag":653,"props":16222,"children":16224},{"className":16223},[],[16225],{"type":32,"value":5487},{"type":32,"value":16227}," functions make a little more sense now. But what about that ",{"type":27,"tag":653,"props":16229,"children":16231},{"className":16230},[],[16232],{"type":32,"value":16069},{"type":32,"value":16234}," variable? It seems like being used inside the chain. Or is it?",{"type":27,"tag":28,"props":16236,"children":16237},{},[16238,16240,16245,16247,16253],{"type":32,"value":16239},"Actually not. It is passed as an argument, so technically it is not inside the command chain, but passed \"from outside\". We declared this variable at the beginning of the test. Within our test, we are telling Cypress that we want to execute ",{"type":27,"tag":653,"props":16241,"children":16243},{"className":16242},[],[16244],{"type":32,"value":14057},{"type":32,"value":16246}," command with whatever ",{"type":27,"tag":653,"props":16248,"children":16250},{"className":16249},[],[16251],{"type":32,"value":16252},"'/board/' + id",{"type":32,"value":16254}," is.",{"type":27,"tag":28,"props":16256,"children":16257},{},[16258],{"type":32,"value":16259},"It starts to make a little more sense when we take a closer look into our \"inside chain vs. outside chain\" principle. Let’s take a look at the code again:",{"type":27,"tag":793,"props":16261,"children":16264},{"className":16262,"code":16263,"language":1513,"meta":5},[1510],"it('stores value in variable', () => {\n  // not waiting to declare the variable\n  let id\n\n  cy.request('/api/boards')\n    .then( res => {\n      // waiting for .request to happen in test, then assign new value\n      id = res.body[0].id\n    })\n\n  // not waiting to pass the variable\n  cy.visit('/board/' + id)\n})\n",[16265],{"type":27,"tag":653,"props":16266,"children":16267},{"__ignoreMap":5},[16268],{"type":32,"value":16263},{"type":27,"tag":28,"props":16270,"children":16271},{},[16272],{"type":32,"value":16273},"Now that the problem is clearer, let’s look at how we can pass values around in our test using different methods. There are many solutions to this, so let’s look at at least a few.",{"type":27,"tag":45,"props":16275,"children":16277},{"id":16276},"possible-solutions",[16278],{"type":32,"value":16056},{"type":27,"tag":1033,"props":16280,"children":16282},{"id":16281},"solution-1-move-the-desired-code-inside-command-chain",[16283],{"type":32,"value":16284},"Solution #1: Move the desired code inside command chain",{"type":27,"tag":28,"props":16286,"children":16287},{},[16288,16290,16295,16297,16302,16304,16309],{"type":32,"value":16289},"The easiest solution is to make sure that anything we include everything in our command chain. To use the new value, we need to call our ",{"type":27,"tag":653,"props":16291,"children":16293},{"className":16292},[],[16294],{"type":32,"value":14057},{"type":32,"value":16296}," function inside the command chain. That way, the ",{"type":27,"tag":653,"props":16298,"children":16300},{"className":16299},[],[16301],{"type":32,"value":16069},{"type":32,"value":16303}," will be passed with a new value. Of course, multiple ",{"type":27,"tag":653,"props":16305,"children":16307},{"className":16306},[],[16308],{"type":32,"value":13338},{"type":32,"value":16310}," funcitons can potentially cause a \"pyramid of doom\", so this solution is best for cases when you want to immediately pass a single variable.",{"type":27,"tag":793,"props":16312,"children":16315},{"className":16313,"code":16314,"language":1513,"meta":5},[1510],"it('stores value in variable', () => {\nlet id // create variable\n\ncy.request('/api/boards')\n  .then( res => {\n    id = res.body[0].id // assign value\n    cy.visit('/board/' + id) // pass the newly assigned value\n  })\n\n})\n",[16316],{"type":27,"tag":653,"props":16317,"children":16318},{"__ignoreMap":5},[16319],{"type":32,"value":16314},{"type":27,"tag":1033,"props":16321,"children":16323},{"id":16322},"solution-2-split-logic-into-multiple-tests",[16324],{"type":32,"value":16325},"Solution #2: Split logic into multiple tests",{"type":27,"tag":28,"props":16327,"children":16328},{},[16329,16331,16336,16338,16343,16345,16350,16352,16357,16359,16364],{"type":32,"value":16330},"Since Cypress runs ",{"type":27,"tag":653,"props":16332,"children":16334},{"className":16333},[],[16335],{"type":32,"value":7187},{"type":32,"value":16337}," blocks one by one, you can split the logic into multiple tests and use a \"setup\" ",{"type":27,"tag":653,"props":16339,"children":16341},{"className":16340},[],[16342],{"type":32,"value":7187},{"type":32,"value":16344}," function for assigning your variables and then execution ",{"type":27,"tag":653,"props":16346,"children":16348},{"className":16347},[],[16349],{"type":32,"value":7187},{"type":32,"value":16351}," block to use that variable. However, this approach might be quite limiting, as you need a separate block for every variable change. ",{"type":27,"tag":79,"props":16353,"children":16354},{},[16355],{"type":32,"value":16356},"It’s also not the best test design",{"type":32,"value":16358},", as not every ",{"type":27,"tag":653,"props":16360,"children":16362},{"className":16361},[],[16363],{"type":32,"value":7187},{"type":32,"value":16365}," function is now a test. This can also create a weird domino effect, where a failure of a test can be caused by a previous test.",{"type":27,"tag":793,"props":16367,"children":16370},{"className":16368,"code":16369,"language":1513,"meta":5},[1510],"let id // declare variable\n\nit('assign new value', () => {\n  cy.request('/api/boards')\n    .then( res => {\n      id = res.body[0].id \n    })\n})\n\nit('use variable', () => {\n  cy.visit('/board/' + id) \n})\n",[16371],{"type":27,"tag":653,"props":16372,"children":16373},{"__ignoreMap":5},[16374],{"type":32,"value":16369},{"type":27,"tag":1033,"props":16376,"children":16378},{"id":16377},"solution-3-use-hooks",[16379],{"type":32,"value":16380},"Solution #3: Use hooks",{"type":27,"tag":28,"props":16382,"children":16383},{},[16384,16386,16391,16392,16397],{"type":32,"value":16385},"A slightly better way to split a test is to use ",{"type":27,"tag":653,"props":16387,"children":16389},{"className":16388},[],[16390],{"type":32,"value":8434},{"type":32,"value":1591},{"type":27,"tag":653,"props":16393,"children":16395},{"className":16394},[],[16396],{"type":32,"value":7243},{"type":32,"value":16398}," hooks. This way you are splitting your test in a more logical way. You have a preparation phase, which is not part of the test, and an execution phase, which is the test itself. Another advantage of this approach is that when a hook fails, you’ll get a clear information about this in the error log.",{"type":27,"tag":793,"props":16400,"children":16403},{"className":16401,"code":16402,"language":1513,"meta":5},[1510],"let id // declare variable\n\nbeforeEach( () => {\n  cy.request('/api/boards')\n    .then( res => {\n    id = res.body[0].id \n  })\n})\n\nit('use variable', () => {\n  cy.visit('/board/' + id) \n})\n",[16404],{"type":27,"tag":653,"props":16405,"children":16406},{"__ignoreMap":5},[16407],{"type":32,"value":16402},{"type":27,"tag":1033,"props":16409,"children":16411},{"id":16410},"solution-4-use-aliases",[16412],{"type":32,"value":16413},"Solution #4: Use aliases",{"type":27,"tag":28,"props":16415,"children":16416},{},[16417],{"type":32,"value":16418},"We can skip creating a variable altogether and use aliases instead. They are not that different from variables, but they live directly in the context of our test. The advantage of this approach is that we don’t need to use the alias right away, but we can use it later in our test.",{"type":27,"tag":793,"props":16420,"children":16423},{"className":16421,"code":16422,"language":1513,"meta":5},[1510],"it('use alias', () => {\n\n  cy.request('/api/boards')\n    .as('board') // create alias\n  \n  // some more code\n  // ...\n\n  cy.get('@board') // use alias\n    .its('body')\n    .then( body => {\n    \n      cy.visit('/board/' + body[0].id)\n\n    })\n\n})\n",[16424],{"type":27,"tag":653,"props":16425,"children":16426},{"__ignoreMap":5},[16427],{"type":32,"value":16422},{"type":27,"tag":1033,"props":16429,"children":16431},{"id":16430},"solution-5-use-aliases-and-hooks",[16432],{"type":32,"value":16433},"Solution #5: Use aliases and hooks",{"type":27,"tag":28,"props":16435,"children":16436},{},[16437,16439,16445,16447,16453,16455,16460,16462,16468,16470,16476],{"type":32,"value":16438},"Aliases are actually part of Mocha - a framework that is bundled within Cypress and is used for executing tests. Whenever you use ",{"type":27,"tag":653,"props":16440,"children":16442},{"className":16441},[],[16443],{"type":32,"value":16444},".as()",{"type":32,"value":16446}," command, it will create the alias within Mocha context which can be accessed by using ",{"type":27,"tag":653,"props":16448,"children":16450},{"className":16449},[],[16451],{"type":32,"value":16452},"this",{"type":32,"value":16454}," keyword as shown in example. It will be a common variable, so you can share variables between tests in the spec. However, ",{"type":27,"tag":653,"props":16456,"children":16458},{"className":16457},[],[16459],{"type":32,"value":16452},{"type":32,"value":16461}," keyword cannot be used in functions with arrow expression ",{"type":27,"tag":653,"props":16463,"children":16465},{"className":16464},[],[16466],{"type":32,"value":16467},"() => {}",{"type":32,"value":16469},", but needs to be used with traditional function expression, ",{"type":27,"tag":653,"props":16471,"children":16473},{"className":16472},[],[16474],{"type":32,"value":16475},"function() {}",{"type":32,"value":16477},". See the example",{"type":27,"tag":793,"props":16479,"children":16482},{"className":16480,"code":16481,"language":1513,"meta":5},[1510],"beforeEach( () => {\n  cy.request('/api/boards')\n    .as('board')\n})\n\n// using  it('use variable', () => { ... would not work \nit('use variable', function() {\n  cy.visit('/board/' + this.board.body[0].id) \n})\n",[16483],{"type":27,"tag":653,"props":16484,"children":16485},{"__ignoreMap":5},[16486],{"type":32,"value":16481},{"type":27,"tag":28,"props":16488,"children":16489},{},[16490,16492],{"type":32,"value":16491},"There are a couple of more examples that can help you with storing variables in Cypress, these are just a few of them. I shared some more advanced examples in my older blog on how to handle data from API, ",{"type":27,"tag":172,"props":16493,"children":16494},{"href":15399},[16495],{"type":32,"value":16496},"you can check it out here.",{"title":5,"searchDepth":320,"depth":320,"links":16498},[16499,16500],{"id":16157,"depth":320,"text":16160},{"id":16276,"depth":320,"text":16056,"children":16501},[16502,16503,16504,16505,16506],{"id":16281,"depth":1606,"text":16284},{"id":16322,"depth":1606,"text":16325},{"id":16377,"depth":1606,"text":16380},{"id":16410,"depth":1606,"text":16413},{"id":16430,"depth":1606,"text":16433},"content:cypress-basics-variables:index.md","cypress-basics-variables/index.md","cypress-basics-variables/index",{"_path":12345,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":16511,"description":16512,"date":16513,"published":10,"slug":16514,"tags":16515,"cypressVersion":5959,"readingTime":16517,"body":16521,"_type":329,"_id":16907,"_source":331,"_file":16908,"_stem":16909,"_extension":334},"Cypress basics: xpath vs. CSS selectors","Using xpath with Cypress is possible through a plugin. In this post I show you how to install this plugin and show you some examples how to use xpath and compare it to Cypress commands.","2021-11-22","cypress-basics-xpath-vs-css-selectors",[5279,8651,12340,16516],"locators",{"text":3229,"minutes":16518,"time":16519,"words":16520},2.335,140100,467,{"type":24,"children":16522,"toc":16891},[16523,16533,16547,16553,16575,16604,16624,16644,16649,16655,16661,16670,16676,16685,16691,16700,16706,16715,16721,16730,16743,16749,16758,16764,16773,16778,16784,16793,16799,16808,16813,16821,16827,16836,16842,16851,16856],{"type":27,"tag":1029,"props":16524,"children":16525},{},[16526,16530],{"type":27,"tag":28,"props":16527,"children":16528},{},[16529],{"type":32,"value":5973},{"type":27,"tag":5975,"props":16531,"children":16532},{},[],{"type":27,"tag":28,"props":16534,"children":16535},{},[16536,16538,16545],{"type":32,"value":16537},"Let me start right of the bat stating that I’m not the biggest fan of ",{"type":27,"tag":172,"props":16539,"children":16542},{"href":16540,"rel":16541},"https://developer.mozilla.org/en-US/docs/Web/XPath",[696],[16543],{"type":32,"value":16544},"xpath selectors",{"type":32,"value":16546},". In my opinion, they are hard to read, and provide little benefits in comparison to CSS selectors or data-* attributes. With jQuery bundled into Cypress, you can select your elements in a much more readable way. However, they are widely used and a go-to choice for projects where you don’t have access to the source code. That’s why it’s useful to have knowledge on how to use them.",{"type":27,"tag":45,"props":16548,"children":16550},{"id":16549},"cypress-and-xpath",[16551],{"type":32,"value":16552},"Cypress and xpath",{"type":27,"tag":28,"props":16554,"children":16555},{},[16556,16558,16565,16567,16573],{"type":32,"value":16557},"To use xpath selectors, you must first ",{"type":27,"tag":172,"props":16559,"children":16562},{"href":16560,"rel":16561},"https://github.com/cypress-io/cypress-xpath",[696],[16563],{"type":32,"value":16564},"install a plugin",{"type":32,"value":16566},". It is an official plugin maintained by Cypress. The installation is pretty standard. Just ",{"type":27,"tag":653,"props":16568,"children":16570},{"className":16569},[],[16571],{"type":32,"value":16572},"npm install -D cypress-xpath",{"type":32,"value":16574}," to install the package.",{"type":27,"tag":28,"props":16576,"children":16577},{},[16578,16580,16586,16588,16594,16596,16602],{"type":32,"value":16579},"You then have to add ",{"type":27,"tag":653,"props":16581,"children":16583},{"className":16582},[],[16584],{"type":32,"value":16585},"require('cypress-xpath')",{"type":32,"value":16587}," to your ",{"type":27,"tag":653,"props":16589,"children":16591},{"className":16590},[],[16592],{"type":32,"value":16593},"cypress/support/index.js",{"type":32,"value":16595}," file. Without this, your plugin is not registered and you will get ",{"type":27,"tag":653,"props":16597,"children":16599},{"className":16598},[],[16600],{"type":32,"value":16601},"cy.xpath is not a function",{"type":32,"value":16603}," error.",{"type":27,"tag":28,"props":16605,"children":16606},{},[16607,16609,16615,16617,16622],{"type":32,"value":16608},"If you are using TypeScript, don’t forget to add ",{"type":27,"tag":653,"props":16610,"children":16612},{"className":16611},[],[16613],{"type":32,"value":16614},"cypress-xpath",{"type":32,"value":16616}," to your types in ",{"type":27,"tag":653,"props":16618,"children":16620},{"className":16619},[],[16621],{"type":32,"value":9229},{"type":32,"value":16623},"file.",{"type":27,"tag":28,"props":16625,"children":16626},{},[16627,16629,16635,16637,16642],{"type":32,"value":16628},"This will add an ",{"type":27,"tag":653,"props":16630,"children":16632},{"className":16631},[],[16633],{"type":32,"value":16634},".xpath()",{"type":32,"value":16636}," command, which works similarly to ",{"type":27,"tag":653,"props":16638,"children":16640},{"className":16639},[],[16641],{"type":32,"value":12748},{"type":32,"value":16643}," command. It will return an HTML element which you can then interact with. Let’s look into a couple of xpath examples and compare them to selector usage with Cypress commands.",{"type":27,"tag":28,"props":16645,"children":16646},{},[16647],{"type":32,"value":16648},"As is usuall with my blog, you can check out the working code in my repository branch.",{"type":27,"tag":45,"props":16650,"children":16652},{"id":16651},"cypress-vs-xpath-examples",[16653],{"type":32,"value":16654},"Cypress vs. xpath examples",{"type":27,"tag":1033,"props":16656,"children":16658},{"id":16657},"select-the-whole-document",[16659],{"type":32,"value":16660},"Select the whole document",{"type":27,"tag":793,"props":16662,"children":16665},{"className":16663,"code":16664,"language":3520,"meta":5},[3517],"cy.xpath('/html')\ncy.root()\n",[16666],{"type":27,"tag":653,"props":16667,"children":16668},{"__ignoreMap":5},[16669],{"type":32,"value":16664},{"type":27,"tag":1033,"props":16671,"children":16673},{"id":16672},"select-an-element-by-text",[16674],{"type":32,"value":16675},"Select an element by text",{"type":27,"tag":793,"props":16677,"children":16680},{"className":16678,"code":16679,"language":3520,"meta":5},[3517],"cy.xpath('//*[text()[contains(.,\"My Boards\")]]')\ncy.contains('My Boards')\n",[16681],{"type":27,"tag":653,"props":16682,"children":16683},{"__ignoreMap":5},[16684],{"type":32,"value":16679},{"type":27,"tag":1033,"props":16686,"children":16688},{"id":16687},"select-a-specific-element-by-text",[16689],{"type":32,"value":16690},"Select a specific element by text",{"type":27,"tag":793,"props":16692,"children":16695},{"className":16693,"code":16694,"language":3520,"meta":5},[3517],"cy.xpath('//h1[contains(.,\"My Boards\")]')\ncy.contains('h1', 'My Boards')\n",[16696],{"type":27,"tag":653,"props":16697,"children":16698},{"__ignoreMap":5},[16699],{"type":32,"value":16694},{"type":27,"tag":1033,"props":16701,"children":16703},{"id":16702},"select-an-element-by-attribute",[16704],{"type":32,"value":16705},"Select an element by attribute",{"type":27,"tag":793,"props":16707,"children":16710},{"className":16708,"code":16709,"language":3520,"meta":5},[3517],"cy.xpath('//*[@data-cy=\"create-board\"]')\ncy.get('[data-cy=\"create-board\"]')\n",[16711],{"type":27,"tag":653,"props":16712,"children":16713},{"__ignoreMap":5},[16714],{"type":32,"value":16709},{"type":27,"tag":1033,"props":16716,"children":16718},{"id":16717},"select-an-element-that-contains-a-class",[16719],{"type":32,"value":16720},"Select an element that contains a class",{"type":27,"tag":793,"props":16722,"children":16725},{"className":16723,"code":16724,"language":3520,"meta":5},[3517],"cy.xpath('//*[contains(@class, \"font-semibold\"]')\ncy.get('.font-semibold')\n",[16726],{"type":27,"tag":653,"props":16727,"children":16728},{"__ignoreMap":5},[16729],{"type":32,"value":16724},{"type":27,"tag":28,"props":16731,"children":16732},{},[16733,16735,16741],{"type":32,"value":16734},"Important side note here. This xpath will match any substring in the class attribute, that means that if we had an element with a class name ",{"type":27,"tag":653,"props":16736,"children":16738},{"className":16737},[],[16739],{"type":32,"value":16740},"button_font-semibold",{"type":32,"value":16742}," it would also be matched by this xpath selector.",{"type":27,"tag":1033,"props":16744,"children":16746},{"id":16745},"select-an-element-with-specific-class-by-text",[16747],{"type":32,"value":16748},"Select an element with specific class, by text",{"type":27,"tag":793,"props":16750,"children":16753},{"className":16751,"code":16752,"language":3520,"meta":5},[3517],"cy.xpath('//*[contains(@class, \"font-semibold\")][text()[contains(.,\"My Boards\")]]')\ncy.contains('.font-semibold', 'My Boards')\n",[16754],{"type":27,"tag":653,"props":16755,"children":16756},{"__ignoreMap":5},[16757],{"type":32,"value":16752},{"type":27,"tag":1033,"props":16759,"children":16761},{"id":16760},"filter-an-element-by-index",[16762],{"type":32,"value":16763},"Filter an element by index",{"type":27,"tag":793,"props":16765,"children":16768},{"className":16766,"code":16767,"language":3520,"meta":5},[3517],"cy.xpath('(//div[contains(@class, \"board\")])[1]')\ncy.get('.board').eq(0)\n",[16769],{"type":27,"tag":653,"props":16770,"children":16771},{"__ignoreMap":5},[16772],{"type":32,"value":16767},{"type":27,"tag":28,"props":16774,"children":16775},{},[16776],{"type":32,"value":16777},"Notice that xpath does not use the numbering from 0, as is often used in other languages, but starts numbering from number 1.",{"type":27,"tag":1033,"props":16779,"children":16781},{"id":16780},"select-a-child-element",[16782],{"type":32,"value":16783},"Select a child element",{"type":27,"tag":793,"props":16785,"children":16788},{"className":16786,"code":16787,"language":3520,"meta":5},[3517],"cy.xpath('//div[contains(@class, \"list\")]//child::div[contains(@class, \"card\")]')\ncy.get('.list').find('.card')\n",[16789],{"type":27,"tag":653,"props":16790,"children":16791},{"__ignoreMap":5},[16792],{"type":32,"value":16787},{"type":27,"tag":1033,"props":16794,"children":16796},{"id":16795},"select-an-element-containing-a-specific-child-element",[16797],{"type":32,"value":16798},"Select an element containing a specific child element",{"type":27,"tag":793,"props":16800,"children":16803},{"className":16801,"code":16802,"language":3520,"meta":5},[3517],"cy.xpath('//div[contains(@class, \"list\")][.//div[contains(@class, \"card\")]]')\ncy.get('.card').parents('.list')\n",[16804],{"type":27,"tag":653,"props":16805,"children":16806},{"__ignoreMap":5},[16807],{"type":32,"value":16802},{"type":27,"tag":28,"props":16809,"children":16810},{},[16811],{"type":32,"value":16812},"In this example, we want to select only the list that contains some cards:",{"type":27,"tag":28,"props":16814,"children":16815},{},[16816],{"type":27,"tag":959,"props":16817,"children":16820},{"alt":16818,"src":16819},"Selecting only the list with cards","list-with-cards.png",[],{"type":27,"tag":1033,"props":16822,"children":16824},{"id":16823},"select-an-element-after-a-specific-element",[16825],{"type":32,"value":16826},"Select an element after a specific element",{"type":27,"tag":793,"props":16828,"children":16831},{"className":16829,"code":16830,"language":3520,"meta":5},[3517],"cy.xpath('//div[contains(@class, \"card\")][preceding::div[contains(., \"milk\")]]')\ncy.contains('.card', 'milk').next('.card')\n",[16832],{"type":27,"tag":653,"props":16833,"children":16834},{"__ignoreMap":5},[16835],{"type":32,"value":16830},{"type":27,"tag":1033,"props":16837,"children":16839},{"id":16838},"select-an-element-before-a-specific-element",[16840],{"type":32,"value":16841},"Select an element before a specific element",{"type":27,"tag":793,"props":16843,"children":16846},{"className":16844,"code":16845,"language":3520,"meta":5},[3517],"cy.xpath('//div[contains(@class, \"card\")][following::div[contains(., \"bread\")]]')\ncy.contains('.card', 'bread').next('.card')\n",[16847],{"type":27,"tag":653,"props":16848,"children":16849},{"__ignoreMap":5},[16850],{"type":32,"value":16845},{"type":27,"tag":28,"props":16852,"children":16853},{},[16854],{"type":32,"value":16855},"Hope this helps. Share this with your friends if you feel like someone can learn from this, I’d greatly appreciate this.",{"type":27,"tag":28,"props":16857,"children":16858},{},[16859,16861,16866,16867,16873,16875,16881,16883,16890],{"type":32,"value":16860},"If you want to learn more about selecting elements, I recommend checking out my other articles on ",{"type":27,"tag":172,"props":16862,"children":16863},{"href":12302},[16864],{"type":32,"value":16865},"selecting elements",{"type":32,"value":3372},{"type":27,"tag":172,"props":16868,"children":16870},{"href":16869},"/autocompleting-selectors-in-cypress-with-typescript",[16871],{"type":32,"value":16872},"autocompleting selectors",{"type":32,"value":16874}," or a very powerful ",{"type":27,"tag":172,"props":16876,"children":16878},{"href":16877},"/contains-an-overlooked-gem-in-cypress",[16879],{"type":32,"value":16880},".contains() command",{"type":32,"value":16882},". Additionally, if you work with xpath, I recommend checking out ",{"type":27,"tag":172,"props":16884,"children":16887},{"href":16885,"rel":16886},"https://selectorshub.com/testcase-studio/",[696],[16888],{"type":32,"value":16889},"Sanjay Kumar’s SelectorsHub tool",{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":16892},[16893,16894],{"id":16549,"depth":320,"text":16552},{"id":16651,"depth":320,"text":16654,"children":16895},[16896,16897,16898,16899,16900,16901,16902,16903,16904,16905,16906],{"id":16657,"depth":1606,"text":16660},{"id":16672,"depth":1606,"text":16675},{"id":16687,"depth":1606,"text":16690},{"id":16702,"depth":1606,"text":16705},{"id":16717,"depth":1606,"text":16720},{"id":16745,"depth":1606,"text":16748},{"id":16760,"depth":1606,"text":16763},{"id":16780,"depth":1606,"text":16783},{"id":16795,"depth":1606,"text":16798},{"id":16823,"depth":1606,"text":16826},{"id":16838,"depth":1606,"text":16841},"content:cypress-basics-xpath-vs-css-selectors:index.md","cypress-basics-xpath-vs-css-selectors/index.md","cypress-basics-xpath-vs-css-selectors/index",{"_path":16869,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":16911,"description":16912,"date":16913,"published":10,"slug":16914,"tags":16915,"cypressVersion":5959,"readingTime":16917,"body":16921,"_type":329,"_id":17252,"_source":331,"_file":17253,"_stem":17254,"_extension":334},"Autocompleting selectors in Cypress with TypeScript","Using TypeScript, we can make our lives with custom selectors easier. Our editor can autocomplete our selectors and check if we aren’t using any that were already deleted","2021-11-15","autocompleting-selectors-in-cypress-with-typescript",[5279,8651,2608,16916,9685],"custom command",{"text":585,"minutes":16918,"time":16919,"words":16920},3.785,227100,757,{"type":24,"children":16922,"toc":17245},[16923,16955,16968,16974,16986,16995,17021,17034,17043,17048,17056,17062,17082,17092,17097,17105,17111,17132,17141,17169,17179,17191,17200,17213,17219,17224,17233],{"type":27,"tag":1029,"props":16924,"children":16925},{},[16926,16932],{"type":27,"tag":1033,"props":16927,"children":16929},{"id":16928},"you-will-learn",[16930],{"type":32,"value":16931},"You will learn:",{"type":27,"tag":105,"props":16933,"children":16934},{},[16935,16940,16945,16950],{"type":27,"tag":109,"props":16936,"children":16937},{},[16938],{"type":32,"value":16939},"how to create custom command that will autocomplete your selectors",{"type":27,"tag":109,"props":16941,"children":16942},{},[16943],{"type":32,"value":16944},"how perform a check on your tests that are written in TypeScript",{"type":27,"tag":109,"props":16946,"children":16947},{},[16948],{"type":32,"value":16949},"how to grep selectors from your app",{"type":27,"tag":109,"props":16951,"children":16952},{},[16953],{"type":32,"value":16954},"how to create warning if there are selectors in your app that you are not using",{"type":27,"tag":28,"props":16956,"children":16957},{},[16958,16960,16966],{"type":32,"value":16959},"Cypress advises to use data-cy selectors as a best practice for selecting your elements on page. Recently, we had a great discussion on our ",{"type":27,"tag":172,"props":16961,"children":16963},{"href":8322,"rel":16962},[696],[16964],{"type":32,"value":16965},"discord server",{"type":32,"value":16967}," about whether this is a good practice. Personally I strongly lean to \"yes\". If you are in this camp as well, I have some nice tips for you today.",{"type":27,"tag":45,"props":16969,"children":16971},{"id":16970},"using-data-selectors-with-custom-commands",[16972],{"type":32,"value":16973},"Using data-* selectors with custom commands",{"type":27,"tag":28,"props":16975,"children":16976},{},[16977,16979,16984],{"type":32,"value":16978},"If you are using ",{"type":27,"tag":653,"props":16980,"children":16982},{"className":16981},[],[16983],{"type":32,"value":8729},{"type":32,"value":16985}," selectors a lot, chances are you want to create a custom command.",{"type":27,"tag":793,"props":16987,"children":16990},{"className":16988,"code":16989,"language":1513,"meta":5},[1510],"Cypress.Commands.add('getDataCy', (input) => {\n  return cy.get(`[data-cy='${input}']`);\n})\n",[16991],{"type":27,"tag":653,"props":16992,"children":16993},{"__ignoreMap":5},[16994],{"type":32,"value":16989},{"type":27,"tag":28,"props":16996,"children":16997},{},[16998,17000,17006,17008,17013,17015],{"type":32,"value":16999},"This way you can skip writing out ",{"type":27,"tag":653,"props":17001,"children":17003},{"className":17002},[],[17004],{"type":32,"value":17005},"[data-cy='']",{"type":32,"value":17007}," part, when you are using a ",{"type":27,"tag":653,"props":17009,"children":17011},{"className":17010},[],[17012],{"type":32,"value":12748},{"type":32,"value":17014}," command. If you’d like to create a fancier version of this, I wrote about an article on this topic, explaining how you can ",{"type":27,"tag":172,"props":17016,"children":17018},{"href":17017},"/improve-your-custom-command-logs-in-cypress#highlighting-elements",[17019],{"type":32,"value":17020},"improve your logs for your custom command.",{"type":27,"tag":28,"props":17022,"children":17023},{},[17024,17026,17032],{"type":32,"value":17025},"When using TypeScript, you can further define what kind of input will our newly created custom function receive. Usually we could go for something simple, like ",{"type":27,"tag":653,"props":17027,"children":17029},{"className":17028},[],[17030],{"type":32,"value":17031},"input: string",{"type":32,"value":17033},". But we can instead create our own type, which will limit what kind of input we can pass to our function.",{"type":27,"tag":793,"props":17035,"children":17038},{"className":17036,"code":17037,"language":3520,"meta":5},[3517],"type Selectors = 'board' | 'list' | 'card'\n\nCypress.Commands.add('getDataCy', (input: Selectors) => {\n  return cy.get(`[data-cy='${input}']`);\n})\n",[17039],{"type":27,"tag":653,"props":17040,"children":17041},{"__ignoreMap":5},[17042],{"type":32,"value":17037},{"type":27,"tag":28,"props":17044,"children":17045},{},[17046],{"type":32,"value":17047},"This way, TypeScript will throw an error when we use a selector that is not allowed:",{"type":27,"tag":28,"props":17049,"children":17050},{},[17051],{"type":27,"tag":959,"props":17052,"children":17055},{"alt":17053,"src":17054},"TypeScript error on unknown selector","selector.png",[],{"type":27,"tag":45,"props":17057,"children":17059},{"id":17058},"creating-a-selector-file",[17060],{"type":32,"value":17061},"Creating a selector file",{"type":27,"tag":28,"props":17063,"children":17064},{},[17065,17067,17073,17075,17081],{"type":32,"value":17066},"The list of selectors might get quite big over time. That’s why it’s probably a good idea to keep it in a separate file. My approach is to create a ",{"type":27,"tag":653,"props":17068,"children":17070},{"className":17069},[],[17071],{"type":32,"value":17072},"selectors.d.ts",{"type":32,"value":17074}," file where I keep a list of all my selectors. I usually save this to the ",{"type":27,"tag":653,"props":17076,"children":17078},{"className":17077},[],[17079],{"type":32,"value":17080},"cypress/support/@types/",{"type":32,"value":9687},{"type":27,"tag":793,"props":17083,"children":17087},{"className":17084,"code":17085,"filename":17086,"language":3520,"meta":5},[3517],"type Selectors = \n  | 'board'\n  | 'list'\n  | 'card'\n  // ...\n","cypress/support/@types/selectors.d.ts",[17088],{"type":27,"tag":653,"props":17089,"children":17090},{"__ignoreMap":5},[17091],{"type":32,"value":17085},{"type":27,"tag":28,"props":17093,"children":17094},{},[17095],{"type":32,"value":17096},"Whenever I add a new selector to my application, I need to add it to my list, otherwise I’ll get an error. On the other hand, when I use a selector that is already in the list, I get a nice autocomplete in VS Code.",{"type":27,"tag":28,"props":17098,"children":17099},{},[17100],{"type":27,"tag":959,"props":17101,"children":17104},{"alt":17102,"src":17103},"Autosuggesting selectors","autosuggest.png",[],{"type":27,"tag":45,"props":17106,"children":17108},{"id":17107},"checking-the-selectors",[17109],{"type":32,"value":17110},"Checking the selectors",{"type":27,"tag":28,"props":17112,"children":17113},{},[17114,17116,17122,17124,17130],{"type":32,"value":17115},"Of course, we can get into a situation where we might delete a selector from our list, and we wouldn’t notice until we open the test file, or our tests run. Since we are using TypeScript, we can create a ",{"type":27,"tag":653,"props":17117,"children":17119},{"className":17118},[],[17120],{"type":32,"value":17121},"typecheck",{"type":32,"value":17123}," script, which will check for any TypeScript errors. This script will use ",{"type":27,"tag":653,"props":17125,"children":17127},{"className":17126},[],[17128],{"type":32,"value":17129},"--noEmit",{"type":32,"value":17131}," flag, because don’t need to compile our files. This is done by Cypress when we run our tests.",{"type":27,"tag":793,"props":17133,"children":17136},{"className":17134,"code":17135,"filename":734,"language":1004,"meta":5},[1002],"// ...\n\"scripts\": {\n  \"typecheck\": \"tsc --noEmit -p .\"\n}\n// ...\n",[17137],{"type":27,"tag":653,"props":17138,"children":17139},{"__ignoreMap":5},[17140],{"type":32,"value":17135},{"type":27,"tag":28,"props":17142,"children":17143},{},[17144,17146,17152,17154,17160,17162,17168],{"type":32,"value":17145},"Another thing we can do is to grep our selectors from our source code. This is of course a little tricky, because not all applications and frameworks work the same. In my ",{"type":27,"tag":172,"props":17147,"children":17149},{"href":9303,"rel":17148},[696],[17150],{"type":32,"value":17151},"Trello app project",{"type":32,"value":17153},", I have a script that will grep these from my ",{"type":27,"tag":653,"props":17155,"children":17157},{"className":17156},[],[17158],{"type":32,"value":17159},".vue",{"type":32,"value":17161}," files inside the ",{"type":27,"tag":653,"props":17163,"children":17165},{"className":17164},[],[17166],{"type":32,"value":17167},"src",{"type":32,"value":9687},{"type":27,"tag":793,"props":17170,"children":17174},{"className":17171,"code":17172,"filename":17173,"language":3589,"meta":5},[3587],"#!/usr/bin/env bash\n\nSRC_SELECTORS=$(grep -hro 'data-cy=\"[^\"]*\"' src | cut -d \\\" -f2 | sort | uniq)\n","scripts/getSelectors.sh",[17175],{"type":27,"tag":653,"props":17176,"children":17177},{"__ignoreMap":5},[17178],{"type":32,"value":17172},{"type":27,"tag":28,"props":17180,"children":17181},{},[17182,17184,17189],{"type":32,"value":17183},"I then use this very cluncky way of generating all of the selectors into the ",{"type":27,"tag":653,"props":17185,"children":17187},{"className":17186},[],[17188],{"type":32,"value":17086},{"type":32,"value":17190}," file. But hey, it works for me so far! 😅",{"type":27,"tag":793,"props":17192,"children":17195},{"className":17193,"code":17194,"filename":17173,"language":3589,"meta":5},[3587],"#!/usr/bin/env bash\n\nSRC_SELECTORS=$(grep -hro 'data-cy=\"[^\"]*\"' src | cut -d \\\" -f2 | sort | uniq)\necho $SRC_SELECTORS | sed \"s/ /'\\n| '/g; s/^/&export type Selectors = \\n| '/; s/.$/&'/;\" | cat > cypress/support/@types/selectors.d.ts\n",[17196],{"type":27,"tag":653,"props":17197,"children":17198},{"__ignoreMap":5},[17199],{"type":32,"value":17194},{"type":27,"tag":28,"props":17201,"children":17202},{},[17203,17205,17211],{"type":32,"value":17204},"This way, if any of my selectors from source code would be deleteed I would immediately notice when I run that ",{"type":27,"tag":653,"props":17206,"children":17208},{"className":17207},[],[17209],{"type":32,"value":17210},"npm run typecheck",{"type":32,"value":17212}," script.",{"type":27,"tag":45,"props":17214,"children":17216},{"id":17215},"finding-unused-data-cy-selectors",[17217],{"type":32,"value":17218},"Finding unused data-cy selectors",{"type":27,"tag":28,"props":17220,"children":17221},{},[17222],{"type":32,"value":17223},"I started having way too much fun with this, so decided to grep my tests too. Then, I would compare these selectors with the ones find in my source code and print out warning if there are any extra.",{"type":27,"tag":793,"props":17225,"children":17228},{"className":17226,"code":17227,"filename":17173,"language":3589,"meta":5},[3587],"#!/usr/bin/env bash\n\n# find all selectors in src folder\nSRC_SELECTORS=$(grep -hro 'data-cy=\"[^\"]*\"' src | cut -d \\\" -f2 | sort | uniq)\n# find all selectors in cypress folder\nTEST_SELECTORS=$(grep -hro '.getDataCy([^\"]*' cypress | cut -d \\' -f2 | sort | uniq)\n\n# compare selector lists\nUNUSED_SELECTORS=$(comm -23 \u003C(echo \"$SRC_SELECTORS\") \u003C(echo \"$TEST_SELECTORS\"))\n\n# if any found in src, print them our\nif [[ $UNUSED_SELECTORS != \"\" ]]; then\necho -e \"WARNING! There are some selectors in your app that are not being used:\n\n$UNUSED_SELECTORS\"\nfi\n\n# create selectors.d.ts from all selectors found in src\necho $SRC_SELECTORS | sed \"s/ /'\\n| '/g; s/^/&export type Selectors = \\n| '/; s/.$/&'/;\" | cat > cypress/support/@types/selectors.d.ts\nfi\n",[17229],{"type":27,"tag":653,"props":17230,"children":17231},{"__ignoreMap":5},[17232],{"type":32,"value":17227},{"type":27,"tag":28,"props":17234,"children":17235},{},[17236,17238,17243],{"type":32,"value":17237},"I imagine there are some better ways to approach this, as I’m not really a shell script expert. I have this script in my ",{"type":27,"tag":172,"props":17239,"children":17241},{"href":9303,"rel":17240},[696],[17242],{"type":32,"value":17151},{"type":32,"value":17244},", feel free to add a comment or PR to improve this.",{"title":5,"searchDepth":320,"depth":320,"links":17246},[17247,17248,17249,17250,17251],{"id":16928,"depth":1606,"text":16931},{"id":16970,"depth":320,"text":16973},{"id":17058,"depth":320,"text":17061},{"id":17107,"depth":320,"text":17110},{"id":17215,"depth":320,"text":17218},"content:autocompleting-selectors-in-cypress-with-typescript:index.md","autocompleting-selectors-in-cypress-with-typescript/index.md","autocompleting-selectors-in-cypress-with-typescript/index",{"_path":17256,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":17257,"description":17258,"date":17259,"published":10,"slug":17260,"tags":17261,"cypressVersion":5959,"readingTime":17264,"body":17268,"_type":329,"_id":17549,"_source":331,"_file":17550,"_stem":17551,"_extension":334},"/generating-random-user-in-cypress-test","Generating a random user in Cypress test","Quick guide on how to generate a random user in Cypress and an explanation of various ways on how to integrate that data into Cypress test.","2021-05-24","generating-random-user-in-cypress-test",[5279,17262,17263],"faker","random",{"text":585,"minutes":17265,"time":17266,"words":17267},3.74,224400,748,{"type":24,"children":17269,"toc":17544},[17270,17275,17280,17286,17312,17322,17335,17340,17351,17357,17369,17379,17390,17399,17420,17429,17448,17453,17459,17472,17483,17496,17506,17526],{"type":27,"tag":28,"props":17271,"children":17272},{},[17273],{"type":32,"value":17274},"Most of web applications under test require some kind of authentication. What I like to do when testing such application is to create a testing user. This is usually a randomly generated user, which I then use for most of my tests. In this blogpost I explore a couple of ways how to generate a user an then use that user in Cypress tests.",{"type":27,"tag":28,"props":17276,"children":17277},{},[17278],{"type":32,"value":17279},"As is usual on this blog, I use my Trello clone app in a repository that you can check out and follow along. I tried to keep this blogpost simple, so the creating of user in my app is just a single http request. But in principle, you should be able to apply this into your test efforts as well.",{"type":27,"tag":45,"props":17281,"children":17283},{"id":17282},"using-a-hook",[17284],{"type":32,"value":17285},"Using a hook",{"type":27,"tag":28,"props":17287,"children":17288},{},[17289,17291,17296,17298,17303,17305,17311],{"type":32,"value":17290},"It is possible to put a ",{"type":27,"tag":653,"props":17292,"children":17294},{"className":17293},[],[17295],{"type":32,"value":8434},{"type":32,"value":17297}," or a ",{"type":27,"tag":653,"props":17299,"children":17301},{"className":17300},[],[17302],{"type":32,"value":7243},{"type":32,"value":17304}," hook in your ",{"type":27,"tag":653,"props":17306,"children":17308},{"className":17307},[],[17309],{"type":32,"value":17310},"support/index.js",{"type":32,"value":1078},{"type":27,"tag":793,"props":17313,"children":17317},{"className":17314,"code":17315,"filename":17316,"language":1513,"meta":5},[1510],"const { internet } = require('faker');\nconst email = internet.exampleEmail()\nconst password = internet.password()\n\nbeforeEach(() => {\n  cy\n    .request('POST', '/signup', { email, password })\n    .then(({ body }) => {\n    cy\n      .setCookie('trello_token', body.accessToken);\n  });\n});\n","./cypress/support/index.js",[17318],{"type":27,"tag":653,"props":17319,"children":17320},{"__ignoreMap":5},[17321],{"type":32,"value":17315},{"type":27,"tag":28,"props":17323,"children":17324},{},[17325,17327,17333],{"type":32,"value":17326},"With a little bit of help from ",{"type":27,"tag":172,"props":17328,"children":17331},{"href":17329,"rel":17330},"https://www.npmjs.com/package/faker",[696],[17332],{"type":32,"value":17262},{"type":32,"value":17334},", we can generate random example emails in our application for each spec or for each test. In our test, we are logging in by inserting authorization cookie into our browser.",{"type":27,"tag":28,"props":17336,"children":17337},{},[17338],{"type":32,"value":17339},"Signing up before each test may create quite a lot of data. On the other hand, it helps our tests with being separated from one another. This is usually a good thing, but for most of the tests, creating a new user might be a little bit of an overkill.",{"type":27,"tag":28,"props":17341,"children":17342},{},[17343,17345,17350],{"type":32,"value":17344},"By the way, that curly bracket syntax is known as destructuring. It’s a JavaScript syntax, and I wrote ",{"type":27,"tag":172,"props":17346,"children":17347},{"href":15387},[17348],{"type":32,"value":17349},"an article about how to use it in Cypress",{"type":32,"value":256},{"type":27,"tag":45,"props":17352,"children":17354},{"id":17353},"creating-a-script",[17355],{"type":32,"value":17356},"Creating a script",{"type":27,"tag":28,"props":17358,"children":17359},{},[17360,17362,17367],{"type":32,"value":17361},"This is an approach I have chosen in the past. Basically, before I’d start my test with ",{"type":27,"tag":653,"props":17363,"children":17365},{"className":17364},[],[17366],{"type":32,"value":14819},{"type":32,"value":17368}," I’d run a script that would create my test user and write it to a file. I’d then use that file in my test and log in with my user.",{"type":27,"tag":793,"props":17370,"children":17374},{"className":17371,"code":17372,"filename":17373,"language":1513,"meta":5},[1510],"const axios = require('axios')\nconst { internet } = require('faker');\nconst email = internet.exampleEmail()\nconst password = internet.password()\nconst fs = require('fs')\n\nconst signupUser = async () => {\n\n  const user = await axios\n    .post('http://localhost:3000/signup', { email, password })\n      \n  fs.writeFileSync(\"./cypress/data.json\", JSON.stringify(user.data))\n\n}\n\nsignupUser()\n","signup.js",[17375],{"type":27,"tag":653,"props":17376,"children":17377},{"__ignoreMap":5},[17378],{"type":32,"value":17372},{"type":27,"tag":28,"props":17380,"children":17381},{},[17382,17384,17389],{"type":32,"value":17383},"I would run this as a separate script, which I’d define in my ",{"type":27,"tag":653,"props":17385,"children":17387},{"className":17386},[],[17388],{"type":32,"value":734},{"type":32,"value":786},{"type":27,"tag":793,"props":17391,"children":17394},{"className":17392,"code":17393,"filename":734,"language":1004,"meta":5},[1002],"\"scripts\": {\n    \"start\": \"cd trelloapp && npm start\",\n    \"cy:run\": \"cypress run\",\n    \"createUser\": \"node signupUser.js\"\n  }\n",[17395],{"type":27,"tag":653,"props":17396,"children":17397},{"__ignoreMap":5},[17398],{"type":32,"value":17393},{"type":27,"tag":28,"props":17400,"children":17401},{},[17402,17404,17410,17412,17418],{"type":32,"value":17403},"When I have a script like this, I can run it by ",{"type":27,"tag":653,"props":17405,"children":17407},{"className":17406},[],[17408],{"type":32,"value":17409},"npm run createUser",{"type":32,"value":17411}," and then run my tests by ",{"type":27,"tag":653,"props":17413,"children":17415},{"className":17414},[],[17416],{"type":32,"value":17417},"npm run cy:run",{"type":32,"value":17419},". In my tests, I can simply read the file and set the cookie from the file:",{"type":27,"tag":793,"props":17421,"children":17424},{"className":17422,"code":17423,"language":1513,"meta":5},[1510],"cy\n  .readFile('./cypress/data.json')\n  .then(({ accessToken }) => {\n  cy\n    .setCookie('trello_token', accessToken);\n}); \n",[17425],{"type":27,"tag":653,"props":17426,"children":17427},{"__ignoreMap":5},[17428],{"type":32,"value":17423},{"type":27,"tag":28,"props":17430,"children":17431},{},[17432,17434,17440,17441,17446],{"type":32,"value":17433},"If you choose this approach, it is important to include the ",{"type":27,"tag":653,"props":17435,"children":17437},{"className":17436},[],[17438],{"type":32,"value":17439},"data.json",{"type":32,"value":15244},{"type":27,"tag":653,"props":17442,"children":17444},{"className":17443},[],[17445],{"type":32,"value":4244},{"type":32,"value":17447}," file. Especially if the user you have created is not a disposable one. It is not a good practice to store any sensitive data in your storage. In fact, the main reason I have moved away from this approach was that it is risky, especially as your team grows and more people start contributing to the code base.",{"type":27,"tag":28,"props":17449,"children":17450},{},[17451],{"type":32,"value":17452},"I also find this solution not to be very elegant. If I forget to run the script my tests will fail. But it was a part of my learning journey and it served me well while I was using it. So far, the best approach for me was to run this script as a plugin and resolve it during Cypress config.",{"type":27,"tag":45,"props":17454,"children":17456},{"id":17455},"writing-a-plugin",[17457],{"type":32,"value":17458},"Writing a plugin",{"type":27,"tag":28,"props":17460,"children":17461},{},[17462,17464,17470],{"type":32,"value":17463},"Let’s now rewrite our ",{"type":27,"tag":653,"props":17465,"children":17467},{"className":17466},[],[17468],{"type":32,"value":17469},"signupUser",{"type":32,"value":17471}," script so that it returns the response data and can be imported as a module:",{"type":27,"tag":793,"props":17473,"children":17478},{"className":17474,"code":17475,"filename":17476,"highlights":17477,"language":1513,"meta":5},[1510],"const signupUser = async () => {\n\n  const user = await axios\n    .post('http://localhost:3000/signup', { email, password })\n      \n  return user.data\n\n}\n\nmodule.exports = signupUser\n","signupUser.js",[3809,3810],[17479],{"type":27,"tag":653,"props":17480,"children":17481},{"__ignoreMap":5},[17482],{"type":32,"value":17475},{"type":27,"tag":28,"props":17484,"children":17485},{},[17486,17488,17494],{"type":32,"value":17487},"We can now import this module to our ",{"type":27,"tag":653,"props":17489,"children":17491},{"className":17490},[],[17492],{"type":32,"value":17493},"plugins/index.js",{"type":32,"value":17495}," file and use it during configuration:",{"type":27,"tag":793,"props":17497,"children":17501},{"className":17498,"code":17499,"filename":17500,"language":1513,"meta":5},[1510],"const signupUser = require('../../signupUser.js')\n\nmodule.exports = async (on, config) => {\n\n  config.env = await signupUser()\n\n  return config;\n\n}\n","cypress/plugins/index.js",[17502],{"type":27,"tag":653,"props":17503,"children":17504},{"__ignoreMap":5},[17505],{"type":32,"value":17499},{"type":27,"tag":28,"props":17507,"children":17508},{},[17509,17511,17517,17519,17524],{"type":32,"value":17510},"Cypress will wait until our ",{"type":27,"tag":653,"props":17512,"children":17514},{"className":17513},[],[17515],{"type":32,"value":17516},"signupUser()",{"type":32,"value":17518}," function is resolved, and then saves the data returned by that function to ",{"type":27,"tag":653,"props":17520,"children":17522},{"className":17521},[],[17523],{"type":32,"value":5957},{"type":32,"value":17525}," object. This way, our data is generated during test run and it’s only available while Cypress runs. If you run your tests in parallel e.g. on 10 machines, then this script will be called 10 times. But that might actually be a good thing since those generated users will not interfere with each other.",{"type":27,"tag":28,"props":17527,"children":17528},{},[17529,17531,17536,17537,17542],{"type":32,"value":17530},"If you enjoyed this blog, subscribe to the newsletter to get notified about new articles. Or follow me on ",{"type":27,"tag":172,"props":17532,"children":17534},{"href":5770,"rel":17533},[696],[17535],{"type":32,"value":1589},{"type":32,"value":4164},{"type":27,"tag":172,"props":17538,"children":17540},{"href":10953,"rel":17539},[696],[17541],{"type":32,"value":1598},{"type":32,"value":17543},", where I usually share when a new blog comes out.",{"title":5,"searchDepth":320,"depth":320,"links":17545},[17546,17547,17548],{"id":17282,"depth":320,"text":17285},{"id":17353,"depth":320,"text":17356},{"id":17455,"depth":320,"text":17458},"content:generating-random-user-in-cypress-test:index.md","generating-random-user-in-cypress-test/index.md","generating-random-user-in-cypress-test/index",{"_path":15869,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":17553,"description":17554,"date":17555,"published":10,"slug":17556,"tags":17557,"cypressVersion":5959,"readingTime":17561,"body":17565,"_type":329,"_id":17799,"_source":331,"_file":17800,"_stem":17801,"_extension":334},"Testing links with Cypress","Let’s say you want to test all the links in a navigation bar, ideally in most effective way. In this article I show you some of the most effective ways","2021-04-26","testing-links-with-cypress",[5279,17558,17559,17560],"links","loop","href",{"text":585,"minutes":17562,"time":17563,"words":17564},3.935,236100,787,{"type":24,"children":17566,"toc":17797},[17567,17572,17585,17593,17598,17607,17612,17617,17627,17640,17670,17690,17702,17711,17743,17756,17765,17770,17780],{"type":27,"tag":28,"props":17568,"children":17569},{},[17570],{"type":32,"value":17571},"I often see a testing case when someone needs to test a navigation bar on a page, to make sure that all the links are actually functioning. This is a very nice problem case, where different strategies may be applied. In this article, I’d like to go through these and show you how can you apply them with Cypress.",{"type":27,"tag":28,"props":17573,"children":17574},{},[17575,17577,17584],{"type":32,"value":17576},"Let’s first see what are we going to be testing. We have a simple navigation bar that will redirect us to different pages of our site. As is usuall on this blog, you can follow along with ",{"type":27,"tag":172,"props":17578,"children":17581},{"href":17579,"rel":17580},"https://github.com/filiphric/testing-links",[696],[17582],{"type":32,"value":17583},"this example on a repo I made",{"type":32,"value":256},{"type":27,"tag":793,"props":17586,"children":17588},{"className":17587,"code":12782,"language":7826,"meta":5},[7824],[17589],{"type":27,"tag":653,"props":17590,"children":17591},{"__ignoreMap":5},[17592],{"type":32,"value":12782},{"type":27,"tag":28,"props":17594,"children":17595},{},[17596],{"type":32,"value":17597},"With Cypress, we want to test this navigation bar, ideally in most effective way. Let’s start with the simplest flow, basically mimicking a real user testing all the links:",{"type":27,"tag":793,"props":17599,"children":17602},{"className":17600,"code":17601,"language":1513,"meta":5},[1510],"it('click all links', () => {\n\n  cy.visit('/')\n\n  // blog page\n  cy.contains('blog').click()\n  cy.location('pathname').should('eq', '/blog')\n  cy.go('back')\n\n  // about page\n  cy.contains('about').click()\n  cy.location('pathname').should('eq', '/about')\n  cy.go('back')\n\n  // contact page\n  cy.contains('contact').click()\n  cy.location('pathname').should('eq', '/contact')\n  cy.go('back')\n\n});\n",[17603],{"type":27,"tag":653,"props":17604,"children":17605},{"__ignoreMap":5},[17606],{"type":32,"value":17601},{"type":27,"tag":28,"props":17608,"children":17609},{},[17610],{"type":32,"value":17611},"With each link we click on a link, check redirect url and then we go back to our home page, and repeat the process. This is of course a little repetetive and you can see it in the code itself. Also, if we were to add another item to our navigation bar, we’d need to repeat all of the actions again in our code.",{"type":27,"tag":28,"props":17613,"children":17614},{},[17615],{"type":32,"value":17616},"Instead of this, we can create a loop and iterate through our items.",{"type":27,"tag":793,"props":17618,"children":17622},{"className":17619,"code":17620,"highlights":17621,"language":1513,"meta":5},[1510],"it('click all links with loop', () => {\n\n  const pages = ['blog', 'about', 'contact']\n\n  cy.visit('/')\n\n  pages.forEach(page => {\n\n    cy.contains(page).click()\n    cy.location('pathname').should('eq', `/${page}`)\n    cy.go('back')\n\n  })\n\n});\n",[1606],[17623],{"type":27,"tag":653,"props":17624,"children":17625},{"__ignoreMap":5},[17626],{"type":32,"value":17620},{"type":27,"tag":28,"props":17628,"children":17629},{},[17630,17632,17638],{"type":32,"value":17631},"In this test, we create an array (line 3) and then we create a ",{"type":27,"tag":653,"props":17633,"children":17635},{"className":17634},[],[17636],{"type":32,"value":17637},"forEach",{"type":32,"value":17639}," loop that will iterate through the array and repeats the action. This is especially useful if for any reason our navigation bar changes items. We’ll just add an item to the array and our test works.",{"type":27,"tag":28,"props":17641,"children":17642},{},[17643,17645,17651,17653,17659,17661,17668],{"type":32,"value":17644},"Although made our test more DRY, there’s a slight problem with this ",{"type":27,"tag":653,"props":17646,"children":17648},{"className":17647},[],[17649],{"type":32,"value":17650},".go('back')",{"type":32,"value":17652}," approach. We are not waiting for our page to fully load. After the assertion on ",{"type":27,"tag":653,"props":17654,"children":17656},{"className":17655},[],[17657],{"type":32,"value":17658},"pathname",{"type":32,"value":17660}," passes, we go straight back. Although the main focus of our test is the navigation bar, we still want to see if the links open the correct page and we are not race conditioning. Gleb Bahmutov ",{"type":27,"tag":172,"props":17662,"children":17665},{"href":17663,"rel":17664},"https://www.cypress.io/blog/2020/08/17/when-can-the-test-navigate/",[696],[17666],{"type":32,"value":17667},"has written a great blog on this topic",{"type":32,"value":17669},". We really don’t want to test too fast, because we might be missing an error.",{"type":27,"tag":28,"props":17671,"children":17672},{},[17673,17675,17681,17683,17688],{"type":32,"value":17674},"And we are! Notice there’s not ",{"type":27,"tag":653,"props":17676,"children":17678},{"className":17677},[],[17679],{"type":32,"value":17680},"about",{"type":32,"value":17682}," page. Clicking on the ",{"type":27,"tag":653,"props":17684,"children":17686},{"className":17685},[],[17687],{"type":32,"value":17680},{"type":32,"value":17689}," link will actually redirect us to a 404 page. Although our navigation bar might seem like it is working, it can in fact contain a broken link. That’s no good.",{"type":27,"tag":28,"props":17691,"children":17692},{},[17693,17695,17700],{"type":32,"value":17694},"We can choose a different approach, and instead of opening our link using ",{"type":27,"tag":653,"props":17696,"children":17698},{"className":17697},[],[17699],{"type":32,"value":12824},{"type":32,"value":17701}," command. This might sound strange - why would you do an API request to a website? But http request is the exact same thing that your browser does when you type in a url. The only difference is, that as a response, you usually get an html document, instead of json object. You will still get a 200 or 404 status code, so the principle is exactly the same. Now, instead of entering the site with our browser, we just make a request to check that the link is acutally live.",{"type":27,"tag":793,"props":17703,"children":17706},{"className":17704,"code":17705,"language":1513,"meta":5},[1510],"it('use requests to navigation bar links', () => {\n\n  const pages = ['blog', 'about', 'contact']\n\n  cy.visit('/')\n\n  pages.forEach(page => {\n\n    cy\n      .contains(page)\n      .then((link) => {\n        cy.request(link.prop('href'))\n      })\n\n  })\n\n});\n",[17707],{"type":27,"tag":653,"props":17708,"children":17709},{"__ignoreMap":5},[17710],{"type":32,"value":17705},{"type":27,"tag":28,"props":17712,"children":17713},{},[17714,17716,17721,17723,17728,17729,17734,17736,17742],{"type":32,"value":17715},"This will help us reveal the error on ",{"type":27,"tag":653,"props":17717,"children":17719},{"className":17718},[],[17720],{"type":32,"value":17680},{"type":32,"value":17722}," page, because we will get a 404 error. As a bonus, you get to test URLs that direct outside your superdomain, ",{"type":27,"tag":14020,"props":17724,"children":17725},{},[17726],{"type":32,"value":17727},"which is a current limitation with Cypress. You cannot visit multiple domains",{"type":32,"value":14042},{"type":27,"tag":172,"props":17730,"children":17732},{"href":14045,"rel":17731},[696],[17733],{"type":32,"value":14049},{"type":32,"value":17735},"), but you can do requests to them and that might be good enough for your use case. I write more about this in my blog on ",{"type":27,"tag":172,"props":17737,"children":17739},{"href":17738},"/opening-a-new-tab-in-cypress",[17740],{"type":32,"value":17741},"how to test tab opening in Cypress",{"type":32,"value":256},{"type":27,"tag":28,"props":17744,"children":17745},{},[17746,17748,17754],{"type":32,"value":17747},"As a last example, let’s say we don’t know for sure how many links does our navbar have. We just want to be absolutely sure that each one of them works. To do that, we can easily just select all ",{"type":27,"tag":653,"props":17749,"children":17751},{"className":17750},[],[17752],{"type":32,"value":17753},"\u003Ca>",{"type":32,"value":17755}," elements and iterate through them:",{"type":27,"tag":793,"props":17757,"children":17760},{"className":17758,"code":17759,"language":1513,"meta":5},[1510],"it('check all links on page', () => {\n\n  cy.visit('/')\n  cy.get('a').each(page => {\n    cy.request(page.prop('href'))\n  })\n\n});\n",[17761],{"type":27,"tag":653,"props":17762,"children":17763},{"__ignoreMap":5},[17764],{"type":32,"value":17759},{"type":27,"tag":28,"props":17766,"children":17767},{},[17768],{"type":32,"value":17769},"This would of course struggle with links that point to email addresses, so in order to skip those, you can change the selector to avoid those:",{"type":27,"tag":793,"props":17771,"children":17775},{"className":17772,"code":17773,"highlights":17774,"language":1513,"meta":5},[1510],"it('check all links to sites', () => {\n\n  cy.visit('/')\n  cy.get(\"a:not([href*='mailto:'])\").each(page => {\n    cy.request(page.prop('href'))\n  })\n\n});\n",[3877],[17776],{"type":27,"tag":653,"props":17777,"children":17778},{"__ignoreMap":5},[17779],{"type":32,"value":17773},{"type":27,"tag":28,"props":17781,"children":17782},{},[17783,17785,17790,17791,17796],{"type":32,"value":17784},"Hope you learned something new. I try to post an article like this every week, so make sure to enter your email in the footer and I will notify you when a new article comes out. You can also follow me on ",{"type":27,"tag":172,"props":17786,"children":17788},{"href":5770,"rel":17787},[696],[17789],{"type":32,"value":1589},{"type":32,"value":4164},{"type":27,"tag":172,"props":17792,"children":17794},{"href":10953,"rel":17793},[696],[17795],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":17798},[],"content:testing-links-with-cypress:index.md","testing-links-with-cypress/index.md","testing-links-with-cypress/index",{"_path":15445,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":17803,"description":17804,"date":17805,"published":10,"slug":17806,"tags":17807,"readingTime":17811,"body":17815,"_type":329,"_id":18346,"_source":331,"_file":18347,"_stem":18348,"_extension":334},"Reading and testing JSON object in Cypress","Ever got that frustrating Cannot read property 'x' of undefined error? If you are starting with JSON objects, it is easy to get lost. In this article I hope to shed some light into how to read and test them with Cypress","2021-04-19","reading-and-testing-json-object-in-cypress",[5279,1004,17808,17809,17810],"data","fixtures","object",{"text":4032,"minutes":17812,"time":17813,"words":17814},5.68,340800,1136,{"type":24,"children":17816,"toc":18339},[17817,17822,17828,17833,17843,17863,17872,17885,17894,17899,17908,17922,17928,17941,17951,17956,17965,17978,17991,18000,18012,18021,18035,18041,18046,18055,18060,18069,18089,18098,18103,18111,18117,18130,18139,18144,18152,18157,18205,18211,18216,18221,18230,18243,18252,18257,18262,18291,18303,18322],{"type":27,"tag":28,"props":17818,"children":17819},{},[17820],{"type":32,"value":17821},"You can start learning Cypress with fairly little knowledge of JavaScript. At least, that was my experience. The first roadblock I hit when I started learning was understanding how to access data in a JSON formatted response. This article is for everyone at the same point of the journey. I hope this will help you get over the roadblock.",{"type":27,"tag":45,"props":17823,"children":17825},{"id":17824},"accessing-items-in-object",[17826],{"type":32,"value":17827},"Accessing items in object",{"type":27,"tag":28,"props":17829,"children":17830},{},[17831],{"type":32,"value":17832},"Let’s start simple, before we dive into the whole JSON. Let’s say we have a super-simple JSON object, that contains just a couple of keys:",{"type":27,"tag":793,"props":17834,"children":17838},{"className":17835,"code":17836,"filename":17837,"language":1004,"meta":5},[1002],"{\n  \"color\": \"red\",\n  \"id\": 4,\n  \"available\": false\n}\n","fixtures/cars.json",[17839],{"type":27,"tag":653,"props":17840,"children":17841},{"__ignoreMap":5},[17842],{"type":32,"value":17836},{"type":27,"tag":28,"props":17844,"children":17845},{},[17846,17848,17854,17856,17861],{"type":32,"value":17847},"To test such an object we can write a following test. Of course, in real world we would probably not test a fixture, but to demonstrate the point I like to keep things simple. Instead of ",{"type":27,"tag":653,"props":17849,"children":17851},{"className":17850},[],[17852],{"type":32,"value":17853},".fixture()",{"type":32,"value":17855}," you can imagine a ",{"type":27,"tag":653,"props":17857,"children":17859},{"className":17858},[],[17860],{"type":32,"value":15586},{"type":32,"value":17862}," command that intercepts a network call.",{"type":27,"tag":793,"props":17864,"children":17867},{"className":17865,"code":17866,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then( car => {\n    expect(car.color).to.eq(\"red\")\n    expect(car.id).to.eq(4)\n    expect(car.available).to.eq(false)\n  })\n",[17868],{"type":27,"tag":653,"props":17869,"children":17870},{"__ignoreMap":5},[17871],{"type":32,"value":17866},{"type":27,"tag":28,"props":17873,"children":17874},{},[17875,17877,17883],{"type":32,"value":17876},"It does not matter what kind of value our object attribute has, we can access it using so-called dot notation. This is the ",{"type":27,"tag":653,"props":17878,"children":17880},{"className":17879},[],[17881],{"type":32,"value":17882},"objectname.attribute",{"type":32,"value":17884}," notation, separated by a dot, hence the name. We can use a so called bracket notation, which does exactly the same thing, but with slightly different syntax:",{"type":27,"tag":793,"props":17886,"children":17889},{"className":17887,"code":17888,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then( car => {\n    expect(car['color']).to.eq(\"red\")\n    expect(car['id']).to.eq(4)\n    expect(car['available']).to.eq(false)\n  })\n",[17890],{"type":27,"tag":653,"props":17891,"children":17892},{"__ignoreMap":5},[17893],{"type":32,"value":17888},{"type":27,"tag":28,"props":17895,"children":17896},{},[17897],{"type":32,"value":17898},"You might ask why would anyone choose this syntax over the dot notation which looks much more polished, especially if we were to access an attribute that’s few levels deep. With bracket syntax, you can actually pass a variable, so you can write something like this:",{"type":27,"tag":793,"props":17900,"children":17903},{"className":17901,"code":17902,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then(car => {\n\n    const attr = 'color'\n    expect(car[attr]).to.eq(\"red\")\n\n  })\n",[17904],{"type":27,"tag":653,"props":17905,"children":17906},{"__ignoreMap":5},[17907],{"type":32,"value":17902},{"type":27,"tag":28,"props":17909,"children":17910},{},[17911,17913,17920],{"type":32,"value":17912},"While we are at it, I suggest you look into ",{"type":27,"tag":17914,"props":17915,"children":17917},"nuxt-link",{"to":17916},"using-destructuring-in-cypress",[17918],{"type":32,"value":17919},"my blog on destructuring",{"type":32,"value":17921},", which has some more tips on how to access properties inside a JSON object.",{"type":27,"tag":45,"props":17923,"children":17925},{"id":17924},"accessing-items-in-array",[17926],{"type":32,"value":17927},"Accessing items in array",{"type":27,"tag":28,"props":17929,"children":17930},{},[17931,17933,17939],{"type":32,"value":17932},"Let’s now add another item in our ",{"type":27,"tag":653,"props":17934,"children":17936},{"className":17935},[],[17937],{"type":32,"value":17938},"car.json",{"type":32,"value":17940}," fixture. This will be a list of feature formatted as an array:",{"type":27,"tag":793,"props":17942,"children":17946},{"className":17943,"code":17944,"highlights":17945,"language":1004,"meta":5},[1002],"{\n  \"color\": \"red\",\n  \"id\": 4,\n  \"available\": false,\n  \"features\": [\"speed limiter\", \"panoramic windshield\", \"automatic transmission\"]\n}\n",[3667],[17947],{"type":27,"tag":653,"props":17948,"children":17949},{"__ignoreMap":5},[17950],{"type":32,"value":17944},{"type":27,"tag":28,"props":17952,"children":17953},{},[17954],{"type":32,"value":17955},"Let’s say we’d like to write a test for couple of these feature items. That test would look something like this.",{"type":27,"tag":793,"props":17957,"children":17960},{"className":17958,"code":17959,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then( car => {\n    expect(car.features[0]).to.eq('speed limiter')\n    expect(car.features[1]).to.eq('panoramic windshield')\n    expect(car.features[2]).to.eq('automatic transmission')\n  })\n",[17961],{"type":27,"tag":653,"props":17962,"children":17963},{"__ignoreMap":5},[17964],{"type":32,"value":17959},{"type":27,"tag":28,"props":17966,"children":17967},{},[17968,17970,17976],{"type":32,"value":17969},"Notice how we use a similar style as the mentioned bracket notation. This is how you access items in and array. However, there is no dot notation for arrays, so attemting to write something like ",{"type":27,"tag":653,"props":17971,"children":17973},{"className":17972},[],[17974],{"type":32,"value":17975},"features.1",{"type":32,"value":17977}," would result in an error.",{"type":27,"tag":28,"props":17979,"children":17980},{},[17981,17983,17989],{"type":32,"value":17982},"Arrays are widely used and can sometimes contain a lot of information. If you want to test an array for just a single item, you can use ",{"type":27,"tag":653,"props":17984,"children":17986},{"className":17985},[],[17987],{"type":32,"value":17988},"include",{"type":32,"value":17990}," assertion:",{"type":27,"tag":793,"props":17992,"children":17995},{"className":17993,"code":17994,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then(car => {\n    expect(car.features).to.include('speed limiter')\n  })\n",[17996],{"type":27,"tag":653,"props":17997,"children":17998},{"__ignoreMap":5},[17999],{"type":32,"value":17994},{"type":27,"tag":28,"props":18001,"children":18002},{},[18003,18005,18010],{"type":32,"value":18004},"Fun fact - strings in JavaScript can behave like arrays. That means, we can access each letter using bracket notation, and we can use the same ",{"type":27,"tag":653,"props":18006,"children":18008},{"className":18007},[],[18009],{"type":32,"value":17988},{"type":32,"value":18011}," assertion as we did in previous example:",{"type":27,"tag":793,"props":18013,"children":18016},{"className":18014,"code":18015,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then(car => {\n    expect(car.color[0]).to.eq('r') // access first letter in \"red\"\n    expect(car.color).to.include('ed')\n  })\n",[18017],{"type":27,"tag":653,"props":18018,"children":18019},{"__ignoreMap":5},[18020],{"type":32,"value":18015},{"type":27,"tag":28,"props":18022,"children":18023},{},[18024,18026,18033],{"type":32,"value":18025},"There are tons of different assertions you can use with arrays, and I suggest you ",{"type":27,"tag":172,"props":18027,"children":18030},{"href":18028,"rel":18029},"https://docs.cypress.io/guides/references/assertions",[696],[18031],{"type":32,"value":18032},"check out the documentation",{"type":32,"value":18034}," to learn more.",{"type":27,"tag":45,"props":18036,"children":18038},{"id":18037},"array-of-objects",[18039],{"type":32,"value":18040},"Array of objects",{"type":27,"tag":28,"props":18042,"children":18043},{},[18044],{"type":32,"value":18045},"Let’s now combine arrays and object. Our car fixture will now have multiple objects inside it - a common situation when working with api that returns a list of items. Our JSON file will look something like this:",{"type":27,"tag":793,"props":18047,"children":18050},{"className":18048,"code":18049,"filename":17837,"language":1004,"meta":5},[1002],"[\n  {\n    \"color\": \"red\",\n    \"id\": 4,\n    \"available\": false,\n    \"features\": [\"speed limiter\", \"panoramic windshield\", \"automatic transmission\"]\n  },\n  {\n    \"color\": \"blue\",\n    \"id\": 7,\n    \"available\": true,\n    \"features\": [\"speed limiter\", \"automatic transmission\"]\n  }\n]\n",[18051],{"type":27,"tag":653,"props":18052,"children":18053},{"__ignoreMap":5},[18054],{"type":32,"value":18049},{"type":27,"tag":28,"props":18056,"children":18057},{},[18058],{"type":32,"value":18059},"To test e.g. the color of our second item, we will to combine two mentioned approaches - referencing array item & referencing object key:",{"type":27,"tag":793,"props":18061,"children":18064},{"className":18062,"code":18063,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then(cars => {\n    expect(cars[1].color).to.eq('blue')\n  })\n",[18065],{"type":27,"tag":653,"props":18066,"children":18067},{"__ignoreMap":5},[18068],{"type":32,"value":18063},{"type":27,"tag":28,"props":18070,"children":18071},{},[18072,18073,18079,18081,18087],{"type":32,"value":7021},{"type":27,"tag":653,"props":18074,"children":18076},{"className":18075},[],[18077],{"type":32,"value":18078},"cars[1]",{"type":32,"value":18080},", we are selecting the second item from our array of objects, and within that object, we are using ",{"type":27,"tag":653,"props":18082,"children":18084},{"className":18083},[],[18085],{"type":32,"value":18086},".color",{"type":32,"value":18088}," to select the proper value from that object. In other words, we are travelling through the JSON structure. We could of course also use bracket notation and write the same thing like this:",{"type":27,"tag":793,"props":18090,"children":18093},{"className":18091,"code":18092,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then(cars => {\n    expect(car[1]['color']).to.eq('blue')\n  })\n",[18094],{"type":27,"tag":653,"props":18095,"children":18096},{"__ignoreMap":5},[18097],{"type":32,"value":18092},{"type":27,"tag":28,"props":18099,"children":18100},{},[18101],{"type":32,"value":18102},"These combinations can be overwhelming at first, but they become natural after a while. Chrome DevTools can be a great help here. Right click on a JSON object can trigger a menu from where you can copy an attribute path to a given value. This works across many places in Chrome DevTools.",{"type":27,"tag":28,"props":18104,"children":18105},{},[18106],{"type":27,"tag":959,"props":18107,"children":18110},{"alt":18108,"src":18109},"Copy property path in DevTools","devtools.png",[],{"type":27,"tag":45,"props":18112,"children":18114},{"id":18113},"common-error-1-comparing-arrays",[18115],{"type":32,"value":18116},"Common error #1 - Comparing arrays",{"type":27,"tag":28,"props":18118,"children":18119},{},[18120,18122,18128],{"type":32,"value":18121},"While trying to find your way around JSON objects, you might come across different errors. For example, there’s this strange behavior when you try to compare our ",{"type":27,"tag":653,"props":18123,"children":18125},{"className":18124},[],[18126],{"type":32,"value":18127},"car.features",{"type":32,"value":18129}," array to another array. Let’s say we have an assertion like this:",{"type":27,"tag":793,"props":18131,"children":18134},{"className":18132,"code":18133,"language":1513,"meta":5},[1510],"cy\n  .fixture('cars')\n  .then(cars => {\n    expect(cars[0].features).to.eq([\"speed limiter\", \"panoramic windshield\", \"automatic transmission\"])\n  })\n",[18135],{"type":27,"tag":653,"props":18136,"children":18137},{"__ignoreMap":5},[18138],{"type":32,"value":18133},{"type":27,"tag":28,"props":18140,"children":18141},{},[18142],{"type":32,"value":18143},"Although it looks like it should definitely pass, our test will fail. What’s even weirder, is the error in the console:",{"type":27,"tag":28,"props":18145,"children":18146},{},[18147],{"type":27,"tag":959,"props":18148,"children":18151},{"alt":18149,"src":18150},"Equal does not work","equal.png",[],{"type":27,"tag":28,"props":18153,"children":18154},{},[18155],{"type":32,"value":18156},"Everything looks the same, so why does the test fail?",{"type":27,"tag":28,"props":18158,"children":18159},{},[18160,18162,18168,18170,18175,18177,18183,18184,18190,18192,18198,18199,18204],{"type":32,"value":18161},"The reason why this is happening is because of how comparison is made in JavaScript. It is very similar to the reason why ",{"type":27,"tag":653,"props":18163,"children":18165},{"className":18164},[],[18166],{"type":32,"value":18167},"'1' == 1",{"type":32,"value":18169}," will return ",{"type":27,"tag":653,"props":18171,"children":18173},{"className":18172},[],[18174],{"type":32,"value":15059},{"type":32,"value":18176},", but ",{"type":27,"tag":653,"props":18178,"children":18180},{"className":18179},[],[18181],{"type":32,"value":18182},"'1' === 1",{"type":32,"value":18169},{"type":27,"tag":653,"props":18185,"children":18187},{"className":18186},[],[18188],{"type":32,"value":18189},"false",{"type":32,"value":18191},". The difference is connected to how strict that comparison is. When comparing two arrays, we need to use ",{"type":27,"tag":653,"props":18193,"children":18195},{"className":18194},[],[18196],{"type":32,"value":18197},"deep.eq",{"type":32,"value":7446},{"type":27,"tag":653,"props":18200,"children":18202},{"className":18201},[],[18203],{"type":32,"value":11421},{"type":32,"value":14411},{"type":27,"tag":45,"props":18206,"children":18208},{"id":18207},"common-error-2-cannot-read-property-x-of-undefined",[18209],{"type":32,"value":18210},"Common error #2 - Cannot read property 'x' of undefined",{"type":27,"tag":28,"props":18212,"children":18213},{},[18214],{"type":32,"value":18215},"Mistakes happen while figuring out the proper path to a property. You may have already encountered an error similar to the one mentioned in subheading. This error can get super confusing and does not tell us a whole lot about what is the problem.",{"type":27,"tag":28,"props":18217,"children":18218},{},[18219],{"type":32,"value":18220},"To dive deeper into the problem, let’s expand our JSON file with some extra attributes:",{"type":27,"tag":793,"props":18222,"children":18225},{"className":18223,"code":18224,"filename":17837,"language":1004,"meta":5},[1002],"[\n  {\n    \"color\": \"red\",\n    \"id\": 4,\n    \"available\": false,\n    \"features\": [\"speed limiter\", \"panoramic windshield\", \"automatic transmission\"],\n    \"engine\": {\n      \"horsepower\": 134,\n      \"rpm\": 6000,\n      \"fueling system\": \"turbo/GDI\"\n    }\n  },\n  {\n    \"color\": \"blue\",\n    \"id\": 7,\n    \"available\": true,\n    \"features\": [\"speed limiter\", \"automatic transmission\"],\n    \"engine\": {\n      \"horsepower\": 175,\n      \"rpm\": 6000,\n      \"fueling system\": \"MPI\"\n    }\n  }\n]\n",[18226],{"type":27,"tag":653,"props":18227,"children":18228},{"__ignoreMap":5},[18229],{"type":32,"value":18224},{"type":27,"tag":28,"props":18231,"children":18232},{},[18233,18235,18241],{"type":32,"value":18234},"We’ve added some ",{"type":27,"tag":653,"props":18236,"children":18238},{"className":18237},[],[18239],{"type":32,"value":18240},"engine",{"type":32,"value":18242}," attributes, so let’s test them with following test:",{"type":27,"tag":793,"props":18244,"children":18247},{"className":18245,"code":18246,"language":1513,"meta":5},[1510],"cy\n  .fixture('car')\n  .then(cars => {\n    expect(cars[0].engines.horsepower).to.eq(134)\n  })\n",[18248],{"type":27,"tag":653,"props":18249,"children":18250},{"__ignoreMap":5},[18251],{"type":32,"value":18246},{"type":27,"tag":28,"props":18253,"children":18254},{},[18255],{"type":32,"value":18256},"This test will fail with a following error:",{"type":27,"tag":28,"props":18258,"children":18259},{},[18260],{"type":32,"value":18261},"![Cannot read property 'x' of undefined](cannot-read-property-x-of-undefined.png\" shadow=\"shadow-md)",{"type":27,"tag":28,"props":18263,"children":18264},{},[18265,18267,18273,18275,18281,18283,18289],{"type":32,"value":18266},"The reason why this test fails is that the ",{"type":27,"tag":653,"props":18268,"children":18270},{"className":18269},[],[18271],{"type":32,"value":18272},"horsepower",{"type":32,"value":18274}," key cannot be found. The ",{"type":27,"tag":653,"props":18276,"children":18278},{"className":18277},[],[18279],{"type":32,"value":18280},"of undefined",{"type":32,"value":18282}," part points us closer to the reason. It seems that key that should be inside ",{"type":27,"tag":653,"props":18284,"children":18286},{"className":18285},[],[18287],{"type":32,"value":18288},"engines",{"type":32,"value":18290}," attribute is not defined.",{"type":27,"tag":28,"props":18292,"children":18293},{},[18294,18296,18301],{"type":32,"value":18295},"What I usually do in these types of cases is that instead of the whole path, I try to ",{"type":27,"tag":653,"props":18297,"children":18299},{"className":18298},[],[18300],{"type":32,"value":5487},{"type":32,"value":18302}," out the parent attribute and then continue up with each parent until I find something that can help me find a way.",{"type":27,"tag":28,"props":18304,"children":18305},{},[18306,18308,18313,18315,18320],{"type":32,"value":18307},"If I do it in this case, I quickly realize that I made a typo and instead of ",{"type":27,"tag":653,"props":18309,"children":18311},{"className":18310},[],[18312],{"type":32,"value":18288},{"type":32,"value":18314}," I should have typed ",{"type":27,"tag":653,"props":18316,"children":18318},{"className":18317},[],[18319],{"type":32,"value":18240},{"type":32,"value":18321},". This seems like an obvious mistake in this example. But things can get pretty tricky with huge payloads that contain multiple levels of information.",{"type":27,"tag":28,"props":18323,"children":18324},{},[18325,18327,18332,18333,18338],{"type":32,"value":18326},"I hope this will help you with working with JSON objects. I write a blog like this every week, so if you’d like to get notified, signup for the newlsetter (form is in footer) and follow me on ",{"type":27,"tag":172,"props":18328,"children":18330},{"href":5770,"rel":18329},[696],[18331],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":18334,"children":18336},{"href":10953,"rel":18335},[696],[18337],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":18340},[18341,18342,18343,18344,18345],{"id":17824,"depth":320,"text":17827},{"id":17924,"depth":320,"text":17927},{"id":18037,"depth":320,"text":18040},{"id":18113,"depth":320,"text":18116},{"id":18207,"depth":320,"text":18210},"content:reading-and-testing-json-object-in-cypress:index.md","reading-and-testing-json-object-in-cypress/index.md","reading-and-testing-json-object-in-cypress/index",{"_path":18350,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":18351,"description":18352,"date":18353,"published":10,"slug":18354,"tags":18355,"cypressVersion":5959,"readingTime":18358,"body":18362,"_type":329,"_id":18658,"_source":331,"_file":18659,"_stem":18660,"_extension":334},"/cypress-basics-check-attributes-value-and-text","Cypress basics: Check attributes, value and text","Short explanation of how to test and access different properties of a given element using .invok() function","2021-04-12","cypress-basics-check-attributes-value-and-text",[5279,13407,18356,18357,32],"attributes","value",{"text":3229,"minutes":18359,"time":18360,"words":18361},2.855,171300,571,{"type":24,"children":18363,"toc":18653},[18364,18374,18380,18385,18394,18399,18408,18438,18444,18456,18465,18477,18486,18491,18500,18506,18519,18527,18540,18549,18561,18570,18575,18584,18606,18627,18636],{"type":27,"tag":1029,"props":18365,"children":18366},{},[18367,18371],{"type":27,"tag":28,"props":18368,"children":18369},{},[18370],{"type":32,"value":5973},{"type":27,"tag":5975,"props":18372,"children":18373},{},[],{"type":27,"tag":45,"props":18375,"children":18377},{"id":18376},"get-element-text",[18378],{"type":32,"value":18379},"Get element text",{"type":27,"tag":28,"props":18381,"children":18382},{},[18383],{"type":32,"value":18384},"To get proper attributes of an element, it’s good to understand some basics of different HTML elements. Let me give you an example. Let’s say we have two elements:",{"type":27,"tag":793,"props":18386,"children":18389},{"className":18387,"code":18388,"language":7826,"meta":5},[7824],"\u003Cdiv>Please type in your name:\u003C/div>\n\u003Cinput type=\"text\">\u003C/input>\n",[18390],{"type":27,"tag":653,"props":18391,"children":18392},{"__ignoreMap":5},[18393],{"type":32,"value":18388},{"type":27,"tag":28,"props":18395,"children":18396},{},[18397],{"type":32,"value":18398},"During my test, I’m going to fill the input field and then check if the text has correct text inside. With both of these elements, you can see the text on page. But if I want to \"check text\" on these elements, I need to use slightly different approach with each:",{"type":27,"tag":793,"props":18400,"children":18403},{"className":18401,"code":18402,"language":1513,"meta":5},[1510],"cy\n  .get('div')\n  .should('have.text', 'Please type in your name:')\n\ncy\n  .get('input')\n  .type('Rick Sanchez')\n  .should('have.value', 'Rick Sanchez')\n",[18404],{"type":27,"tag":653,"props":18405,"children":18406},{"__ignoreMap":5},[18407],{"type":32,"value":18402},{"type":27,"tag":28,"props":18409,"children":18410},{},[18411,18413,18419,18421,18427,18429,18436],{"type":32,"value":18412},"The difference here is, that our ",{"type":27,"tag":653,"props":18414,"children":18416},{"className":18415},[],[18417],{"type":32,"value":18418},"div",{"type":32,"value":18420}," element contains a certain text, but ",{"type":27,"tag":653,"props":18422,"children":18424},{"className":18423},[],[18425],{"type":32,"value":18426},"input",{"type":32,"value":18428}," elements in HTML are used for inserting value. I strongly suggest checking out ",{"type":27,"tag":172,"props":18430,"children":18433},{"href":18431,"rel":18432},"https://www.w3schools.com/html/html_form_input_types.asp",[696],[18434],{"type":32,"value":18435},"W3Schools docs",{"type":32,"value":18437}," to explore different types of input form fields.",{"type":27,"tag":45,"props":18439,"children":18441},{"id":18440},"get-attribute",[18442],{"type":32,"value":18443},"Get attribute",{"type":27,"tag":28,"props":18445,"children":18446},{},[18447,18449,18454],{"type":32,"value":18448},"You may be in a situation where you need to check your links. In that case, getting your ",{"type":27,"tag":653,"props":18450,"children":18452},{"className":18451},[],[18453],{"type":32,"value":17560},{"type":32,"value":18455}," attribute from anchor element would be useful. Let’s say we have a following link:",{"type":27,"tag":793,"props":18457,"children":18460},{"className":18458,"code":18459,"language":7826,"meta":5},[7824],"\u003Ca href=\"https://docs.cypress.io\">Read the docs!\u003C/a>\n",[18461],{"type":27,"tag":653,"props":18462,"children":18463},{"__ignoreMap":5},[18464],{"type":32,"value":18459},{"type":27,"tag":28,"props":18466,"children":18467},{},[18468,18470,18475],{"type":32,"value":18469},"To check the ",{"type":27,"tag":653,"props":18471,"children":18473},{"className":18472},[],[18474],{"type":32,"value":17560},{"type":32,"value":18476}," attribute, you can write a test like this:",{"type":27,"tag":793,"props":18478,"children":18481},{"className":18479,"code":18480,"language":1513,"meta":5},[1510],"cy\n  .get('a')\n  .invoke('attr', 'href')\n  .should('eq', 'https://docs.cypress.io')\n",[18482],{"type":27,"tag":653,"props":18483,"children":18484},{"__ignoreMap":5},[18485],{"type":32,"value":18480},{"type":27,"tag":28,"props":18487,"children":18488},{},[18489],{"type":32,"value":18490},"In addition, you can test if the link is actually valid, by making an http request to it:",{"type":27,"tag":793,"props":18492,"children":18495},{"className":18493,"code":18494,"language":1513,"meta":5},[1510],"cy\n  .get('a')\n  .invoke('attr', 'href')\n  .then(href => {\n\n    cy\n      .request(href)\n      .its('status')\n      .should('eq', 200);\n\n});\n",[18496],{"type":27,"tag":653,"props":18497,"children":18498},{"__ignoreMap":5},[18499],{"type":32,"value":18494},{"type":27,"tag":45,"props":18501,"children":18503},{"id":18502},"invoke-properties",[18504],{"type":32,"value":18505},"Invoke properties",{"type":27,"tag":28,"props":18507,"children":18508},{},[18509,18511,18517],{"type":32,"value":18510},"By using ",{"type":27,"tag":653,"props":18512,"children":18514},{"className":18513},[],[18515],{"type":32,"value":18516},".invoke('prop')",{"type":32,"value":18518},", you can access many different properties from selected element. The whole list of that properties can be found in Chrome DevTools. To access them, click on the given element and open properties panel.",{"type":27,"tag":28,"props":18520,"children":18521},{},[18522],{"type":27,"tag":959,"props":18523,"children":18526},{"alt":18524,"src":18525},"Element properties in Chrome DevTools","chrome-props.mp4",[],{"type":27,"tag":28,"props":18528,"children":18529},{},[18530,18532,18538],{"type":32,"value":18531},"As you can see, there are tons of options. For example, we can use ",{"type":27,"tag":653,"props":18533,"children":18535},{"className":18534},[],[18536],{"type":32,"value":18537},".invoke()",{"type":32,"value":18539}," command to look into whether checkbox element is checked.",{"type":27,"tag":793,"props":18541,"children":18544},{"className":18542,"code":18543,"language":1513,"meta":5},[1510],"cy\n  .get('input')\n  .invoke('prop', 'checked')\n  .then(state => {\n\n    console.log(`checkbox is ${state ? 'checked' : 'not checked'}`)\n\n  });\n",[18545],{"type":27,"tag":653,"props":18546,"children":18547},{"__ignoreMap":5},[18548],{"type":32,"value":18543},{"type":27,"tag":28,"props":18550,"children":18551},{},[18552,18554,18559],{"type":32,"value":18553},"Remember how we tested the value of a certain input? With ",{"type":27,"tag":653,"props":18555,"children":18557},{"className":18556},[],[18558],{"type":32,"value":18537},{"type":32,"value":18560}," we can pass the value of that input to another function, like this:",{"type":27,"tag":793,"props":18562,"children":18565},{"className":18563,"code":18564,"language":1513,"meta":5},[1510],"cy\n  .get('input')\n  .type('Rick Sanchez')\n  .invoke('val')\n  .then(val => {\n\n    const inputValue = val;\n\n  });\n",[18566],{"type":27,"tag":653,"props":18567,"children":18568},{"__ignoreMap":5},[18569],{"type":32,"value":18564},{"type":27,"tag":28,"props":18571,"children":18572},{},[18573],{"type":32,"value":18574},"In the past, I had a bad input element in my app that would re-render during my test and delete my input in the test. I would write a special \"type and check\" command that would retry if the input would not work properly.",{"type":27,"tag":793,"props":18576,"children":18579},{"className":18577,"code":18578,"language":1513,"meta":5},[1510],"Cypress.Commands.add('typeAndCheck', { prevSubject: true }, (subject, input) => {\n\n  cy\n    .wrap(subject)\n    .type(input);\n\n  cy\n    .wrap(subject)\n    .then(($subj) => {\n      if ($subj[0].value !== input) {\n\n        cy\n          .wrap(subject)\n          .clear({ force: true })\n          .typeAndCheck(input);\n\n      }\n    });\n\n});\n",[18580],{"type":27,"tag":653,"props":18581,"children":18582},{"__ignoreMap":5},[18583],{"type":32,"value":18578},{"type":27,"tag":28,"props":18585,"children":18586},{},[18587,18589,18596,18598,18605],{"type":32,"value":18588},"This is definitely very hacky solution. I’d recomment checking out this great blog on ",{"type":27,"tag":172,"props":18590,"children":18593},{"href":18591,"rel":18592},"https://codingitwrong.com/2020/10/09/identifying-code-smells-in-cypress.html",[696],[18594],{"type":32,"value":18595},"identifying code smells",{"type":32,"value":18597}," (as the described situation is definitely one!) or looking into Gleb Bahmutov’s blog about the topic of ",{"type":27,"tag":172,"props":18599,"children":18602},{"href":18600,"rel":18601},"https://www.cypress.io/blog/2018/02/05/when-can-the-test-start/",[696],[18603],{"type":32,"value":18604},"when can a test start typing",{"type":32,"value":256},{"type":27,"tag":28,"props":18607,"children":18608},{},[18609,18611,18616,18618,18625],{"type":32,"value":18610},"The other interesting thing about ",{"type":27,"tag":653,"props":18612,"children":18614},{"className":18613},[],[18615],{"type":32,"value":18537},{"type":32,"value":18617}," val is that by passing a second argument to this function will enable you to change the value and (kinda) simulate pasting a text to an textarea. I wrote about this in a ",{"type":27,"tag":172,"props":18619,"children":18622},{"href":18620,"rel":18621},"https://egghead.io/blog/handling-copy-and-paste-in-cypress",[696],[18623],{"type":32,"value":18624},"recent blog for egghead.io",{"type":32,"value":18626},", so give that a read. The simple example goes something like this:",{"type":27,"tag":793,"props":18628,"children":18631},{"className":18629,"code":18630,"language":1513,"meta":5},[1510],"cy\n  .get(\"input\")\n  .invoke('val', 'paste this text')\n",[18632],{"type":27,"tag":653,"props":18633,"children":18634},{"__ignoreMap":5},[18635],{"type":32,"value":18630},{"type":27,"tag":28,"props":18637,"children":18638},{},[18639,18641,18646,18647,18652],{"type":32,"value":18640},"Hope you like this. If you did, you can subscribe to a newsletter to get notified about a new article every week. You can also follow me on ",{"type":27,"tag":172,"props":18642,"children":18644},{"href":5770,"rel":18643},[696],[18645],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":18648,"children":18650},{"href":10953,"rel":18649},[696],[18651],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":18654},[18655,18656,18657],{"id":18376,"depth":320,"text":18379},{"id":18440,"depth":320,"text":18443},{"id":18502,"depth":320,"text":18505},"content:cypress-basics-check-attributes-value-and-text:index.md","cypress-basics-check-attributes-value-and-text/index.md","cypress-basics-check-attributes-value-and-text/index",{"_path":16877,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":18662,"description":18663,"date":18664,"published":10,"slug":18665,"tags":18666,"readingTime":18669,"body":18673,"_type":329,"_id":18990,"_source":331,"_file":18991,"_stem":18992,"_extension":334},".contains() - an overlooked gem in Cypress","Although the name of this command sounds like an assertion, it is actually a selecting command. Let’s look into what makes this command great.","2021-04-05","contains-an-overlooked-gem-in-cypress",[5279,8651,18667,32,18668],"contains","assertions",{"text":3229,"minutes":18670,"time":18671,"words":18672},2.63,157800,526,{"type":24,"children":18674,"toc":18985},[18675,18685,18704,18709,18714,18723,18729,18734,18743,18755,18763,18790,18796,18815,18824,18860,18872,18881,18891,18903,18909,18914,18923,18928,18933,18942,18968],{"type":27,"tag":28,"props":18676,"children":18677},{},[18678,18683],{"type":27,"tag":653,"props":18679,"children":18681},{"className":18680},[],[18682],{"type":32,"value":13507},{"type":32,"value":18684}," is one of my favorite commands in Cypress. Although the name of this command sounds like an assertion, it is actually a selecting command. You could argue that all selecting commands are also assertions of element existence, but let’s not get too philosophical here 😃",{"type":27,"tag":28,"props":18686,"children":18687},{},[18688,18690,18695,18697,18702],{"type":32,"value":18689},"It is important though, to make this distinction. The slightly confusing naming of ",{"type":27,"tag":653,"props":18691,"children":18693},{"className":18692},[],[18694],{"type":32,"value":13507},{"type":32,"value":18696}," command may cause overlooking its powers. Part of eternal struggles in testing is to find a suitable selector, while keeping your test easy to read. ",{"type":27,"tag":653,"props":18698,"children":18700},{"className":18699},[],[18701],{"type":32,"value":13507},{"type":32,"value":18703}," relies on selecting an element by text, but it can do much more than that.",{"type":27,"tag":28,"props":18705,"children":18706},{},[18707],{"type":32,"value":18708},"Let’s look into what makes this command great. I use some examples which can be found in a repo on my GitHub.",{"type":27,"tag":28,"props":18710,"children":18711},{},[18712],{"type":32,"value":18713},"The app actually has a pretty simple structure:",{"type":27,"tag":793,"props":18715,"children":18718},{"className":18716,"code":18717,"language":7826,"meta":5},[7824],"\u003Ch1>Apples and other fruits\u003C/h1>\n\u003Cul>\n  \u003Cli>Apple 🍏\u003C/li>\n  \u003Cli>Pear 🍐\u003C/li>\n\u003Cul>\n",[18719],{"type":27,"tag":653,"props":18720,"children":18721},{"__ignoreMap":5},[18722],{"type":32,"value":18717},{"type":27,"tag":45,"props":18724,"children":18726},{"id":18725},"simple-usage-select-element-containing-a-text",[18727],{"type":32,"value":18728},"Simple usage - select element containing a text",{"type":27,"tag":28,"props":18730,"children":18731},{},[18732],{"type":32,"value":18733},"If you are familiar with this command, you probably already know that it helps you select an element using a text:",{"type":27,"tag":793,"props":18735,"children":18738},{"className":18736,"code":18737,"language":3520,"meta":5},[3517],"cy\n  .contains('Apples')\n",[18739],{"type":27,"tag":653,"props":18740,"children":18741},{"__ignoreMap":5},[18742],{"type":32,"value":18737},{"type":27,"tag":28,"props":18744,"children":18745},{},[18746,18748,18753],{"type":32,"value":18747},"Plain and simple. You know what this will do. It will select our heading. Notice how we don’t even need to write the whole text, just ",{"type":27,"tag":653,"props":18749,"children":18751},{"className":18750},[],[18752],{"type":32,"value":13663},{"type":32,"value":18754}," is good enough.",{"type":27,"tag":28,"props":18756,"children":18757},{},[18758],{"type":27,"tag":959,"props":18759,"children":18762},{"alt":18760,"src":18761},"Selecting by text","simple.png",[],{"type":27,"tag":28,"props":18764,"children":18765},{},[18766,18768,18774,18776,18781,18783,18788],{"type":32,"value":18767},"Bear in mind, that if I just used ",{"type":27,"tag":653,"props":18769,"children":18771},{"className":18770},[],[18772],{"type":32,"value":18773},"Apple",{"type":32,"value":18775}," as a text, the result would be different, since ",{"type":27,"tag":653,"props":18777,"children":18779},{"className":18778},[],[18780],{"type":32,"value":18773},{"type":32,"value":18782}," appears twice on our page. By default ",{"type":27,"tag":653,"props":18784,"children":18786},{"className":18785},[],[18787],{"type":32,"value":13507},{"type":32,"value":18789}," will search the whole DOM, and return the first element with matching parameters. If you have multiple elements with the same text, you will need to scope the search,",{"type":27,"tag":45,"props":18791,"children":18793},{"id":18792},"scoping-the-search",[18794],{"type":32,"value":18795},"Scoping the search",{"type":27,"tag":28,"props":18797,"children":18798},{},[18799,18801,18806,18808,18813],{"type":32,"value":18800},"On my workshops, I like to explain the difference between child, parent and dual commands with the ",{"type":27,"tag":653,"props":18802,"children":18804},{"className":18803},[],[18805],{"type":32,"value":13507},{"type":32,"value":18807}," command. It is a great example of a dual command. ",{"type":27,"tag":653,"props":18809,"children":18811},{"className":18810},[],[18812],{"type":32,"value":13507},{"type":32,"value":18814}," will search within the scope of a previous command if there is one.",{"type":27,"tag":793,"props":18816,"children":18819},{"className":18817,"code":18818,"language":3520,"meta":5},[3517],"cy\n  .contains('Apple') // will select heading\n\ncy\n  .get('li')\n  .contains('Apple') // will select the \"Apple 🍏\" element\n",[18820],{"type":27,"tag":653,"props":18821,"children":18822},{"__ignoreMap":5},[18823],{"type":32,"value":18818},{"type":27,"tag":28,"props":18825,"children":18826},{},[18827,18829,18835,18837,18843,18845,18850,18852,18859],{"type":32,"value":18828},"This command helps you find the right element, so if you have a ",{"type":27,"tag":653,"props":18830,"children":18832},{"className":18831},[],[18833],{"type":32,"value":18834},"\u003Cbutton>",{"type":32,"value":18836},", where text is inside a ",{"type":27,"tag":653,"props":18838,"children":18840},{"className":18839},[],[18841],{"type":32,"value":18842},"\u003Cspan>",{"type":32,"value":18844}," element, a ",{"type":27,"tag":653,"props":18846,"children":18848},{"className":18847},[],[18849],{"type":32,"value":18834},{"type":32,"value":18851}," will be selected. There’s an element preference order which I suggest you ",{"type":27,"tag":172,"props":18853,"children":18856},{"href":18854,"rel":18855},"https://docs.cypress.io/api/commands/contains#Element-preference-order",[696],[18857],{"type":32,"value":18858},"check out in the docs",{"type":32,"value":256},{"type":27,"tag":28,"props":18861,"children":18862},{},[18863,18865,18870],{"type":32,"value":18864},"However, there’s another way you can approach selecting the right element. And that is by passing two arguments to the ",{"type":27,"tag":653,"props":18866,"children":18868},{"className":18867},[],[18869],{"type":32,"value":13507},{"type":32,"value":18871}," function. In this case, first element will be a selector, specifying the scope of our searched element.",{"type":27,"tag":793,"props":18873,"children":18876},{"className":18874,"code":18875,"language":3520,"meta":5},[3517],"cy\n  .contains('ul', 'Pear')\n",[18877],{"type":27,"tag":653,"props":18878,"children":18879},{"__ignoreMap":5},[18880],{"type":32,"value":18875},{"type":27,"tag":28,"props":18882,"children":18883},{},[18884,18886],{"type":32,"value":18885},"Take a look into which element was selected:\n",{"type":27,"tag":959,"props":18887,"children":18890},{"alt":18888,"src":18889},"Selecting the parent element","parent.png",[],{"type":27,"tag":28,"props":18892,"children":18893},{},[18894,18896,18901],{"type":32,"value":18895},"This way I’m selecting the parent ",{"type":27,"tag":653,"props":18897,"children":18899},{"className":18898},[],[18900],{"type":32,"value":105},{"type":32,"value":18902}," element.",{"type":27,"tag":45,"props":18904,"children":18906},{"id":18905},"matching-the-correct-text",[18907],{"type":32,"value":18908},"Matching the correct text",{"type":27,"tag":28,"props":18910,"children":18911},{},[18912],{"type":32,"value":18913},"If you don’t mind small and big letters, you can pass an additional parameter to the function to ignore case:",{"type":27,"tag":793,"props":18915,"children":18918},{"className":18916,"code":18917,"language":3520,"meta":5},[3517],"cy\n  .contains('apples', { matchCase: false })\n",[18919],{"type":27,"tag":653,"props":18920,"children":18921},{"__ignoreMap":5},[18922],{"type":32,"value":18917},{"type":27,"tag":28,"props":18924,"children":18925},{},[18926],{"type":32,"value":18927},"This is especially useful when you have your text abstracted in a separate variable or a file because you might be using different language mutations.",{"type":27,"tag":28,"props":18929,"children":18930},{},[18931],{"type":32,"value":18932},"If this is not enough, you regex to matching any string you like.",{"type":27,"tag":793,"props":18934,"children":18937},{"className":18935,"code":18936,"language":3520,"meta":5},[3517],"cy\n  .contains(/Apple/)\n",[18938],{"type":27,"tag":653,"props":18939,"children":18940},{"__ignoreMap":5},[18941],{"type":32,"value":18936},{"type":27,"tag":28,"props":18943,"children":18944},{},[18945,18947,18952,18954,18959,18961,18966],{"type":32,"value":18946},"Although ",{"type":27,"tag":653,"props":18948,"children":18950},{"className":18949},[],[18951],{"type":32,"value":13507},{"type":32,"value":18953}," sounds like an assertion and can be used as one, the intention is aimed for selecting elements. I written about different ways of ",{"type":27,"tag":172,"props":18955,"children":18956},{"href":12302},[18957],{"type":32,"value":18958},"selecting elements with Cypress",{"type":32,"value":18960}," in the past, and ",{"type":27,"tag":653,"props":18962,"children":18964},{"className":18963},[],[18965],{"type":32,"value":13507},{"type":32,"value":18967}," makes a great addition to that. Even if you don’t have the tested app under full control, these commands can definitely be a good substitution over xpath or complicated css selectors.",{"type":27,"tag":28,"props":18969,"children":18970},{},[18971,18973,18978,18979,18984],{"type":32,"value":18972},"If you enjoyed this, feel free to follow me on ",{"type":27,"tag":172,"props":18974,"children":18976},{"href":5770,"rel":18975},[696],[18977],{"type":32,"value":1589},{"type":32,"value":7692},{"type":27,"tag":172,"props":18980,"children":18982},{"href":10953,"rel":18981},[696],[18983],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":18986},[18987,18988,18989],{"id":18725,"depth":320,"text":18728},{"id":18792,"depth":320,"text":18795},{"id":18905,"depth":320,"text":18908},"content:contains-an-overlooked-gem-in-cypress:index.md","contains-an-overlooked-gem-in-cypress/index.md","contains-an-overlooked-gem-in-cypress/index",{"_path":10472,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":18994,"description":18995,"date":18996,"published":10,"slug":18997,"tags":18998,"readingTime":19001,"body":19005,"_type":329,"_id":19562,"_source":331,"_file":19563,"_stem":19564,"_extension":334},"Improve your custom command logs in Cypress","Short explanation on how to take your custom commands to another level with custom logging, snapshots and many more.","2021-03-16","improve-your-custom-command-logs-in-cypress",[5279,18999,19000],"custom commands","logging",{"text":1933,"minutes":19002,"time":19003,"words":19004},6.755,405300,1351,{"type":24,"children":19006,"toc":19555},[19007,19020,19034,19048,19054,19059,19068,19073,19082,19087,19095,19100,19109,19114,19122,19127,19133,19138,19143,19164,19173,19186,19194,19207,19216,19221,19232,19283,19291,19297,19332,19341,19346,19354,19366,19376,19416,19422,19434,19444,19456,19464,19469,19475,19515,19530,19550],{"type":27,"tag":28,"props":19008,"children":19009},{},[19010,19012,19018],{"type":32,"value":19011},"In the past, I wrote about custom commands and how you ",{"type":27,"tag":172,"props":19013,"children":19015},{"href":19014},"/starting-with-typescript-in-cypress",[19016],{"type":32,"value":19017},"leverage TypeScript",{"type":32,"value":19019}," to give you some great autocomplete capabilities. Using TypeScript is definitely worth it when working on a bigger project with multiple collaborators. In this blog, I’d like to take our custom commands one level further and enable custom logging. This will improve the experience in test runner.",{"type":27,"tag":28,"props":19021,"children":19022},{},[19023,19025,19032],{"type":32,"value":19024},"If you are interested in this topic, I suggest you ",{"type":27,"tag":172,"props":19026,"children":19029},{"href":19027,"rel":19028},"https://www.youtube.com/watch?v=V-o8WzlwKmM",[696],[19030],{"type":32,"value":19031},"check out the webinar on patterns and practices",{"type":32,"value":19033}," that Cypress DX team has done. It’s full of great tips, and they talk about custom logging too.",{"type":27,"tag":28,"props":19035,"children":19036},{},[19037,19039,19046],{"type":32,"value":19038},"As is usual here on this blog, I’m using my Trello clone app, which you can ",{"type":27,"tag":172,"props":19040,"children":19043},{"href":19041,"rel":19042},"https://github.com/filiphric/trelloapp",[696],[19044],{"type":32,"value":19045},"find on my GitHub",{"type":32,"value":19047}," page.",{"type":27,"tag":45,"props":19049,"children":19051},{"id":19050},"creating-a-custom-error",[19052],{"type":32,"value":19053},"Creating a custom error",{"type":27,"tag":28,"props":19055,"children":19056},{},[19057],{"type":32,"value":19058},"Let’s say we create a simple custom command, that will interact with UI and create a new board. It will look something like this.",{"type":27,"tag":793,"props":19060,"children":19063},{"className":19061,"code":19062,"language":3520,"meta":5},[3517],"Cypress.Commands.add('addBoardUi', (name: string) => {\n\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=\"new-board-input\"]')\n    .type(`${name}`)\n    .type('{enter}');\n\n});\n",[19064],{"type":27,"tag":653,"props":19065,"children":19066},{"__ignoreMap":5},[19067],{"type":32,"value":19062},{"type":27,"tag":28,"props":19069,"children":19070},{},[19071],{"type":32,"value":19072},"We will then use it in our test like this:",{"type":27,"tag":793,"props":19074,"children":19077},{"className":19075,"code":19076,"language":3520,"meta":5},[3517],"it('Creates a new board', () => {\n\n  cy\n    .visit('/')\n\n  cy\n    .addBoardUi() // forgot the board name!\n\n});\n",[19078],{"type":27,"tag":653,"props":19079,"children":19080},{"__ignoreMap":5},[19081],{"type":32,"value":19076},{"type":27,"tag":28,"props":19083,"children":19084},{},[19085],{"type":32,"value":19086},"If we want to use our custom command, we need to provide a name for the board. If we don’t our test will still run, but will throw an error:",{"type":27,"tag":28,"props":19088,"children":19089},{},[19090],{"type":27,"tag":959,"props":19091,"children":19094},{"alt":19092,"src":19093},".type() accepts only a string or number","error.png",[],{"type":27,"tag":28,"props":19096,"children":19097},{},[19098],{"type":32,"value":19099},"While this is certainly a well written error, we may want to provide some more insight on what exactly went wrong in this case. To do that, we can check whether an argument was provided. If not, we’ll say that right inside our test runner.",{"type":27,"tag":793,"props":19101,"children":19104},{"className":19102,"code":19103,"language":3520,"meta":5},[3517],"Cypress.Commands.add('addBoardUi', (name: string) => {\n\n  if (!name) throw new Error('You need to provide a board name');\n\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=\"new-board-input\"]')\n    .type(`${name}`)\n    .type('{enter}');\n\n});\n",[19105],{"type":27,"tag":653,"props":19106,"children":19107},{"__ignoreMap":5},[19108],{"type":32,"value":19103},{"type":27,"tag":28,"props":19110,"children":19111},{},[19112],{"type":32,"value":19113},"When we now run an error, we get a slightly more informative message:",{"type":27,"tag":28,"props":19115,"children":19116},{},[19117],{"type":27,"tag":959,"props":19118,"children":19121},{"alt":19119,"src":19120},"Custom error - board name needs to be provided","customError.png",[],{"type":27,"tag":28,"props":19123,"children":19124},{},[19125],{"type":32,"value":19126},"Of course, if we are using TypeScript, it’s hard to miss this kind of error. But mistakes happen, and when they do, it’s nice to find out about the root of the problem as soon as possible.",{"type":27,"tag":45,"props":19128,"children":19130},{"id":19129},"custom-messages",[19131],{"type":32,"value":19132},"Custom messages",{"type":27,"tag":28,"props":19134,"children":19135},{},[19136],{"type":32,"value":19137},"So far, our command is just a sequence of actions. If you look at the first screenshot in out test, you can see that there’s not really any trace of our custom command. I believe this is actually a good thing. We’ve been talking about debuggability and finding root of problems fast - it’s really easy to go to the exact command where an error happened.",{"type":27,"tag":28,"props":19139,"children":19140},{},[19141],{"type":32,"value":19142},"But you may be in a situation where you want to build a library of commands for your colleagues to use and you want your custom commands to be visible in GUI.",{"type":27,"tag":28,"props":19144,"children":19145},{},[19146,19148,19154,19156,19162],{"type":32,"value":19147},"To add some logs, you can just use ",{"type":27,"tag":653,"props":19149,"children":19151},{"className":19150},[],[19152],{"type":32,"value":19153},"cy.log()",{"type":32,"value":19155}," command as you would use any other command in your test. But you can take things one step further with ",{"type":27,"tag":653,"props":19157,"children":19159},{"className":19158},[],[19160],{"type":32,"value":19161},"Cypress.log()",{"type":32,"value":19163}," api. Let’s add some custom logs to our custom command:",{"type":27,"tag":793,"props":19165,"children":19168},{"className":19166,"code":19167,"language":3520,"meta":5},[3517],"Cypress.Commands.add('addBoardUi', (name: string) => {\n\n  Cypress.log({\n    displayName: 'addBoardUi',\n    message: name,\n    name: 'Add new board'\n  });\n\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=\"new-board-input\"]')\n    .type(`${name}`)\n    .type('{enter}');\n\n});\n",[19169],{"type":27,"tag":653,"props":19170,"children":19171},{"__ignoreMap":5},[19172],{"type":32,"value":19167},{"type":27,"tag":28,"props":19174,"children":19175},{},[19176,19178,19184],{"type":32,"value":19177},"This will now display our custom command in Cypress runner. Nice addition to this, is our ",{"type":27,"tag":653,"props":19179,"children":19181},{"className":19180},[],[19182],{"type":32,"value":19183},"name",{"type":32,"value":19185}," parameter, that is printed next to our command name.",{"type":27,"tag":28,"props":19187,"children":19188},{},[19189],{"type":27,"tag":959,"props":19190,"children":19193},{"alt":19191,"src":19192},"Custom log - new board name is displayed in command log","customLog.png",[],{"type":27,"tag":28,"props":19195,"children":19196},{},[19197,19199,19205],{"type":32,"value":19198},"To bring some more information to our board, we can add a ",{"type":27,"tag":653,"props":19200,"children":19202},{"className":19201},[],[19203],{"type":32,"value":19204},"consoleProps",{"type":32,"value":19206}," function, that will print additional info to our console:",{"type":27,"tag":793,"props":19208,"children":19211},{"className":19209,"code":19210,"language":3520,"meta":5},[3517],"Cypress.log({\n  consoleProps() {\n    return {\n      'board name': name\n    }\n  },\n  displayName: 'addBoardUi',\n  message: name,\n  name: 'Add new board'\n});\n",[19212],{"type":27,"tag":653,"props":19213,"children":19214},{"__ignoreMap":5},[19215],{"type":32,"value":19210},{"type":27,"tag":28,"props":19217,"children":19218},{},[19219],{"type":32,"value":19220},"But what if we have some information that is not available in parameter? Let’s say we want to print our board url to the console. Might be useful for debugging after test run. To do this, we need to write our function differently:",{"type":27,"tag":793,"props":19222,"children":19227},{"className":19223,"code":19224,"highlights":19225,"language":3520,"meta":5},[3517],"Cypress.Commands.add('addBoardUi', (name: string) => {\n\n  let boardUrl;\n\n  const log = Cypress.log({\n    autoEnd: false,\n    consoleProps() {\n      return {\n        'board name': name,\n        'board url': boardUrl\n      }\n    },\n    displayName: 'addBoardUi',\n    message: name,\n    name: 'Add new board'\n  });\n\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=\"new-board-input\"]')\n    .type(name)\n    .type('{enter}');\n\n  cy\n    .url()\n    .then((url) => {\n      boardUrl = url\n      log.end()\n    })\n\n});\n",[3809,19226],31,[19228],{"type":27,"tag":653,"props":19229,"children":19230},{"__ignoreMap":5},[19231],{"type":32,"value":19224},{"type":27,"tag":28,"props":19233,"children":19234},{},[19235,19237,19243,19245,19250,19252,19257,19259,19265,19267,19273,19275,19281],{"type":32,"value":19236},"First, we define a variable ",{"type":27,"tag":653,"props":19238,"children":19240},{"className":19239},[],[19241],{"type":32,"value":19242},"boardUrl",{"type":32,"value":19244},". This will be used for assigning our url, later on line 30. The other thing we are doing slightly differently is that we assign our ",{"type":27,"tag":653,"props":19246,"children":19248},{"className":19247},[],[19249],{"type":32,"value":19161},{"type":32,"value":19251}," to a variable. This enables us to continuously feed data into our log. This means that although our ",{"type":27,"tag":653,"props":19253,"children":19255},{"className":19254},[],[19256],{"type":32,"value":19242},{"type":32,"value":19258}," will be ",{"type":27,"tag":653,"props":19260,"children":19262},{"className":19261},[],[19263],{"type":32,"value":19264},"undefined",{"type":32,"value":19266}," at first, we can fill the information later and it will appear in the test runner. The last thing about this is the ",{"type":27,"tag":653,"props":19268,"children":19270},{"className":19269},[],[19271],{"type":32,"value":19272},"autoEnd",{"type":32,"value":19274}," attribute, which will tell Cypress not to finish logging until we explicitly say so using ",{"type":27,"tag":653,"props":19276,"children":19278},{"className":19277},[],[19279],{"type":32,"value":19280},".end()",{"type":32,"value":19282}," function on line 31.",{"type":27,"tag":28,"props":19284,"children":19285},{},[19286],{"type":27,"tag":959,"props":19287,"children":19290},{"alt":19288,"src":19289},"Information logged into console","consoleLog.png",[],{"type":27,"tag":45,"props":19292,"children":19294},{"id":19293},"highlighting-elements",[19295],{"type":32,"value":19296},"Highlighting elements",{"type":27,"tag":28,"props":19298,"children":19299},{},[19300,19302,19308,19310,19315,19317,19323,19324,19330],{"type":32,"value":19301},"Let’s move on from our current example to something else. In my app I have couple of data attributes, and I want to create a custom command for selecting them. I will call it ",{"type":27,"tag":653,"props":19303,"children":19305},{"className":19304},[],[19306],{"type":32,"value":19307},"take",{"type":32,"value":19309}," and it will basically be a shortcut for ",{"type":27,"tag":653,"props":19311,"children":19313},{"className":19312},[],[19314],{"type":32,"value":12748},{"type":32,"value":19316}," command. I want to be able to write ",{"type":27,"tag":653,"props":19318,"children":19320},{"className":19319},[],[19321],{"type":32,"value":19322},".take('create-board')",{"type":32,"value":7446},{"type":27,"tag":653,"props":19325,"children":19327},{"className":19326},[],[19328],{"type":32,"value":19329},".get([data-cy='create-board'])",{"type":32,"value":19331},". The basics will look like this:",{"type":27,"tag":793,"props":19333,"children":19336},{"className":19334,"code":19335,"language":3520,"meta":5},[3517],"Cypress.Commands.add('take', (input: string) => {\n\n  const log = Cypress.log({\n    consoleProps() {\n      return {\n        selector: input\n      };\n    },\n    displayName: 'take',\n    name: 'Get by [data-cy] attribute'\n  });\n\n  cy\n    .get(`[data-cy=${input}]`)\n\n});\n",[19337],{"type":27,"tag":653,"props":19338,"children":19339},{"__ignoreMap":5},[19340],{"type":32,"value":19335},{"type":27,"tag":28,"props":19342,"children":19343},{},[19344],{"type":32,"value":19345},"But you can notice that our new command does not highlight our element:",{"type":27,"tag":28,"props":19347,"children":19348},{},[19349],{"type":27,"tag":959,"props":19350,"children":19353},{"alt":19351,"src":19352},"Missing highlight on custom command","highlight.mp4",[],{"type":27,"tag":28,"props":19355,"children":19356},{},[19357,19359,19364],{"type":32,"value":19358},"Let’s fix that and also get rid of our ",{"type":27,"tag":653,"props":19360,"children":19362},{"className":19361},[],[19363],{"type":32,"value":12748},{"type":32,"value":19365}," command, so that we don’t have duplicity in our test:",{"type":27,"tag":793,"props":19367,"children":19371},{"className":19368,"code":19369,"highlights":19370,"language":3520,"meta":5},[3517],"Cypress.Commands.add('take', (input: string) => {\n\n  const log = Cypress.log({\n    autoEnd: false,\n    consoleProps() {\n      return {\n        selector: input,\n      };\n    },\n    displayName: 'take',\n    name: 'Get by [data-cy] attribute'\n  });\n\n  cy\n    .get(`[data-cy=${input}]`, { log: false })\n    .then(($el) => {\n      log.set({ $el });\n      log.snapshot()\n      log.end();\n    });\n\n});\n",[10872,10246],[19372],{"type":27,"tag":653,"props":19373,"children":19374},{"__ignoreMap":5},[19375],{"type":32,"value":19369},{"type":27,"tag":28,"props":19377,"children":19378},{},[19379,19381,19387,19389,19394,19396,19401,19402,19407,19409,19415],{"type":32,"value":19380},"Now we only have a single command in our Cypress runner. Not only that, but by using ",{"type":27,"tag":653,"props":19382,"children":19384},{"className":19383},[],[19385],{"type":32,"value":19386},"log.set({ $el });",{"type":32,"value":19388}," we are now highlighting the element that our ",{"type":27,"tag":653,"props":19390,"children":19392},{"className":19391},[],[19393],{"type":32,"value":12748},{"type":32,"value":19395}," command finds. Similar to our previous example, we are using ",{"type":27,"tag":653,"props":19397,"children":19399},{"className":19398},[],[19400],{"type":32,"value":19272},{"type":32,"value":4164},{"type":27,"tag":653,"props":19403,"children":19405},{"className":19404},[],[19406],{"type":32,"value":19280},{"type":32,"value":19408}," function to finish our logging. To make our highlight work, we need to do at least one snapshot in our command using ",{"type":27,"tag":653,"props":19410,"children":19412},{"className":19411},[],[19413],{"type":32,"value":19414},".snapshot()",{"type":32,"value":6658},{"type":27,"tag":45,"props":19417,"children":19419},{"id":19418},"adding-more-logs",[19420],{"type":32,"value":19421},"Adding more logs",{"type":27,"tag":28,"props":19423,"children":19424},{},[19425,19427,19432],{"type":32,"value":19426},"But now we have lost a couple of console logs that the original ",{"type":27,"tag":653,"props":19428,"children":19430},{"className":19429},[],[19431],{"type":32,"value":12748},{"type":32,"value":19433}," command gives us. To fix that, we’ll once again create placeholder variables and fill in the information as we proceed through our actions.",{"type":27,"tag":793,"props":19435,"children":19439},{"className":19436,"code":19437,"highlights":19438,"language":3520,"meta":5},[3517],"Cypress.Commands.add('take', (input: string) => {\n\n  let element: JQuery\u003CHTMLElement> | HTMLElement[];\n  let count: number;\n\n  const log = Cypress.log({\n    autoEnd: false,\n    consoleProps() {\n      return {\n        selector: input,\n        'Yielded': element,\n        'Elements': count\n      };\n    },\n    displayName: 'take',\n    name: 'Get by [data-cy] attribute'\n  });\n\n  cy\n    .get(`[data-cy=${input}]`, { log: false })\n    .then(($el) => {\n      element = Cypress.dom.getElements($el)\n      count = $el.length;\n      log.set({ $el });\n      log.snapshot().end();\n    });\n\n});\n",[1606,3877,3811,3812,7544,7545],[19440],{"type":27,"tag":653,"props":19441,"children":19442},{"__ignoreMap":5},[19443],{"type":32,"value":19437},{"type":27,"tag":28,"props":19445,"children":19446},{},[19447,19449,19454],{"type":32,"value":19448},"Our command is starting to look pretty neat. In fact, our console print looks exactly as the original ",{"type":27,"tag":653,"props":19450,"children":19452},{"className":19451},[],[19453],{"type":32,"value":12748},{"type":32,"value":19455}," command:",{"type":27,"tag":28,"props":19457,"children":19458},{},[19459],{"type":27,"tag":959,"props":19460,"children":19463},{"alt":19461,"src":19462},"Custom command with logging","takeLogs.png",[],{"type":27,"tag":28,"props":19465,"children":19466},{},[19467],{"type":32,"value":19468},"There’s one small bug here, which might not be visible at first sight. Congratulations if you spotted it.",{"type":27,"tag":45,"props":19470,"children":19472},{"id":19471},"handling-errors",[19473],{"type":32,"value":19474},"Handling errors",{"type":27,"tag":28,"props":19476,"children":19477},{},[19478,19479,19484,19486,19491,19493,19498,19500,19505,19507,19513],{"type":32,"value":11386},{"type":27,"tag":653,"props":19480,"children":19482},{"className":19481},[],[19483],{"type":32,"value":19272},{"type":32,"value":19485}," attribute will wait until our ",{"type":27,"tag":653,"props":19487,"children":19489},{"className":19488},[],[19490],{"type":32,"value":19280},{"type":32,"value":19492}," function is called. But if the ",{"type":27,"tag":653,"props":19494,"children":19496},{"className":19495},[],[19497],{"type":32,"value":12748},{"type":32,"value":19499}," command does not find an element, our log will never finish. The test would still fail, so no real harm is done. It’s just that our ",{"type":27,"tag":653,"props":19501,"children":19503},{"className":19502},[],[19504],{"type":32,"value":19307},{"type":32,"value":19506}," command will be stuck in a loading state. To fix that, I’ll just tap into the ",{"type":27,"tag":653,"props":19508,"children":19510},{"className":19509},[],[19511],{"type":32,"value":19512},"fail",{"type":32,"value":19514}," event, finish my log and throw error:",{"type":27,"tag":793,"props":19516,"children":19525},{"className":19517,"code":19518,"highlights":19519,"language":3520,"meta":5},[3517],"Cypress.Commands.add('take', (input: string) => {\n\n  let element: JQuery\u003CHTMLElement> | HTMLElement[];\n  let count: number;\n\n  const log = Cypress.log({\n    autoEnd: false,\n    consoleProps() {\n      return {\n        selector: input,\n        'Yielded': element,\n        'Elements': count\n      };\n    },\n    displayName: 'take',\n    name: 'Get by [data-cy] attribute'\n  });\n\n  cy\n    .get(`[data-cy=${input}]`, { log: false })\n    .then(($el) => {\n      element = Cypress.dom.getElements($el)\n      count = $el.length;\n      log.set({ $el });\n      log.snapshot().end();\n    });\n\n  cy\n    .on('fail', (err) => {\n      log.error(err)\n      log.end()\n      throw err\n    })\n\n});\n",[19520,19521,19522,19226,19523,19524],28,29,30,32,33,[19526],{"type":27,"tag":653,"props":19527,"children":19528},{"__ignoreMap":5},[19529],{"type":32,"value":19518},{"type":27,"tag":28,"props":19531,"children":19532},{},[19533,19535,19540,19542,19548],{"type":32,"value":19534},"For the sake of keeping this article simple, I’m not going to dive into any more details. I am using custom commands e.g. for logging information from API requests (check out how I work with those in my ",{"type":27,"tag":172,"props":19536,"children":19537},{"href":15399},[19538],{"type":32,"value":19539},"previous blog",{"type":32,"value":19541},"), where I snapshot the \"before\" and \"after\" state. In my work, I use a similar command to our ",{"type":27,"tag":653,"props":19543,"children":19545},{"className":19544},[],[19546],{"type":32,"value":19547},".take()",{"type":32,"value":19549}," custom command, but I am actually using it as a dual command, meaning that I can get a parent element, and then only select a child element within the context of that parent element. There’s really a lot of cool stuff you can do with this.",{"type":27,"tag":28,"props":19551,"children":19552},{},[19553],{"type":32,"value":19554},"Hope you liked this. I’m writing a blogpost like this every week, so if you are interested, make sure to follow me on Twitter, connect with me on LinkedIn, or subscribe to my newsletter where I will let you know each time I publish a new article.",{"title":5,"searchDepth":320,"depth":320,"links":19556},[19557,19558,19559,19560,19561],{"id":19050,"depth":320,"text":19053},{"id":19129,"depth":320,"text":19132},{"id":19293,"depth":320,"text":19296},{"id":19418,"depth":320,"text":19421},{"id":19471,"depth":320,"text":19474},"content:improve-your-custom-command-logs-in-cypress:index.md","improve-your-custom-command-logs-in-cypress/index.md","improve-your-custom-command-logs-in-cypress/index",{"_path":19566,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":19567,"description":19568,"date":19569,"published":10,"slug":19570,"tags":19571,"readingTime":19574,"body":19578,"_type":329,"_id":19887,"_source":331,"_file":19888,"_stem":19889,"_extension":334},"/tips-for-debugging-tests-in-cypress","Tips for debugging tests in Cypress","If you’ve been testing for a longer time, you know that writing a test is only half of the story. The other half is maintenance. I share a couple of ways you can debug your tests in Cypress in my latest article.","2021-03-08","tips-for-debugging-tests-in-cypress",[5279,1498,19572,19573],"flake","error",{"text":927,"minutes":19575,"time":19576,"words":19577},4.62,277200,924,{"type":24,"children":19579,"toc":19879},[19580,19585,19591,19604,19612,19630,19635,19647,19656,19675,19681,19694,19714,19723,19729,19742,19768,19774,19779,19788,19793,19812,19820,19833,19839,19861],{"type":27,"tag":28,"props":19581,"children":19582},{},[19583],{"type":32,"value":19584},"Debugging, right? Not sure if I love it or hate it. The part of me that loves it enjoys the learning that comes with it. The hated part is usually the fact that debugging feels like wasted time. In this article I’d like to share a couple of tips on how I usually debug tests in Cypress in hope of helping you with your own debugging.",{"type":27,"tag":45,"props":19586,"children":19588},{"id":19587},"pause-your-test",[19589],{"type":32,"value":19590},".pause() your test",{"type":27,"tag":28,"props":19592,"children":19593},{},[19594,19596,19602],{"type":32,"value":19595},"When using Cypress in GUI mode, you can use ",{"type":27,"tag":653,"props":19597,"children":19599},{"className":19598},[],[19600],{"type":32,"value":19601},".pause()",{"type":32,"value":19603}," command to stop your test at a problematic spot. I usually do this to look at the test right before problematic assertion or action that caused the test to fail. After pausing your test, you can interact with your page, examine the state and then click play button to continue with the test.",{"type":27,"tag":28,"props":19605,"children":19606},{},[19607],{"type":27,"tag":959,"props":19608,"children":19611},{"alt":19609,"src":19610},".pause() command in action","pause.mp4",[],{"type":27,"tag":28,"props":19613,"children":19614},{},[19615,19617,19622,19623,19629],{"type":32,"value":19616},"You don’t need to worry about leaving this command in your tests, since it is ignored when you run your tests in headless mode. Read more about ",{"type":27,"tag":653,"props":19618,"children":19620},{"className":19619},[],[19621],{"type":32,"value":19601},{"type":32,"value":15244},{"type":27,"tag":172,"props":19624,"children":19627},{"href":19625,"rel":19626},"https://docs.cypress.io/api/commands/pause.html#Syntax",[696],[19628],{"type":32,"value":14078},{"type":32,"value":256},{"type":27,"tag":45,"props":19631,"children":19633},{"id":19632},"consolelog",[19634],{"type":32,"value":5487},{"type":27,"tag":28,"props":19636,"children":19637},{},[19638,19640,19645],{"type":32,"value":19639},"Cypress is all JavaScript & it runs inside the browser, where you can make use of all the powers of DevTools. If you are not yet comfortable with using debugger or don’t feel like using it, simple ",{"type":27,"tag":653,"props":19641,"children":19643},{"className":19642},[],[19644],{"type":32,"value":5487},{"type":32,"value":19646}," is your friend.",{"type":27,"tag":793,"props":19648,"children":19651},{"className":19649,"code":19650,"language":1513,"meta":5},[1510],"cy\n  .intercept('POST', '/api/boards')\n  .as('createBoard')\n\ncy\n  .wait('@createBoard')\n  .then( ({ response }) => {\n    console.log(response.body)\n  })\n\n",[19652],{"type":27,"tag":653,"props":19653,"children":19654},{"__ignoreMap":5},[19655],{"type":32,"value":19650},{"type":27,"tag":28,"props":19657,"children":19658},{},[19659,19661,19666,19668,19673],{"type":32,"value":19660},"This code will output the response body of an intercepted request. You can see the output in your browser console. I’ve seen this confuse few people, as they would look for this output in the terminal. But while you start your Cypress runner using ",{"type":27,"tag":653,"props":19662,"children":19664},{"className":19663},[],[19665],{"type":32,"value":11098},{"type":32,"value":19667}," command, the Cypress script itself runs inside the browser. And that’s where your ",{"type":27,"tag":653,"props":19669,"children":19671},{"className":19670},[],[19672],{"type":32,"value":5487},{"type":32,"value":19674}," output will be.",{"type":27,"tag":45,"props":19676,"children":19678},{"id":19677},"run-your-test-multiple-times",[19679],{"type":32,"value":19680},"Run your test multiple times",{"type":27,"tag":28,"props":19682,"children":19683},{},[19684,19686,19693],{"type":32,"value":19685},"Sometimes you need to debug a test, because it’s flaky. In my experience, the biggest source of flakiness is the speed of how test is executed. There’s a really good section on Cypress blog page on the whole topic of ",{"type":27,"tag":172,"props":19687,"children":19690},{"href":19688,"rel":19689},"https://cypress.io/blog/tag/flake/",[696],[19691],{"type":32,"value":19692},"how to stabilize a flaky test",{"type":32,"value":256},{"type":27,"tag":28,"props":19695,"children":19696},{},[19697,19699,19705,19707,19713],{"type":32,"value":19698},"But knowing a test is flaky is only a part of the story. To stabilize a test, you need to find the source of the problem. When fighting different race condition situations (click goes too fast, assertion goes too fast, network is unstable) I tend to run my tests multiple times. This is because I often get into situations where test fails on pipeline but passes locally. Running a test multiple times usually surfaces the problem. You can use standard ",{"type":27,"tag":653,"props":19700,"children":19702},{"className":19701},[],[19703],{"type":32,"value":19704},"for",{"type":32,"value":19706}," loop, but I’ve enjoyed wrapping a single test with Lodash ",{"type":27,"tag":653,"props":19708,"children":19710},{"className":19709},[],[19711],{"type":32,"value":19712},"times",{"type":32,"value":12902},{"type":27,"tag":793,"props":19715,"children":19718},{"className":19716,"code":19717,"language":1513,"meta":5},[1510],"Cypress._.times(10, () => {\n\n  it('flaky test', () => {\n\n    // test code\n\n  });\n\n});\n",[19719],{"type":27,"tag":653,"props":19720,"children":19721},{"__ignoreMap":5},[19722],{"type":32,"value":19717},{"type":27,"tag":45,"props":19724,"children":19726},{"id":19725},"watch-the-video",[19727],{"type":32,"value":19728},"Watch the video",{"type":27,"tag":28,"props":19730,"children":19731},{},[19732,19734,19740],{"type":32,"value":19733},"Might seem like an obvious one, but many times I tend to forget that the first thing I should look at is not the error itself, but the context in which the error happens. Cypress records all video automatically in headless mode, but it can be disabled if screenshots are good enough. I have written an article on ",{"type":27,"tag":172,"props":19735,"children":19737},{"href":19736},"/improve-your-error-screenshots-in-cypress",[19738],{"type":32,"value":19739},"how you can improve your screenshots",{"type":32,"value":19741}," to make them more useful for debugging purposes.",{"type":27,"tag":28,"props":19743,"children":19744},{},[19745,19747,19753,19754,19759,19761,19766],{"type":32,"value":19746},"The most often, people disable video recording for speed purposes, but you can actually make a pretty good compromise. ",{"type":27,"tag":653,"props":19748,"children":19750},{"className":19749},[],[19751],{"type":32,"value":19752},"videoUploadOnPasses",{"type":32,"value":14683},{"type":27,"tag":653,"props":19755,"children":19757},{"className":19756},[],[19758],{"type":32,"value":18189},{"type":32,"value":19760}," in your ",{"type":27,"tag":653,"props":19762,"children":19764},{"className":19763},[],[19765],{"type":32,"value":6028},{"type":32,"value":19767}," will upload video only if there is a failed test in your spec. This can shave of minutes from your test run.",{"type":27,"tag":45,"props":19769,"children":19771},{"id":19770},"travel-through-the-timeline",[19772],{"type":32,"value":19773},"Travel through the timeline",{"type":27,"tag":28,"props":19775,"children":19776},{},[19777],{"type":32,"value":19778},"Timeline in GUI is a great debugging tool. You can look at the state at each of the stage of your tests and examine what might have caused the failure. I see a common error happening with a following test:",{"type":27,"tag":793,"props":19780,"children":19783},{"className":19781,"code":19782,"language":1513,"meta":5},[1510],"// create item\ncy\n  .get('input')\n  .type('new item{enter}')\n\n// item appears as a second item on page\ncy\n  .get('.item')\n  .eq(1)\n  .should('be.visible')\n",[19784],{"type":27,"tag":653,"props":19785,"children":19786},{"__ignoreMap":5},[19787],{"type":32,"value":19782},{"type":27,"tag":28,"props":19789,"children":19790},{},[19791],{"type":32,"value":19792},"I’ve seen a test like this fail a lot. There is a lot happening in between typing in the new item and the item actually appearing on the page. There might be a http request, websocket message, reorder, re-rendering of a list. Al these processes might have caused the test to fail.",{"type":27,"tag":28,"props":19794,"children":19795},{},[19796,19798,19803,19805,19810],{"type":32,"value":19797},"This is because Cypress will automatically retry an assertion + previous command. But it will only retry the previous command, not entire command chain. If you would hover over the ",{"type":27,"tag":653,"props":19799,"children":19801},{"className":19800},[],[19802],{"type":32,"value":13728},{"type":32,"value":19804}," command in our test, you would see that we are actually not getting the right element. If an item renders with a delay, you would get stuck with the state of your app as it was when ",{"type":27,"tag":653,"props":19806,"children":19808},{"className":19807},[],[19809],{"type":32,"value":12748},{"type":32,"value":19811}," command was made. Example of what might be happening:",{"type":27,"tag":28,"props":19813,"children":19814},{},[19815],{"type":27,"tag":959,"props":19816,"children":19819},{"alt":19817,"src":19818},"Failing assertion on .eq() command","list.mp4",[],{"type":27,"tag":28,"props":19821,"children":19822},{},[19823,19825,19831],{"type":32,"value":19824},"I explore this topic a little more in one of my ",{"type":27,"tag":17914,"props":19826,"children":19828},{"to":19827},"testing-lists-of-items",[19829],{"type":32,"value":19830},"previous blogs",{"type":32,"value":19832},". While confusing at a first glance, looking at the timeline might shed some more light into what test did before it failed.",{"type":27,"tag":45,"props":19834,"children":19836},{"id":19835},"use-the-cypress-dashboard",[19837],{"type":32,"value":19838},"Use the Cypress dashboard",{"type":27,"tag":28,"props":19840,"children":19841},{},[19842,19844,19851,19852,19859],{"type":32,"value":19843},"Cypress Dashboard is ",{"type":27,"tag":172,"props":19845,"children":19848},{"href":19846,"rel":19847},"https://www.cypress.io/pricing/",[696],[19849],{"type":32,"value":19850},"free to use for 500 monthly recordings",{"type":32,"value":15404},{"type":27,"tag":172,"props":19853,"children":19856},{"href":19854,"rel":19855},"https://www.cypress.io/oss-plan/",[696],[19857],{"type":32,"value":19858},"unlimited if you are working on an open source project",{"type":32,"value":19860},". If you record your test results to dashboard, you and your team can look into screenshots and examine the test failures. But not only that. I especially enjoy the analytics overview, where I can look into most common failures or most flaky tests. This gives me some great pointers into what may be be the greatest weak points in my tests. I still need to roll up my sleeves and use previously mentioned tools for debugging, but analytics provides great set of toolset for finding issues proactively.",{"type":27,"tag":28,"props":19862,"children":19863},{},[19864,19866,19871,19872,19877],{"type":32,"value":19865},"If you have enjoyed this, feel free to let me know. I write posts like this every week, so if you feel like getting notified, put your email down below this article or follow me on ",{"type":27,"tag":172,"props":19867,"children":19869},{"href":5770,"rel":19868},[696],[19870],{"type":32,"value":1589},{"type":32,"value":4164},{"type":27,"tag":172,"props":19873,"children":19875},{"href":10953,"rel":19874},[696],[19876],{"type":32,"value":1598},{"type":32,"value":19878}," where I usually let the world know that a new article is out there.",{"title":5,"searchDepth":320,"depth":320,"links":19880},[19881,19882,19883,19884,19885,19886],{"id":19587,"depth":320,"text":19590},{"id":19632,"depth":320,"text":5487},{"id":19677,"depth":320,"text":19680},{"id":19725,"depth":320,"text":19728},{"id":19770,"depth":320,"text":19773},{"id":19835,"depth":320,"text":19838},"content:tips-for-debugging-tests-in-cypress:index.md","tips-for-debugging-tests-in-cypress/index.md","tips-for-debugging-tests-in-cypress/index",{"_path":19891,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":19892,"description":19893,"date":19894,"published":10,"slug":19895,"tags":19896,"readingTime":19900,"body":19904,"_type":329,"_id":20212,"_source":331,"_file":20213,"_stem":20214,"_extension":334},"/make-your-cypress-tests-faster-with-clock","Make your Cypress tests faster with .clock()","With .clock() and .tick() functions, it is possible to manipulate app’s time and make your test faster by skipping waits of setTimeout() and setInterval() functions.","2021-03-01","make-your-cypress-tests-faster-with-clock",[5279,19897,19898,19899],"date","time","clock",{"text":585,"minutes":19901,"time":19902,"words":19903},3.585,215100,717,{"type":24,"children":19905,"toc":20210},[19906,19927,19941,19954,19963,19998,20010,20019,20039,20059,20068,20103,20112,20125,20130,20138,20143,20148,20157,20188,20193],{"type":27,"tag":28,"props":19907,"children":19908},{},[19909,19911,19918,19920,19925],{"type":32,"value":19910},"The fact that Cypress is running inside the same context as the tested application is one of its greatest advantages. I wrote about this in previous posts about ",{"type":27,"tag":172,"props":19912,"children":19915},{"href":19913,"rel":19914},"https://applitools.com/blog/page-objects-app-actions-cypress/",[696],[19916],{"type":32,"value":19917},"Page Objects vs. App Actions",{"type":32,"value":19919}," and also in article about ",{"type":27,"tag":172,"props":19921,"children":19922},{"href":17738},[19923],{"type":32,"value":19924},"opening a new tab in Cypress",{"type":32,"value":19926},". This architecture allows us to look into the functions our app is executing. It also allows adjust the app’s context such as browser preferences and time. The latter is the subject of this blog.",{"type":27,"tag":28,"props":19928,"children":19929},{},[19930,19932,19939],{"type":32,"value":19931},"Let’s look at a simple app ",{"type":27,"tag":172,"props":19933,"children":19936},{"href":19934,"rel":19935},"https://github.com/filiphric/cypress-clock",[696],[19937],{"type":32,"value":19938},"I’ve made for this article",{"type":32,"value":19940},". It basically just opens up and starts counting seconds. Like a stopwatch, but resetting only on refresh.",{"type":27,"tag":28,"props":19942,"children":19943},{},[19944,19946,19952],{"type":32,"value":19945},"The way this app works, is that inside this app, we have a ",{"type":27,"tag":653,"props":19947,"children":19949},{"className":19948},[],[19950],{"type":32,"value":19951},"setInterval()",{"type":32,"value":19953}," function. This is a JS function that takes two arguments. First one is the function we want to run, and the second one is time in milliseconds, that tells our function how often should it run this function.",{"type":27,"tag":793,"props":19955,"children":19958},{"className":19956,"code":19957,"filename":7849,"language":1513,"meta":5},[1510],"setInterval(updateTime, 1000);\n",[19959],{"type":27,"tag":653,"props":19960,"children":19961},{"__ignoreMap":5},[19962],{"type":32,"value":19957},{"type":27,"tag":28,"props":19964,"children":19965},{},[19966,19967,19973,19975,19981,19983,19988,19990,19996],{"type":32,"value":11386},{"type":27,"tag":653,"props":19968,"children":19970},{"className":19969},[],[19971],{"type":32,"value":19972},"updateTime()",{"type":32,"value":19974}," function does all the work in this app. The way it works is pretty simple. There are two ",{"type":27,"tag":653,"props":19976,"children":19978},{"className":19977},[],[19979],{"type":32,"value":19980},"Date",{"type":32,"value":19982}," objects in our app. One will get our time at the moment we open our app, and the other one is created every time our ",{"type":27,"tag":653,"props":19984,"children":19986},{"className":19985},[],[19987],{"type":32,"value":19972},{"type":32,"value":19989}," function is called. These two are then compared so every second we get a new time information. We then take the  ",{"type":27,"tag":653,"props":19991,"children":19993},{"className":19992},[],[19994],{"type":32,"value":19995},"\u003Cdiv class=\"timer\">\u003C/div>",{"type":32,"value":19997}," element, and update it’s text with the new time.",{"type":27,"tag":28,"props":19999,"children":20000},{},[20001,20002,20008],{"type":32,"value":3349},{"type":27,"tag":653,"props":20003,"children":20005},{"className":20004},[],[20006],{"type":32,"value":20007},".clock()",{"type":32,"value":20009}," function in Cypress allows us to tap into all the Date objects and time handling functions and move them around as we wish. Let’s say we want to move our time 10 seconds further when we open the app. We would do it with following code:",{"type":27,"tag":793,"props":20011,"children":20014},{"className":20012,"code":20013,"language":3520,"meta":5},[3517],"it('move timer', () => {\n\n  const now = new Date()\n\n  cy\n    .visit('index.html')\n\n  cy\n    .clock(now)\n\n  cy\n    .tick(10000)\n\n});\n",[20015],{"type":27,"tag":653,"props":20016,"children":20017},{"__ignoreMap":5},[20018],{"type":32,"value":20013},{"type":27,"tag":28,"props":20020,"children":20021},{},[20022,20024,20030,20032,20037],{"type":32,"value":20023},"Passing the ",{"type":27,"tag":653,"props":20025,"children":20027},{"className":20026},[],[20028],{"type":32,"value":20029},"now",{"type":32,"value":20031}," variable to our",{"type":27,"tag":653,"props":20033,"children":20035},{"className":20034},[],[20036],{"type":32,"value":20007},{"type":32,"value":20038}," command will set our Cypress clock to the current moment. Without this argument it would start with at the beginning of UNIX epoch, which would set our clock more than 50 years backwards.",{"type":27,"tag":28,"props":20040,"children":20041},{},[20042,20044,20050,20052,20057],{"type":32,"value":20043},"Using the ",{"type":27,"tag":653,"props":20045,"children":20047},{"className":20046},[],[20048],{"type":32,"value":20049},".tick()",{"type":32,"value":20051}," Cypress function will move our time 10 seconds forward. To restore our time and move everything back to normal, just invoke the ",{"type":27,"tag":653,"props":20053,"children":20055},{"className":20054},[],[20056],{"type":32,"value":20007},{"type":32,"value":20058}," restore function like this:",{"type":27,"tag":793,"props":20060,"children":20063},{"className":20061,"code":20062,"language":3520,"meta":5},[3517],"it('move timer', () => {\n\n  const now = new Date()\n\n  cy\n    .visit('index.html')\n\n  cy\n    .clock(now)\n\n  cy\n    .tick(10000)\n\n  cy.clock().invoke('restore')\n\n});\n",[20064],{"type":27,"tag":653,"props":20065,"children":20066},{"__ignoreMap":5},[20067],{"type":32,"value":20062},{"type":27,"tag":28,"props":20069,"children":20070},{},[20071,20073,20079,20081,20087,20089,20094,20096,20102],{"type":32,"value":20072},"This is a great tool for improving the speed of your tests. In my ",{"type":27,"tag":172,"props":20074,"children":20076},{"href":19041,"rel":20075},[696],[20077],{"type":32,"value":20078},"Trello clone app",{"type":32,"value":20080},", I have an error message that appears when we get a non-200 response from server. That error message uses a ",{"type":27,"tag":653,"props":20082,"children":20084},{"className":20083},[],[20085],{"type":32,"value":20086},"setTimeout()",{"type":32,"value":20088}," function, so that after 4 seconds, it automatically disappears. To test it, I use the ",{"type":27,"tag":653,"props":20090,"children":20092},{"className":20091},[],[20093],{"type":32,"value":14731},{"type":32,"value":20095}," command, which I have mentioned in my ",{"type":27,"tag":172,"props":20097,"children":20099},{"href":20098},"/migrating-route-to-intercept-in-cypress",[20100],{"type":32,"value":20101},"previous blog post",{"type":32,"value":256},{"type":27,"tag":793,"props":20104,"children":20107},{"className":20105,"code":20106,"language":3520,"meta":5},[3517],"it('error message works', () => {\n\n  cy\n    .intercept('POST', '/api/boards', {\n      forceNetworkError: true\n    })\n    .as('boardCreate')\n\n  cy\n    .visit('/');\n\n  cy\n    .get('[data-cy=create-board]')\n    .click()\n\n  cy\n    .get('[data-cy=new-board-input]')\n    .type('new garden{enter}')\n\n  cy\n    .get('#errorMessage')\n    .should('be.visible')\n\n  cy\n    .get('#errorMessage')\n    .should('not.be.visible')\n\n})\n",[20108],{"type":27,"tag":653,"props":20109,"children":20110},{"__ignoreMap":5},[20111],{"type":32,"value":20106},{"type":27,"tag":28,"props":20113,"children":20114},{},[20115,20117,20123],{"type":32,"value":20116},"In my test I’m testing that the ",{"type":27,"tag":653,"props":20118,"children":20120},{"className":20119},[],[20121],{"type":32,"value":20122},"#errorMessage",{"type":32,"value":20124}," element appears, but also that it disappears. Since default retry timeout in Cypress is set to 4000ms this test works beautifully.",{"type":27,"tag":28,"props":20126,"children":20127},{},[20128],{"type":32,"value":20129},"Except one thing.",{"type":27,"tag":28,"props":20131,"children":20132},{},[20133],{"type":27,"tag":959,"props":20134,"children":20137},{"alt":20135,"src":20136},"Test is taking way too long","long.mp4",[],{"type":27,"tag":28,"props":20139,"children":20140},{},[20141],{"type":32,"value":20142},"In this test, we are waiting for 4 seconds while the error message disappears. That’s 4 seconds of idle waiting. It doesn’t seem like much, but if your test suite contains hundreds of tests, you might want to optimize. With a test like this, you definitely should.",{"type":27,"tag":28,"props":20144,"children":20145},{},[20146],{"type":32,"value":20147},"We can do the exact same thing we did with our precious test on the timer. We can move our time forward and skip the wait:",{"type":27,"tag":793,"props":20149,"children":20152},{"className":20150,"code":20151,"language":3520,"meta":5},[3517],"it('error message works', () => {\n\n  cy\n    .intercept('POST', '/api/boards', {\n      forceNetworkError: true\n    })\n    .as('boardCreate')\n\n  cy\n    .clock();\n\n  cy\n    .visit('/');\n\n  cy\n    .get('[data-cy=create-board]')\n    .click()\n\n  cy\n    .get('[data-cy=new-board-input]')\n    .type('new garden{enter}')\n\n  cy\n    .get('#errorMessage')\n    .should('be.visible')\n\n  cy\n    .tick(4000)\n\n  cy\n    .get('#errorMessage')\n    .should('not.be.visible')\n\n})\n\n",[20153],{"type":27,"tag":653,"props":20154,"children":20155},{"__ignoreMap":5},[20156],{"type":32,"value":20151},{"type":27,"tag":28,"props":20158,"children":20159},{},[20160,20162,20167,20169,20174,20176,20181,20183],{"type":32,"value":20161},"In this case, we don’t really need to pass a Date object to our ",{"type":27,"tag":653,"props":20163,"children":20165},{"className":20164},[],[20166],{"type":32,"value":20007},{"type":32,"value":20168}," function since we are working with a ",{"type":27,"tag":653,"props":20170,"children":20172},{"className":20171},[],[20173],{"type":32,"value":20086},{"type":32,"value":20175}," function which will just get moved with the ",{"type":27,"tag":653,"props":20177,"children":20179},{"className":20178},[],[20180],{"type":32,"value":20049},{"type":32,"value":20182}," function and we don’t really mind the exact date. Now our test passes immediately:\n",{"type":27,"tag":959,"props":20184,"children":20187},{"alt":20185,"src":20186},"Test is running faster","short.mp4",[],{"type":27,"tag":28,"props":20189,"children":20190},{},[20191],{"type":32,"value":20192},"We have made our function 4 seconds faster and removed the idle time.",{"type":27,"tag":28,"props":20194,"children":20195},{},[20196,20198,20203,20204,20209],{"type":32,"value":20197},"Hope you like this. You can find more cool Cypress material on this blog, just click the \"blog\" section at the top of this page. I write content like this every week, so if you enjoyed it, consider subscribing - I send out an email when a new article comes out. I also share it on social network, so you can follow me on ",{"type":27,"tag":172,"props":20199,"children":20201},{"href":5770,"rel":20200},[696],[20202],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":20205,"children":20207},{"href":10953,"rel":20206},[696],[20208],{"type":32,"value":1598},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":20211},[],"content:make-your-cypress-tests-faster-with-clock:index.md","make-your-cypress-tests-faster-with-clock/index.md","make-your-cypress-tests-faster-with-clock/index",{"_path":19014,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":20216,"description":20217,"slug":20218,"date":20219,"published":10,"tags":20220,"readingTime":20221,"body":20225,"_type":329,"_id":20740,"_source":331,"_file":20741,"_stem":20742,"_extension":334},"Starting with TypeScript in Cypress","TypeScript has been gaining popularity over last couple of years, and for good reason. Learn how you can implement TypeScript features in your tests.","starting-with-typescript-in-cypress","2021-02-22",[2608,5279],{"text":2436,"minutes":20222,"time":20223,"words":20224},7.215,432900,1443,{"type":24,"children":20226,"toc":20733},[20227,20232,20238,20251,20260,20265,20274,20300,20308,20313,20318,20323,20332,20337,20343,20348,20357,20362,20370,20412,20417,20426,20439,20444,20450,20463,20471,20500,20506,20553,20563,20576,20580,20593,20603,20615,20628,20638,20659,20664,20673,20694,20703,20728],{"type":27,"tag":28,"props":20228,"children":20229},{},[20230],{"type":32,"value":20231},"TypeScript has been gaining popularity over last couple of years, and for good reason. It enables developers to create their own types. This helps creating less mistakes and self documenting code. In this article, I’ll show you the basics of TypeScript.",{"type":27,"tag":45,"props":20233,"children":20235},{"id":20234},"simple-use-of-typescript-in-cypress",[20236],{"type":32,"value":20237},"Simple use of Typescript in Cypress",{"type":27,"tag":28,"props":20239,"children":20240},{},[20241,20243,20249],{"type":32,"value":20242},"Let’s start with a simple example. As is usual, I’m going to be using my Trello clone app, ",{"type":27,"tag":172,"props":20244,"children":20246},{"href":19041,"rel":20245},[696],[20247],{"type":32,"value":20248},"which you can fork on GitHub",{"type":32,"value":20250},". In the following code, I have a very simple set of steps. I open my app and create new board:",{"type":27,"tag":793,"props":20252,"children":20255},{"className":20253,"code":20254,"language":1513,"meta":5},[1510],"/// \u003Creference types=\"cypress\" />\n\nit('creating a board', () => {\n\n  cy\n    .visit('/')\n\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=new-board-input]')\n    .type('new board{enter}');\n\n})\n",[20256],{"type":27,"tag":653,"props":20257,"children":20258},{"__ignoreMap":5},[20259],{"type":32,"value":20254},{"type":27,"tag":28,"props":20261,"children":20262},{},[20263],{"type":32,"value":20264},"Since creating a new board is a pretty common action in my tests, I want to pull lines 8-14 into a separate function. This function will be able to take an argument so that we can customize the board name as we use the function.",{"type":27,"tag":793,"props":20266,"children":20269},{"className":20267,"code":20268,"language":1513,"meta":5},[1510],"/// \u003Creference types=\"cypress\" />\n\nconst addBoard = (input) => {\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=new-board-input]')\n    .type(`${input}{enter}`);\n}\n\nit('creating a board', () => {\n\n  cy\n    .visit('/')\n\n  addBoard()\n\n})\n",[20270],{"type":27,"tag":653,"props":20271,"children":20272},{"__ignoreMap":5},[20273],{"type":32,"value":20268},{"type":27,"tag":28,"props":20275,"children":20276},{},[20277,20279,20285,20287,20292,20293,20298],{"type":32,"value":20278},"You might notice, looking at the code, that I left my ",{"type":27,"tag":653,"props":20280,"children":20282},{"className":20281},[],[20283],{"type":32,"value":20284},".addBoard()",{"type":32,"value":20286}," function empty. Since I did not input any text, my test would fail. My app does not allow creating a board with empty name. Let’s now simply switch the extension name from ",{"type":27,"tag":653,"props":20288,"children":20290},{"className":20289},[],[20291],{"type":32,"value":7869},{"type":32,"value":14944},{"type":27,"tag":653,"props":20294,"children":20296},{"className":20295},[],[20297],{"type":32,"value":6717},{"type":32,"value":20299}," and see what happens in our text editor.",{"type":27,"tag":28,"props":20301,"children":20302},{},[20303],{"type":27,"tag":959,"props":20304,"children":20307},{"alt":20305,"src":20306},"Function has an error underline when argument is not provided","ts.png",[],{"type":27,"tag":28,"props":20309,"children":20310},{},[20311],{"type":32,"value":20312},"As you can see, our function now shows an error in our editor. I made the screenshot while hovering over our underlined function. VS Code has provided an explanation - our function expects to have at least one argument, but none were provided.",{"type":27,"tag":28,"props":20314,"children":20315},{},[20316],{"type":32,"value":20317},"Nothing is actually preventing us from running this code. But with TypeScript, we get an instant feedback on the validity. Once we see the error, we can easily fix it, by providing our function with an argument.",{"type":27,"tag":28,"props":20319,"children":20320},{},[20321],{"type":32,"value":20322},"Let’s now play with this a little. With TypeScript, you can define what kind of input we want to accept. Since we obviously want the argument to be a string, let’s define a type, by writing it like this:",{"type":27,"tag":793,"props":20324,"children":20327},{"className":20325,"code":20326,"language":3520,"meta":5},[3517],"const addBoard = (input: string) => {\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=new-board-input]')\n    .type(`${input}{enter}`);\n}\n\nit('creating a board', () => {\n\n  cy\n    .visit('/')\n\n  addBoard('new board')\n\n})\n",[20328],{"type":27,"tag":653,"props":20329,"children":20330},{"__ignoreMap":5},[20331],{"type":32,"value":20326},{"type":27,"tag":28,"props":20333,"children":20334},{},[20335],{"type":32,"value":20336},"Now that we have specified a type of our argument, we’ll get an error every time we would try to pass anything else, like boolean or number. For this particular function, it does not seem like much, but imagine a function that will call an API endpoint with bunch of different stuff in it’s payload, like arrays, objects, strings and numbers. We can have types for all that too.",{"type":27,"tag":45,"props":20338,"children":20340},{"id":20339},"lets-play-with-types",[20341],{"type":32,"value":20342},"Let’s play with types",{"type":27,"tag":28,"props":20344,"children":20345},{},[20346],{"type":32,"value":20347},"The options for setting types are really vast. So far I’ve been really just scratching surface since I’m also still learning about this. There’s a lot to learn about TypeScript, so let’s play and experiment. For example, I’d like to change my function in a way that it only takes a certain text.",{"type":27,"tag":793,"props":20349,"children":20352},{"className":20350,"code":20351,"language":3520,"meta":5},[3517],"const addBoard = (input: 'new board' | 'my board') => {\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=new-board-input]')\n    .type(`${input}{enter}`);\n}\n",[20353],{"type":27,"tag":653,"props":20354,"children":20355},{"__ignoreMap":5},[20356],{"type":32,"value":20351},{"type":27,"tag":28,"props":20358,"children":20359},{},[20360],{"type":32,"value":20361},"By adding this, I can specify which types of input will my function take. What’s even nicer, VS Code provides me with autocomplete:",{"type":27,"tag":28,"props":20363,"children":20364},{},[20365],{"type":27,"tag":959,"props":20366,"children":20369},{"alt":20367,"src":20368},"Autocompleting function arguments","autocomplete_uua3td.png",[],{"type":27,"tag":28,"props":20371,"children":20372},{},[20373,20375,20380,20382,20388,20390,20395,20397,20402,20404,20410],{"type":32,"value":20374},"I can imagine there might be some very cool use cases for this. Not sure why, but ",{"type":27,"tag":653,"props":20376,"children":20378},{"className":20377},[],[20379],{"type":32,"value":8674},{"type":32,"value":20381}," attribute selectors come to mind immediately. I have this ",{"type":27,"tag":653,"props":20383,"children":20385},{"className":20384},[],[20386],{"type":32,"value":20387},"getDataCy",{"type":32,"value":20389}," command,that is just a simple wrapper around Cypress ",{"type":27,"tag":653,"props":20391,"children":20393},{"className":20392},[],[20394],{"type":32,"value":12748},{"type":32,"value":20396}," command. It will select element based on ",{"type":27,"tag":653,"props":20398,"children":20400},{"className":20399},[],[20401],{"type":32,"value":8674},{"type":32,"value":20403}," attribute so I don’t have to type the whole ",{"type":27,"tag":653,"props":20405,"children":20407},{"className":20406},[],[20408],{"type":32,"value":20409},"[data-cy=selector]",{"type":32,"value":20411}," text all the time. What if I could autocomplete my selectors? 🤔 If I find out this would be something useful, I’ll let you know on this blog, subscribe down at the bottom of the page if you are interested.",{"type":27,"tag":28,"props":20413,"children":20414},{},[20415],{"type":32,"value":20416},"Let’s now say I want to add multiple boards with this function. Inside the function, I will loop through the array of strings and add all the boards I need. Instead of a single string, we want to be able to input an array of strings instead.",{"type":27,"tag":793,"props":20418,"children":20421},{"className":20419,"code":20420,"language":3520,"meta":5},[3517],"const addTodo = (titles: string[]) => {\n\n  titles.forEach(title => {\n\n    cy\n      .get('[data-cy=\"create-board\"]')\n      .click();\n\n    cy\n      .get('[data-cy=new-board-input]')\n      .type(`${title}{enter}`);\n\n  })\n\n}\n",[20422],{"type":27,"tag":653,"props":20423,"children":20424},{"__ignoreMap":5},[20425],{"type":32,"value":20420},{"type":27,"tag":28,"props":20427,"children":20428},{},[20429,20431,20437],{"type":32,"value":20430},"Whenever I will add something else to my ",{"type":27,"tag":653,"props":20432,"children":20434},{"className":20433},[],[20435],{"type":32,"value":20436},"addTodo",{"type":32,"value":20438}," function instead of array, I will get an error as I type it. So before even running my test, I get a feedback that the way I’m using this function is not valid. This helps tremendously with typos, incorrect types or forgotten commas.",{"type":27,"tag":28,"props":20440,"children":20441},{},[20442],{"type":32,"value":20443},"If your project already uses TypeScript, there’s a good chance that a big part of your project already contains types. You might be able to reuse your types from your app inside your tests. I imagine that in a strict type checking, a change in the app might trigger an error in tests right away.",{"type":27,"tag":45,"props":20445,"children":20447},{"id":20446},"using-jsdoc",[20448],{"type":32,"value":20449},"Using JSDoc",{"type":27,"tag":28,"props":20451,"children":20452},{},[20453,20455,20461],{"type":32,"value":20454},"There’s another cool TypeScript feature, which you can use even with pure JavaScript. With JSDoc, you can add documentation to your functions. In VS Code, you can add your documentation by typing ",{"type":27,"tag":653,"props":20456,"children":20458},{"className":20457},[],[20459],{"type":32,"value":20460},"/** */",{"type":32,"value":20462},". This will create a special comment, that will pop out once you hover over your function.",{"type":27,"tag":28,"props":20464,"children":20465},{},[20466],{"type":27,"tag":959,"props":20467,"children":20470},{"alt":20468,"src":20469},"JSDoc comment pop out on hover","jsdoc.png",[],{"type":27,"tag":28,"props":20472,"children":20473},{},[20474,20476,20482,20484,20490,20492,20498],{"type":32,"value":20475},"There are many different flags, like ",{"type":27,"tag":653,"props":20477,"children":20479},{"className":20478},[],[20480],{"type":32,"value":20481},"@param",{"type":32,"value":20483}," to give explanation for your parameters ",{"type":27,"tag":653,"props":20485,"children":20487},{"className":20486},[],[20488],{"type":32,"value":20489},"@example",{"type":32,"value":20491}," to provide a whole example of usage for that command or ",{"type":27,"tag":653,"props":20493,"children":20495},{"className":20494},[],[20496],{"type":32,"value":20497},"@deprecated",{"type":32,"value":20499}," to mark that command as soon to be obsolete. You can see JSDoc being used with Cypress commands too where they even show a link to a documentation. Imagine using these for your custom commands or your page objects and giving them instant context.",{"type":27,"tag":45,"props":20501,"children":20503},{"id":20502},"setting-up-typescript-in-your-project",[20504],{"type":32,"value":20505},"Setting up TypeScript in your project",{"type":27,"tag":28,"props":20507,"children":20508},{},[20509,20511,20516,20517,20522,20524,20531,20533,20539,20540,20546,20548],{"type":32,"value":20510},"Let’s back up a little. So far, what we have done is just change our file extension from ",{"type":27,"tag":653,"props":20512,"children":20514},{"className":20513},[],[20515],{"type":32,"value":7869},{"type":32,"value":14944},{"type":27,"tag":653,"props":20518,"children":20520},{"className":20519},[],[20521],{"type":32,"value":6717},{"type":32,"value":20523},". But before we would be able to run these tests in Cypress, there are still two more things we need to do. Cypress documentation has a ",{"type":27,"tag":172,"props":20525,"children":20528},{"href":20526,"rel":20527},"https://docs.cypress.io/guides/tooling/typescript-support.html",[696],[20529],{"type":32,"value":20530},"really nice article on this",{"type":32,"value":20532},". To sum it up, you need to do is to install TypeScript via ",{"type":27,"tag":653,"props":20534,"children":20536},{"className":20535},[],[20537],{"type":32,"value":20538},"npm",{"type":32,"value":1591},{"type":27,"tag":653,"props":20541,"children":20543},{"className":20542},[],[20544],{"type":32,"value":20545},"yarn",{"type":32,"value":20547},", and then create a ",{"type":27,"tag":653,"props":20549,"children":20551},{"className":20550},[],[20552],{"type":32,"value":9229},{"type":27,"tag":793,"props":20554,"children":20558},{"className":20555,"code":20556,"highlights":20557,"language":1004,"meta":5},[1002],"{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"es5\", \"dom\"],\n    \"types\": [\"cypress\"]\n  },\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n",[3667],[20559],{"type":27,"tag":653,"props":20560,"children":20561},{"__ignoreMap":5},[20562],{"type":32,"value":20556},{"type":27,"tag":28,"props":20564,"children":20565},{},[20566,20568,20574],{"type":32,"value":20567},"Just copying and pasting this into your project should get you started, but there are tons of options on how to configure this file. Notice how on line 5, we are defining types. This is the exact thing as adding ",{"type":27,"tag":653,"props":20569,"children":20571},{"className":20570},[],[20572],{"type":32,"value":20573},"/// \u003Creference types=\"cypress\" />",{"type":32,"value":20575}," at the beginning of your file to make autocomplete work. With TypeScript, you don’t need to do that, because it will be enabled globally.",{"type":27,"tag":45,"props":20577,"children":20578},{"id":13185},[20579],{"type":32,"value":13188},{"type":27,"tag":28,"props":20581,"children":20582},{},[20583,20585,20591],{"type":32,"value":20584},"Let’s now pull our ",{"type":27,"tag":653,"props":20586,"children":20588},{"className":20587},[],[20589],{"type":32,"value":20590},"addBoard()",{"type":32,"value":20592}," function out of our file and create a custom Cypress command. We will then be able to use it across our project.",{"type":27,"tag":793,"props":20594,"children":20598},{"className":20595,"code":20596,"filename":20597,"language":3520,"meta":5},[3517],"Cypress.Commands.add('addBoard', (input: string) => {\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=new-board-input]')\n    .type(`${input}{enter}`);\n})\n","support/commands/addBoard.ts",[20599],{"type":27,"tag":653,"props":20600,"children":20601},{"__ignoreMap":5},[20602],{"type":32,"value":20596},{"type":27,"tag":28,"props":20604,"children":20605},{},[20606,20608,20613],{"type":32,"value":20607},"This is all nice, but just adding this command will not start autocompleting it when I try to use it in my test. For that, I need to expand the ",{"type":27,"tag":653,"props":20609,"children":20611},{"className":20610},[],[20612],{"type":32,"value":13207},{"type":32,"value":20614}," object.",{"type":27,"tag":28,"props":20616,"children":20617},{},[20618,20620,20626],{"type":32,"value":20619},"There are two ways to do this. The first way can be found in mentioned documentation. All you need to do is to create a definitions file, with the extension ",{"type":27,"tag":653,"props":20621,"children":20623},{"className":20622},[],[20624],{"type":32,"value":20625},".d.ts",{"type":32,"value":20627}," and declare your command there:",{"type":27,"tag":793,"props":20629,"children":20633},{"className":20630,"code":20631,"filename":20632,"language":3520,"meta":5},[3517],"declare namespace Cypress {\n  interface Chainable {\n    addBoard(value: string): void\n  }\n}\n","support/commands.d.ts",[20634],{"type":27,"tag":653,"props":20635,"children":20636},{"__ignoreMap":5},[20637],{"type":32,"value":20631},{"type":27,"tag":28,"props":20639,"children":20640},{},[20641,20643,20649,20651,20657],{"type":32,"value":20642},"There’s a lot to unpack here, but I don’t really want to go into too much detail. Simply put, we are adding our ",{"type":27,"tag":653,"props":20644,"children":20646},{"className":20645},[],[20647],{"type":32,"value":20648},"addBoard",{"type":32,"value":20650}," Cypress command into a Cypress interface. The ",{"type":27,"tag":653,"props":20652,"children":20654},{"className":20653},[],[20655],{"type":32,"value":20656},"void",{"type":32,"value":20658}," at the end of our function just means that our function will not pass anything on. If we wanted, it could return a selected element or a response body from API call, all depending on what we do with the function.",{"type":27,"tag":28,"props":20660,"children":20661},{},[20662],{"type":32,"value":20663},"The second way to add a custom command is to add the definition right inside our custom command. The file will look like this:",{"type":27,"tag":793,"props":20665,"children":20668},{"className":20666,"code":20667,"filename":20597,"language":3520,"meta":5},[3517],"declare global {\n  namespace Cypress {\n    interface Chainable {\n      addBoard: typeof addBoard;\n    }\n  }\n}\n\nexport const addBoard = (input: string) => {\n  cy\n    .get('[data-cy=\"create-board\"]')\n    .click();\n\n  cy\n    .get('[data-cy=new-board-input]')\n    .type(`${input}{enter}`);\n}\n",[20669],{"type":27,"tag":653,"props":20670,"children":20671},{"__ignoreMap":5},[20672],{"type":32,"value":20667},{"type":27,"tag":28,"props":20674,"children":20675},{},[20676,20678,20684,20686,20692],{"type":32,"value":20677},"Since I am not using ",{"type":27,"tag":653,"props":20679,"children":20681},{"className":20680},[],[20682],{"type":32,"value":20683},"Cypress.Commands.add",{"type":32,"value":20685}," api here, but instead I’m exporting a function, I need to add this to my ",{"type":27,"tag":653,"props":20687,"children":20689},{"className":20688},[],[20690],{"type":32,"value":20691},"support/index.ts",{"type":32,"value":20693}," file or add it right into my test. Usually, that looks something like this:",{"type":27,"tag":793,"props":20695,"children":20698},{"className":20696,"code":20697,"filename":20691,"language":3520,"meta":5},[3517],"import { addBoard } from './commands/addBoard';\n\nCypress.Commands.add('addBoard', addBoard);\n\n",[20699],{"type":27,"tag":653,"props":20700,"children":20701},{"__ignoreMap":5},[20702],{"type":32,"value":20697},{"type":27,"tag":28,"props":20704,"children":20705},{},[20706,20708,20713,20714,20719,20720,20726],{"type":32,"value":20707},"I’ve found this approach while going to GitHub and I kinda like it a little more. It keeps everything inside the same file, which works a little better for me. I think it might be a personal preference. It may also be that I’m not seeing some obvious problem with it (I’m still learning 🙂), in which case feel free to ping me on ",{"type":27,"tag":172,"props":20709,"children":20711},{"href":5770,"rel":20710},[696],[20712],{"type":32,"value":1589},{"type":32,"value":3372},{"type":27,"tag":172,"props":20715,"children":20717},{"href":10953,"rel":20716},[696],[20718],{"type":32,"value":1598},{"type":32,"value":1591},{"type":27,"tag":172,"props":20721,"children":20724},{"href":20722,"rel":20723},"https://bit.ly/cy-discord",[696],[20725],{"type":32,"value":10970},{"type":32,"value":20727},". I’d be happy to learn.",{"type":27,"tag":28,"props":20729,"children":20730},{},[20731],{"type":32,"value":20732},"If you enjoyed this, you might be interested to know that I post articles like this every week and send a short email when I do. On the bottom of this page you can subscribe and get new article notifications.",{"title":5,"searchDepth":320,"depth":320,"links":20734},[20735,20736,20737,20738,20739],{"id":20234,"depth":320,"text":20237},{"id":20339,"depth":320,"text":20342},{"id":20446,"depth":320,"text":20449},{"id":20502,"depth":320,"text":20505},{"id":13185,"depth":320,"text":13188},"content:starting-with-typescript-in-cypress:index.md","starting-with-typescript-in-cypress/index.md","starting-with-typescript-in-cypress/index",{"_path":20744,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":20745,"description":20746,"date":20747,"published":10,"slug":20748,"tags":20749,"cypressVersion":5959,"readingTime":20754,"body":20758,"_type":329,"_id":21083,"_source":331,"_file":21084,"_stem":21085,"_extension":334},"/cypress-basics-before-beforeeach-after-aftereach","Cypress basics: before(), beforeEach(), after() and afterEach()","Mocha hooks can help you tremendously when trying to avoid repetition in your tests. This article explains how these hooks work and how to use them effectively.","2021-02-15","cypress-basics-before-beforeeach-after-aftereach",[5279,13407,20750,20751,20752,20753],"mocha","hooks","before","beforeEach",{"text":927,"minutes":20755,"time":20756,"words":20757},4.125,247500,825,{"type":24,"children":20759,"toc":21076},[20760,20770,20776,20787,20796,20808,20817,20831,20862,20868,20873,20882,20914,20919,20937,20942,20948,20966,20975,20985,20996,21002,21027,21036,21041,21047,21066,21071],{"type":27,"tag":1029,"props":20761,"children":20762},{},[20763,20767],{"type":27,"tag":28,"props":20764,"children":20765},{},[20766],{"type":32,"value":5973},{"type":27,"tag":5975,"props":20768,"children":20769},{},[],{"type":27,"tag":45,"props":20771,"children":20773},{"id":20772},"basics-before-and-beforeeach",[20774],{"type":32,"value":20775},"Basics - before() and beforeEach()",{"type":27,"tag":28,"props":20777,"children":20778},{},[20779,20781,20786],{"type":32,"value":20780},"Let’s say you have a spec that has a couple of tests in it. In these tests you are opening a page and testing some functionality. Each time you want to open that page using ",{"type":27,"tag":653,"props":20782,"children":20784},{"className":20783},[],[20785],{"type":32,"value":14057},{"type":32,"value":13730},{"type":27,"tag":793,"props":20788,"children":20791},{"className":20789,"code":20790,"language":1513,"meta":5},[1510],"it('test #1', () => {\n\n  cy.visit('/')\n  // rest of your test\n\n})\n\nit('test #2', () => {\n\n  cy.visit('/')\n  // rest of your test\n\n})\n",[20792],{"type":27,"tag":653,"props":20793,"children":20794},{"__ignoreMap":5},[20795],{"type":32,"value":20790},{"type":27,"tag":28,"props":20797,"children":20798},{},[20799,20801,20806],{"type":32,"value":20800},"With couple of tests, things might get somehow repetitive. For this case, you might instead use a ",{"type":27,"tag":653,"props":20802,"children":20804},{"className":20803},[],[20805],{"type":32,"value":8434},{"type":32,"value":20807}," hook, that will open up your page before all of your tests:",{"type":27,"tag":793,"props":20809,"children":20812},{"className":20810,"code":20811,"language":1513,"meta":5},[1510],"\nbefore(() => {\n\n  cy.visit('/')\n\n})\n\nit('test #1', () => {\n  // rest of your test\n})\n\nit('test #2', () => {\n  // rest of your test\n})\n",[20813],{"type":27,"tag":653,"props":20814,"children":20815},{"__ignoreMap":5},[20816],{"type":32,"value":20811},{"type":27,"tag":28,"props":20818,"children":20819},{},[20820,20822,20829],{"type":32,"value":20821},"Bear in mind that ",{"type":27,"tag":172,"props":20823,"children":20826},{"href":20824,"rel":20825},"https://docs.cypress.io/guides/core-concepts/test-isolation",[696],[20827],{"type":32,"value":20828},"Cypress clears out the state of browser",{"type":32,"value":20830}," in between tests. Coming with version 12, it even visits an empty page so that there’s a firm test isolation. There are ways to configure this behavior.",{"type":27,"tag":1029,"props":20832,"children":20833},{},[20834],{"type":27,"tag":28,"props":20835,"children":20836},{},[20837,20839,20845,20847,20853,20855,20860],{"type":32,"value":20838},"One thing to note here - ",{"type":27,"tag":653,"props":20840,"children":20842},{"className":20841},[],[20843],{"type":32,"value":20844},"test #2",{"type":32,"value":20846}," should be independent on the result of ",{"type":27,"tag":653,"props":20848,"children":20850},{"className":20849},[],[20851],{"type":32,"value":20852},"test #1",{"type":32,"value":20854},". If this is not the case, you will create a domino effect for all the tests in that spec. It is a good practice to isolate your tests in such a way that tests don’t affect each other. For this, you might find ",{"type":27,"tag":653,"props":20856,"children":20858},{"className":20857},[],[20859],{"type":32,"value":7243},{"type":32,"value":20861}," hook more useful. This may require you to structure your tests in a certain way, but will help you gain overall test stability.",{"type":27,"tag":45,"props":20863,"children":20865},{"id":20864},"after-and-aftereach",[20866],{"type":32,"value":20867},"after() and afterEach()",{"type":27,"tag":28,"props":20869,"children":20870},{},[20871],{"type":32,"value":20872},"Similarly to previous hooks, there’s a way to do an action before after your tests finish. It’s commonly used for doing a cleanup after your test is finish, which might look something like this:",{"type":27,"tag":793,"props":20874,"children":20877},{"className":20875,"code":20876,"language":1513,"meta":5},[1510],"after(() => {\n\n  resetDb()\n\n})\n\nit('test #1', () => {\n  // your test\n})\n\nit('test #2', () => {\n  // your test\n})\n",[20878],{"type":27,"tag":653,"props":20879,"children":20880},{"__ignoreMap":5},[20881],{"type":32,"value":20876},{"type":27,"tag":28,"props":20883,"children":20884},{},[20885,20887,20893,20894,20899,20901,20906,20907,20912],{"type":32,"value":20886},"This follows some good principles, as you clearly have a goal of cleaning up the data your tests generate. There are some reasons though, why you might want to reconsider using ",{"type":27,"tag":653,"props":20888,"children":20890},{"className":20889},[],[20891],{"type":32,"value":20892},"after()",{"type":32,"value":4164},{"type":27,"tag":653,"props":20895,"children":20897},{"className":20896},[],[20898],{"type":32,"value":7331},{"type":32,"value":20900}," and use ",{"type":27,"tag":653,"props":20902,"children":20904},{"className":20903},[],[20905],{"type":32,"value":8434},{"type":32,"value":4164},{"type":27,"tag":653,"props":20908,"children":20910},{"className":20909},[],[20911],{"type":32,"value":7243},{"type":32,"value":20913}," instead.",{"type":27,"tag":28,"props":20915,"children":20916},{},[20917],{"type":32,"value":20918},"First of all, when writing your test in GUI mode, after a test is finished, you can keep interacting with your page. With data deleted, you may lose that option as your data might no longer be available to you.",{"type":27,"tag":28,"props":20920,"children":20921},{},[20922,20924,20929,20930,20935],{"type":32,"value":20923},"Second, the action that is happening in ",{"type":27,"tag":653,"props":20925,"children":20927},{"className":20926},[],[20928],{"type":32,"value":20892},{"type":32,"value":1591},{"type":27,"tag":653,"props":20931,"children":20933},{"className":20932},[],[20934],{"type":32,"value":7331},{"type":32,"value":20936}," log might error. In that case, you may face a similar domino effect as described couple of paragraphs earlier.",{"type":27,"tag":28,"props":20938,"children":20939},{},[20940],{"type":32,"value":20941},"There are of course situations where these hooks are very useful, like collecting data gathered from tests etc.",{"type":27,"tag":45,"props":20943,"children":20945},{"id":20944},"nested-before-and-beforeeach-hooks",[20946],{"type":32,"value":20947},"Nested before() and beforeEach() hooks",{"type":27,"tag":28,"props":20949,"children":20950},{},[20951,20953,20958,20959,20964],{"type":32,"value":20952},"Let’s now say you have multiple hooks and different ",{"type":27,"tag":653,"props":20954,"children":20956},{"className":20955},[],[20957],{"type":32,"value":7173},{"type":32,"value":4164},{"type":27,"tag":653,"props":20960,"children":20962},{"className":20961},[],[20963],{"type":32,"value":7187},{"type":32,"value":20965}," blocks. This is where it might get a little confusing at first, but becomes very clear when you know how it works. Consider following code:",{"type":27,"tag":793,"props":20967,"children":20970},{"className":20968,"code":20969,"language":1513,"meta":5},[1510],"before(() => {\n\n  cy.log('orange')\n\n})\n\nbeforeEach(() => {\n\n  cy.log('banana')\n\n})\n\ndescribe('group #1', () => {\n\n  before(() => {\n\n    cy.log('apple')\n\n  })\n\n  beforeEach(() => {\n\n    cy.log('cherry')\n\n  })\n\n  it('test #1', () => {\n      // your test\n  })\n\n})\n",[20971],{"type":27,"tag":653,"props":20972,"children":20973},{"__ignoreMap":5},[20974],{"type":32,"value":20969},{"type":27,"tag":28,"props":20976,"children":20977},{},[20978,20980],{"type":32,"value":20979},"Do you know in what order will these logs be called? The correct answer is this:\n",{"type":27,"tag":959,"props":20981,"children":20984},{"alt":20982,"src":20983},"before and beforeEach hooks","hooks.png",[],{"type":27,"tag":28,"props":20986,"children":20987},{},[20988,20990,20995],{"type":32,"value":20989},"Compare the order of which we have written these in our test and in which these tests are executed. I like to think of all hooks as being \"squashed\" together before executing a test or a ",{"type":27,"tag":653,"props":20991,"children":20993},{"className":20992},[],[20994],{"type":32,"value":7173},{"type":32,"value":8630},{"type":27,"tag":45,"props":20997,"children":20999},{"id":20998},"using-beforeeach-block-in-supportindexjs",[21000],{"type":32,"value":21001},"Using beforeEach block in support/index.js",{"type":27,"tag":28,"props":21003,"children":21004},{},[21005,21007,21012,21014,21019,21021,21026],{"type":32,"value":21006},"Sometimes I like to use these blocks to run a \"global\" ",{"type":27,"tag":653,"props":21008,"children":21010},{"className":21009},[],[21011],{"type":32,"value":7243},{"type":32,"value":21013}," block that I want to run before all of my tests. I describe one case like this in my blog about ",{"type":27,"tag":172,"props":21015,"children":21016},{"href":15399},[21017],{"type":32,"value":21018},"handling data from API",{"type":32,"value":21020},". I create a storage for myself, which I erase before each of my tests. I use my ",{"type":27,"tag":653,"props":21022,"children":21024},{"className":21023},[],[21025],{"type":32,"value":17310},{"type":32,"value":1078},{"type":27,"tag":793,"props":21028,"children":21031},{"className":21029,"code":21030,"filename":17310,"language":1513,"meta":5},[1510],"beforeEach(() => {\n\n  Cypress.env('boards', []);\n  Cypress.env('lists', []);\n\n});\n",[21032],{"type":27,"tag":653,"props":21033,"children":21034},{"__ignoreMap":5},[21035],{"type":32,"value":21030},{"type":27,"tag":28,"props":21037,"children":21038},{},[21039],{"type":32,"value":21040},"In my tests, I add data to these empty arrays as needed.",{"type":27,"tag":45,"props":21042,"children":21044},{"id":21043},"what-should-you-put-in-these-blocks",[21045],{"type":32,"value":21046},"What should you put in these blocks?",{"type":27,"tag":28,"props":21048,"children":21049},{},[21050,21052,21057,21059,21065],{"type":32,"value":21051},"I like to think of these hooks as a preparation state of my test. I rarely put ",{"type":27,"tag":653,"props":21053,"children":21055},{"className":21054},[],[21056],{"type":32,"value":14057},{"type":32,"value":21058}," in there, which means more repetition, but a clearer view of what I am trying to do in my test. I often use these hooks to fire a couple of API requests, seed data, set authorization cookie or do some similar action that does not require app to be open in the browser. There’s a great post by a friend of mine, Martin Škarbala from kiwi.com about how you can speed up your tests by ",{"type":27,"tag":172,"props":21060,"children":21062},{"href":21061},"https://code.kiwi.com/skip-the-ui-using-api-calls-d358b9b61b91",[21063],{"type":32,"value":21064},"skipping UI and use API instead",{"type":32,"value":256},{"type":27,"tag":28,"props":21067,"children":21068},{},[21069],{"type":32,"value":21070},"Hope this helped, if that was the case, pass it on and share it on your social networks. You’ll help my blog grow and I will be very thankful for that.",{"type":27,"tag":28,"props":21072,"children":21073},{},[21074],{"type":32,"value":21075},"If you liked this article, more are coming! I write one every week so you can sign up for a newsletter at the bottom of this page and get notified once a new one is out.",{"title":5,"searchDepth":320,"depth":320,"links":21077},[21078,21079,21080,21081,21082],{"id":20772,"depth":320,"text":20775},{"id":20864,"depth":320,"text":20867},{"id":20944,"depth":320,"text":20947},{"id":20998,"depth":320,"text":21001},{"id":21043,"depth":320,"text":21046},"content:cypress-basics-before-beforeeach-after-aftereach:index.md","cypress-basics-before-beforeeach-after-aftereach/index.md","cypress-basics-before-beforeeach-after-aftereach/index",{"_path":21087,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":21088,"description":21089,"date":21090,"published":10,"slug":21091,"author":21092,"cypressVersion":21093,"tags":21094,"readingTime":21098,"body":21102,"_type":329,"_id":21547,"_source":331,"_file":21548,"_stem":21549,"_extension":334},"/skip-test-conditionally-with-cypress","Skip test conditionally with Cypress","Description of various ways of how you can filter your tests, run them based on a given condition or skip them altogether in Cypress.","2021-02-08","skip-test-conditionally-with-cypress","Filip Hric","v9.6.0",[21095,21096,5279,21097],"skip","grep","conditional",{"text":927,"minutes":21099,"time":21100,"words":21101},4.02,241200,804,{"type":24,"children":21103,"toc":21539},[21104,21117,21123,21155,21164,21175,21184,21212,21221,21227,21239,21248,21267,21276,21324,21333,21346,21355,21384,21390,21395,21404,21409,21418,21423,21429,21443,21452,21457,21466,21472,21484,21493,21498,21507,21513,21534],{"type":27,"tag":28,"props":21105,"children":21106},{},[21107,21109,21115],{"type":32,"value":21108},"In this blog, I’d like to show you various strategies and tools how you can skip your test and run them conditionally. In the past, I wrote a blogpost on how you can ",{"type":27,"tag":172,"props":21110,"children":21112},{"href":21111},"/test-grepping-in-cypress-using-module-api",[21113],{"type":32,"value":21114},"grep your tests using module API.",{"type":32,"value":21116}," You might like that as well.",{"type":27,"tag":45,"props":21118,"children":21120},{"id":21119},"skip-and-only",[21121],{"type":32,"value":21122},".skip and .only",{"type":27,"tag":28,"props":21124,"children":21125},{},[21126,21128,21134,21135,21141,21143,21148,21149],{"type":32,"value":21127},"The easiest way to skip or filter a test is to use ",{"type":27,"tag":653,"props":21129,"children":21131},{"className":21130},[],[21132],{"type":32,"value":21133},".only",{"type":32,"value":4164},{"type":27,"tag":653,"props":21136,"children":21138},{"className":21137},[],[21139],{"type":32,"value":21140},".skip",{"type":32,"value":21142}," functions. You can use them with multiple tests in a single spec, so that you’ll run only those test that you want. This code will tun only ",{"type":27,"tag":653,"props":21144,"children":21146},{"className":21145},[],[21147],{"type":32,"value":20852},{"type":32,"value":4164},{"type":27,"tag":653,"props":21150,"children":21152},{"className":21151},[],[21153],{"type":32,"value":21154},"test #3",{"type":27,"tag":793,"props":21156,"children":21159},{"className":21157,"code":21158,"language":1513,"meta":5},[1510],"it.only('test #1', () => {\n  // ...\n})\nit('test #2', () => {\n  // ...\n})\nit.only('test #3', () => {\n  // ...\n})\n",[21160],{"type":27,"tag":653,"props":21161,"children":21162},{"__ignoreMap":5},[21163],{"type":32,"value":21158},{"type":27,"tag":28,"props":21165,"children":21166},{},[21167,21169,21174],{"type":32,"value":21168},"And this test will only run ",{"type":27,"tag":653,"props":21170,"children":21172},{"className":21171},[],[21173],{"type":32,"value":20852},{"type":32,"value":256},{"type":27,"tag":793,"props":21176,"children":21179},{"className":21177,"code":21178,"language":1513,"meta":5},[1510],"it('test #1', () => {\n  // ...\n})\nit.skip('test #2', () => {\n  // ...\n})\nit.skip('test #2', () => {\n  // ...\n})\n",[21180],{"type":27,"tag":653,"props":21181,"children":21182},{"__ignoreMap":5},[21183],{"type":32,"value":21178},{"type":27,"tag":28,"props":21185,"children":21186},{},[21187,21189,21194,21196,21203,21205,21210],{"type":32,"value":21188},"These work whether you use them in GUI or CI. By the way, to avoid accidentally leaving ",{"type":27,"tag":653,"props":21190,"children":21192},{"className":21191},[],[21193],{"type":32,"value":21133},{"type":32,"value":21195}," in your test, make sure you setup precommit hook that will prevent this, or simply install ",{"type":27,"tag":172,"props":21197,"children":21200},{"href":21198,"rel":21199},"https://www.npmjs.com/package/stop-only",[696],[21201],{"type":32,"value":21202},"this nice plugin.",{"type":32,"value":21204}," You can also use these keywords for your ",{"type":27,"tag":653,"props":21206,"children":21208},{"className":21207},[],[21209],{"type":32,"value":15742},{"type":32,"value":21211}," blocks. In that case, tests will work like this:",{"type":27,"tag":793,"props":21213,"children":21216},{"className":21214,"code":21215,"language":1513,"meta":5},[1510],"describe.only('suite #1', () => {\n\n  it('test #1', () => {\n    // this test will run\n  });\n\n})\n\ndescribe('suite #2', () => {\n\n  it('test #2', () => {\n    // this test will not run\n  })\n\n})\n",[21217],{"type":27,"tag":653,"props":21218,"children":21219},{"__ignoreMap":5},[21220],{"type":32,"value":21215},{"type":27,"tag":45,"props":21222,"children":21224},{"id":21223},"config-file",[21225],{"type":32,"value":21226},"Config file",{"type":27,"tag":28,"props":21228,"children":21229},{},[21230,21232,21237],{"type":32,"value":21231},"In your ",{"type":27,"tag":653,"props":21233,"children":21235},{"className":21234},[],[21236],{"type":32,"value":6028},{"type":32,"value":21238}," you can specify which tests you want to run or skip. You can use the name of your tests or use minimatch to pick which tests should run. Let's say you have three tests:",{"type":27,"tag":793,"props":21240,"children":21243},{"className":21241,"code":21242,"language":2250,"meta":5},[2248],"test1.ts\ntest2.ts\ntest.smoke.ts\n",[21244],{"type":27,"tag":653,"props":21245,"children":21246},{"__ignoreMap":5},[21247],{"type":32,"value":21242},{"type":27,"tag":28,"props":21249,"children":21250},{},[21251,21253,21259,21261,21266],{"type":32,"value":21252},"To just run ",{"type":27,"tag":653,"props":21254,"children":21256},{"className":21255},[],[21257],{"type":32,"value":21258},"test1",{"type":32,"value":21260},", you'd put this in your ",{"type":27,"tag":653,"props":21262,"children":21264},{"className":21263},[],[21265],{"type":32,"value":6028},{"type":32,"value":7538},{"type":27,"tag":793,"props":21268,"children":21271},{"className":21269,"code":21270,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    specPattern: 'cypress/e2e/test1.ts'\n  }\n})\n",[21272],{"type":27,"tag":653,"props":21273,"children":21274},{"__ignoreMap":5},[21275],{"type":32,"value":21270},{"type":27,"tag":28,"props":21277,"children":21278},{},[21279,21281,21286,21288,21294,21296,21302,21304,21310,21311,21316,21318,21323],{"type":32,"value":21280},"If you use subfolders in your ",{"type":27,"tag":653,"props":21282,"children":21284},{"className":21283},[],[21285],{"type":32,"value":6684},{"type":32,"value":21287}," folder, you can select all tests with the same name inside all of those folder by passing ",{"type":27,"tag":653,"props":21289,"children":21291},{"className":21290},[],[21292],{"type":32,"value":21293},"**/test.ts",{"type":32,"value":21295},". More importantly, if you use a naming convention for your smoke tests, like ",{"type":27,"tag":653,"props":21297,"children":21299},{"className":21298},[],[21300],{"type":32,"value":21301},"test.smoke.ts",{"type":32,"value":21303},", you can filter these by passing ",{"type":27,"tag":653,"props":21305,"children":21307},{"className":21306},[],[21308],{"type":32,"value":21309},"*.smoke.ts",{"type":32,"value":16587},{"type":27,"tag":653,"props":21312,"children":21314},{"className":21313},[],[21315],{"type":32,"value":6668},{"type":32,"value":21317},". So for example this configuration will run just our ",{"type":27,"tag":653,"props":21319,"children":21321},{"className":21320},[],[21322],{"type":32,"value":21301},{"type":32,"value":7538},{"type":27,"tag":793,"props":21325,"children":21328},{"className":21326,"code":21327,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    specPattern: 'cypress/e2e/*.smoke.ts'\n  }\n})\n",[21329],{"type":27,"tag":653,"props":21330,"children":21331},{"__ignoreMap":5},[21332],{"type":32,"value":21327},{"type":27,"tag":28,"props":21334,"children":21335},{},[21336,21338,21344],{"type":32,"value":21337},"All these principles apply to skipping tests as well. For ignoring your test files, use ",{"type":27,"tag":653,"props":21339,"children":21341},{"className":21340},[],[21342],{"type":32,"value":21343},"excludeSpecPattern",{"type":32,"value":21345},". You can also pass arrays to these attributes. These can contain either test name or minimatch, so you can do stuff like:",{"type":27,"tag":793,"props":21347,"children":21350},{"className":21348,"code":21349,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    excludeSpecPattern: [\"cypress/e2e/test1.ts\", \"cypress/e2e/*.smoke.ts\"]\n  }\n})\n",[21351],{"type":27,"tag":653,"props":21352,"children":21353},{"__ignoreMap":5},[21354],{"type":32,"value":21349},{"type":27,"tag":28,"props":21356,"children":21357},{},[21358,21360,21366,21368,21375,21376,21383],{"type":32,"value":21359},"This configuration will run only our ",{"type":27,"tag":653,"props":21361,"children":21363},{"className":21362},[],[21364],{"type":32,"value":21365},"test2.ts",{"type":32,"value":21367}," file. To get a better understanding of how minimatch works, you can experiment on ",{"type":27,"tag":172,"props":21369,"children":21372},{"href":21370,"rel":21371},"https://globster.xyz",[696],[21373],{"type":32,"value":21374},"globster.xyz",{"type":32,"value":7692},{"type":27,"tag":172,"props":21377,"children":21380},{"href":21378,"rel":21379},"https://pthrasher.github.io/minimatch-test/",[696],[21381],{"type":32,"value":21382},"this nice little site",{"type":32,"value":256},{"type":27,"tag":45,"props":21385,"children":21387},{"id":21386},"test-configuration",[21388],{"type":32,"value":21389},"Test configuration",{"type":27,"tag":28,"props":21391,"children":21392},{},[21393],{"type":32,"value":21394},"If you want to add more complex conditions to your tests, you can use spec configuration. For example, you can configure your test to run only in a certain browser.",{"type":27,"tag":793,"props":21396,"children":21399},{"className":21397,"code":21398,"language":1513,"meta":5},[1510],"it('will run only on chrome', { browser: 'chrome' }, () => {\n    // ...\n  });\n\nit('will run only on firefox', { browser: 'firefox' }, () => {\n  // ...\n});\n",[21400],{"type":27,"tag":653,"props":21401,"children":21402},{"__ignoreMap":5},[21403],{"type":32,"value":21398},{"type":27,"tag":28,"props":21405,"children":21406},{},[21407],{"type":32,"value":21408},"This test configuration enables you to do much more, but as far as test skipping and filtering goes, this is it. But then again, Cypress tests are all JavaScript. Nothing stops you from writing a condition right inside your spec file:",{"type":27,"tag":793,"props":21410,"children":21413},{"className":21411,"code":21412,"language":1513,"meta":5},[1510],"if (Cypress.config('viewportWidth') > 350) {\n\n  it('does not run on mobile viewports', () => {\n    // ...\n  });\n\n}\n",[21414],{"type":27,"tag":653,"props":21415,"children":21416},{"__ignoreMap":5},[21417],{"type":32,"value":21412},{"type":27,"tag":28,"props":21419,"children":21420},{},[21421],{"type":32,"value":21422},"This test will only run when the set viewport is larger than 350 pixels.",{"type":27,"tag":45,"props":21424,"children":21426},{"id":21425},"plugin",[21427],{"type":32,"value":21428},"Plugin",{"type":27,"tag":28,"props":21430,"children":21431},{},[21432,21434,21441],{"type":32,"value":21433},"There’s a ",{"type":27,"tag":172,"props":21435,"children":21438},{"href":21436,"rel":21437},"https://github.com/cypress-io/cypress-skip-test",[696],[21439],{"type":32,"value":21440},"really nice plugin",{"type":32,"value":21442}," that enables you to skip your tests based on various conditions. It makes it easier to run certain tests only on Mac or only on Windows, as well as skip them using these conditions. It looks something like this:",{"type":27,"tag":793,"props":21444,"children":21447},{"className":21445,"code":21446,"language":1513,"meta":5},[1510],"it('runs only on mac', () => {\n  cy.onlyOn('mac')\n  // ...\n})\n",[21448],{"type":27,"tag":653,"props":21449,"children":21450},{"__ignoreMap":5},[21451],{"type":32,"value":21446},{"type":27,"tag":28,"props":21453,"children":21454},{},[21455],{"type":32,"value":21456},"There are more cool examples on readme page, make sure you check them out. What’s even cooler, you can set any condition you like with this plugin, so our mobile-only example from above would be written like this:",{"type":27,"tag":793,"props":21458,"children":21461},{"className":21459,"code":21460,"language":1513,"meta":5},[1510],"it('does not run on mobile viewports', () => {\n  cy.skipOn(Cypress.config('viewportWidth') \u003C 350);\n  // ...\n})\n",[21462],{"type":27,"tag":653,"props":21463,"children":21464},{"__ignoreMap":5},[21465],{"type":32,"value":21460},{"type":27,"tag":45,"props":21467,"children":21469},{"id":21468},"using-cli",[21470],{"type":32,"value":21471},"Using CLI",{"type":27,"tag":28,"props":21473,"children":21474},{},[21475,21477,21482],{"type":32,"value":21476},"Similarly to our ",{"type":27,"tag":653,"props":21478,"children":21480},{"className":21479},[],[21481],{"type":32,"value":6028},{"type":32,"value":21483}," configuration, we can pass CLI arguments to run only those tests we want. To run only smoke tests we'll run:",{"type":27,"tag":793,"props":21485,"children":21488},{"className":21486,"code":21487,"language":1084,"meta":5},[1082],"npx cypress run --spec 'cypress/e2e/*.smoke.ts'\n",[21489],{"type":27,"tag":653,"props":21490,"children":21491},{"__ignoreMap":5},[21492],{"type":32,"value":21487},{"type":27,"tag":28,"props":21494,"children":21495},{},[21496],{"type":32,"value":21497},"To run all tests except the smoke ones, we'll run a command like this:",{"type":27,"tag":793,"props":21499,"children":21502},{"className":21500,"code":21501,"language":1084,"meta":5},[1082],"npx cypress run --spec 'cypress/e2e/*[!.smoke].ts'\n",[21503],{"type":27,"tag":653,"props":21504,"children":21505},{"__ignoreMap":5},[21506],{"type":32,"value":21501},{"type":27,"tag":45,"props":21508,"children":21510},{"id":21509},"your-own-logic",[21511],{"type":32,"value":21512},"Your own logic",{"type":27,"tag":28,"props":21514,"children":21515},{},[21516,21518,21523,21525,21532],{"type":32,"value":21517},"There are quite a lot options you can use out there. I described one of them ",{"type":27,"tag":172,"props":21519,"children":21520},{"href":21111},[21521],{"type":32,"value":21522},"in my blog",{"type":32,"value":21524},". With Module API, there’s really nothing stopping you from organizing your test suite in various different ways. There’s also an option for grepping all of your tests ",{"type":27,"tag":172,"props":21526,"children":21529},{"href":21527,"rel":21528},"https://github.com/bahmutov/cypress-select-tests",[696],[21530],{"type":32,"value":21531},"via plugin",{"type":32,"value":21533},". Whichever you choose!",{"type":27,"tag":28,"props":21535,"children":21536},{},[21537],{"type":32,"value":21538},"If you liked this blog, you can help me grow it by sharing it on your favourite social network. I write posts like these every week, so if you like them, you can subscribe to my newsletter down below this article.",{"title":5,"searchDepth":320,"depth":320,"links":21540},[21541,21542,21543,21544,21545,21546],{"id":21119,"depth":320,"text":21122},{"id":21223,"depth":320,"text":21226},{"id":21386,"depth":320,"text":21389},{"id":21425,"depth":320,"text":21428},{"id":21468,"depth":320,"text":21471},{"id":21509,"depth":320,"text":21512},"content:skip-test-conditionally-with-cypress:index.md","skip-test-conditionally-with-cypress/index.md","skip-test-conditionally-with-cypress/index",{"_path":21551,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":21552,"description":21553,"date":21554,"published":10,"slug":21555,"tags":21556,"readingTime":21557,"body":21561,"_type":329,"_id":21837,"_source":331,"_file":21838,"_stem":21839,"_extension":334},"/cypress-basics-check-if-element-exists","Cypress basics: check if element exists","In this article we are exploring ways to assert visibility of an element on a page. There are couple of gotchas that may be confusing at times.","2021-02-01","cypress-basics-check-if-element-exists",[5279,13407,18668],{"text":585,"minutes":21558,"time":21559,"words":21560},3.845,230700,769,{"type":24,"children":21562,"toc":21832},[21563,21568,21578,21590,21596,21601,21610,21615,21623,21628,21634,21639,21649,21657,21682,21687,21696,21702,21713,21721,21734,21746,21751,21756,21765,21777,21782,21791,21810],{"type":27,"tag":28,"props":21564,"children":21565},{},[21566],{"type":32,"value":21567},"One of the first things you might want to test in your app with Cypress is element presence. In this article I’d like to take a look into how test if element exists, is visible and discuss some gotchas that might occur during some of these tests.",{"type":27,"tag":1029,"props":21569,"children":21570},{},[21571,21575],{"type":27,"tag":28,"props":21572,"children":21573},{},[21574],{"type":32,"value":5973},{"type":27,"tag":5975,"props":21576,"children":21577},{},[],{"type":27,"tag":28,"props":21579,"children":21580},{},[21581,21583,21588],{"type":32,"value":21582},"During this blog, I will be using my Trello clone app. You can ",{"type":27,"tag":172,"props":21584,"children":21585},{"href":19041},[21586],{"type":32,"value":21587},"clone it from GitHub",{"type":32,"value":21589}," and follow along with this blog.",{"type":27,"tag":45,"props":21591,"children":21593},{"id":21592},"check-visibility",[21594],{"type":32,"value":21595},"Check visibility",{"type":27,"tag":28,"props":21597,"children":21598},{},[21599],{"type":32,"value":21600},"Let’s start with the simplest use case. On our page we have a list of boards. I’ll check the visibility of my board with following code:",{"type":27,"tag":793,"props":21602,"children":21605},{"className":21603,"code":21604,"language":3520,"meta":5},[3517],"it('has a board', () => {\n\n  cy\n    .visit('/');\n\n  cy\n    .get('[data-cy=board-item]')\n    .should('be.visible');\n\n});\n",[21606],{"type":27,"tag":653,"props":21607,"children":21608},{"__ignoreMap":5},[21609],{"type":32,"value":21604},{"type":27,"tag":28,"props":21611,"children":21612},{},[21613],{"type":32,"value":21614},"Our test does the exact thing we would expect. It will check the visibility of our element and pass our test.",{"type":27,"tag":28,"props":21616,"children":21617},{},[21618],{"type":27,"tag":959,"props":21619,"children":21622},{"alt":21620,"src":21621},"Check if element exists","element-visible.mp4",[],{"type":27,"tag":28,"props":21624,"children":21625},{},[21626],{"type":32,"value":21627},"The interesting thing here is that although our element is rendered based on data from network, Cypress’ internal logic has automatic retries implemented, so it will actually wait for an element to render without us having to add any extra command. In other words, even if our element is not yet rendered at the moment of execution, Cypress will wait for it to render.",{"type":27,"tag":45,"props":21629,"children":21631},{"id":21630},"check-non-visibility",[21632],{"type":32,"value":21633},"Check non-visibility",{"type":27,"tag":28,"props":21635,"children":21636},{},[21637],{"type":32,"value":21638},"Let’s now check the exact opposite. I will delete my board and check that it is not visible.",{"type":27,"tag":793,"props":21640,"children":21644},{"className":21641,"code":21642,"highlights":21643,"language":3520,"meta":5},[3517],"it('has a board', () => {\n\n  cy\n    .visit('/');\n\n  cy\n    .get('[data-cy=board-item]')\n    .should('not.be.visible');\n\n});\n",[3723],[21645],{"type":27,"tag":653,"props":21646,"children":21647},{"__ignoreMap":5},[21648],{"type":32,"value":21642},{"type":27,"tag":28,"props":21650,"children":21651},{},[21652],{"type":27,"tag":959,"props":21653,"children":21656},{"alt":21654,"src":21655},"Check if element does not exist","element-not-visible.mp4",[],{"type":27,"tag":28,"props":21658,"children":21659},{},[21660,21662,21667,21668,21674,21676],{"type":32,"value":21661},"Surprisingly, our test has failed now. This is because Cypress actually verifies that element is hidden via css property like ",{"type":27,"tag":653,"props":21663,"children":21665},{"className":21664},[],[21666],{"type":32,"value":11969},{"type":32,"value":1591},{"type":27,"tag":653,"props":21669,"children":21671},{"className":21670},[],[21672],{"type":32,"value":21673},"visibility: hidden",{"type":32,"value":21675},". But in our case, the element we are trying to assert is not even present in our app. That is why our assertion fails. Instead of visibility check, we should be doing an assertion of non-existence, so ",{"type":27,"tag":653,"props":21677,"children":21679},{"className":21678},[],[21680],{"type":32,"value":21681},".should('not.exist')",{"type":27,"tag":28,"props":21683,"children":21684},{},[21685],{"type":32,"value":21686},"Be careful with negative assertions though, because sometimes the reason for that might be that the element was not yet rendered because of a network lag etc. In case you want to assert that an element stops existing, I suggest you first check that the element is visible (or exists) first:",{"type":27,"tag":793,"props":21688,"children":21691},{"className":21689,"code":21690,"language":3520,"meta":5},[3517],"it('deletes a board', () => {\n\n  cy\n    .visit('/');\n\n  cy\n    .get('[data-cy=board-item]')\n    .should('exist');\n\n  cy\n    .request('DELETE', '/boards/2626653025');\n\n  cy\n    .get('[data-cy=board-item]')\n    .should('not.exist');\n\n});\n",[21692],{"type":27,"tag":653,"props":21693,"children":21694},{"__ignoreMap":5},[21695],{"type":32,"value":21690},{"type":27,"tag":45,"props":21697,"children":21699},{"id":21698},"checking-visibility-on-viewport",[21700],{"type":32,"value":21701},"Checking visibility on viewport",{"type":27,"tag":28,"props":21703,"children":21704},{},[21705,21707,21712],{"type":32,"value":21706},"Let’s now create a long list of boards in my list. I will check visibility of all these. A slightly unexpected thing happens. My assertion still passes, but I will get a warning on my ",{"type":27,"tag":653,"props":21708,"children":21710},{"className":21709},[],[21711],{"type":32,"value":12748},{"type":32,"value":19455},{"type":27,"tag":28,"props":21714,"children":21715},{},[21716],{"type":27,"tag":959,"props":21717,"children":21720},{"alt":21718,"src":21719},"Matched elements","matched-elements.png",[],{"type":27,"tag":28,"props":21722,"children":21723},{},[21724,21726,21732],{"type":32,"value":21725},"This is a good thing to have in mind when making assertions on multiple elements at once. It’s important to understand how an element is considered visible from perspective of browser. In our app, we have a container element that has a property ",{"type":27,"tag":653,"props":21727,"children":21729},{"className":21728},[],[21730],{"type":32,"value":21731},"overflow: scroll",{"type":32,"value":21733},". If that wasn’t the case, Cypress would declare all my elements visible. Even the last one.",{"type":27,"tag":28,"props":21735,"children":21736},{},[21737,21739,21744],{"type":32,"value":21738},"The difference that the ",{"type":27,"tag":653,"props":21740,"children":21742},{"className":21741},[],[21743],{"type":32,"value":21731},{"type":32,"value":21745}," makes is actually important. Without it, my list would stretch as far as I need. Even though I couldn’t see all my elements because of my browser height, they would still be considered visible. But the case changes if I decide that user will need to scroll to see the elements that are overflowing the height of our container. Some elements may not be visible.",{"type":27,"tag":28,"props":21747,"children":21748},{},[21749],{"type":32,"value":21750},"If placing elements on a page is an issue for your use case (e.g. you need to have your homepage to be pixel-perfect), I suggest rather testing this with a visual test. The human-eye definitions on visibility might be slightly different in cases like this.",{"type":27,"tag":28,"props":21752,"children":21753},{},[21754],{"type":32,"value":21755},"That said, we can still check non-visibility of our last element, that is hidden from viewport:",{"type":27,"tag":793,"props":21757,"children":21760},{"className":21758,"code":21759,"language":3520,"meta":5},[3517],"it('has a board', () => {\n\n  cy\n    .visit('/');\n\n  cy\n    .get('[data-cy=board-item]')\n    .last()\n    .should('not.be.visible');\n\n});\n",[21761],{"type":27,"tag":653,"props":21762,"children":21763},{"__ignoreMap":5},[21764],{"type":32,"value":21759},{"type":27,"tag":28,"props":21766,"children":21767},{},[21768,21770,21775],{"type":32,"value":21769},"This test would pass. It is in fact not visible, because of that ",{"type":27,"tag":653,"props":21771,"children":21773},{"className":21772},[],[21774],{"type":32,"value":21731},{"type":32,"value":21776}," property of our container.",{"type":27,"tag":28,"props":21778,"children":21779},{},[21780],{"type":32,"value":21781},"The whole thing with visibility might be better explained with a simple demonstration. Let’s consider this test:",{"type":27,"tag":793,"props":21783,"children":21786},{"className":21784,"code":21785,"language":3520,"meta":5},[3517],"it('has a board', () => {\n\n  cy\n    .visit('/');\n\n  cy\n    .get('[data-cy=login-menu]')\n    .click();\n\n  cy\n    .get('[data-cy=board-item]')\n    .eq(1)\n    .should('be.visible')\n    .click();\n\n});\n",[21787],{"type":27,"tag":653,"props":21788,"children":21789},{"__ignoreMap":5},[21790],{"type":32,"value":21785},{"type":27,"tag":28,"props":21792,"children":21793},{},[21794,21796,21801,21803,21808],{"type":32,"value":21795},"Our test would not fail on line 13, but on line 14. Our ",{"type":27,"tag":653,"props":21797,"children":21799},{"className":21798},[],[21800],{"type":32,"value":8109},{"type":32,"value":21802}," assertion would be visible, since our element is not hidden by scroll, and it’s possible to see it. But the ",{"type":27,"tag":653,"props":21804,"children":21806},{"className":21805},[],[21807],{"type":32,"value":12806},{"type":32,"value":21809}," action would in fact fail, because our board element is in fact covered by our login module.",{"type":27,"tag":28,"props":21811,"children":21812},{},[21813,21815,21819,21820,21824,21826,21830],{"type":32,"value":21814},"Hope this helps. If you are still struggling with checking visibility, let me know on ",{"type":27,"tag":172,"props":21816,"children":21817},{"href":5770},[21818],{"type":32,"value":1589},{"type":32,"value":1591},{"type":27,"tag":172,"props":21821,"children":21822},{"href":10953},[21823],{"type":32,"value":1598},{"type":32,"value":21825},". And If you want to talk Cypress, I suggest you join the ",{"type":27,"tag":172,"props":21827,"children":21828},{"href":20722},[21829],{"type":32,"value":15123},{"type":32,"value":21831},", where we talk about Cypress, share articles, tips and help each other grow.",{"title":5,"searchDepth":320,"depth":320,"links":21833},[21834,21835,21836],{"id":21592,"depth":320,"text":21595},{"id":21630,"depth":320,"text":21633},{"id":21698,"depth":320,"text":21701},"content:cypress-basics-check-if-element-exists:index.md","cypress-basics-check-if-element-exists/index.md","cypress-basics-check-if-element-exists/index",{"_path":21841,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":21842,"description":21843,"date":21844,"published":10,"slug":21845,"tags":21846,"readingTime":21848,"body":21853,"_type":329,"_id":21979,"_source":331,"_file":21980,"_stem":21981,"_extension":334},"/dealing-with-multiple-redirects-in-cypress","Dealing with multiple redirects in Cypress","There are cases where you might deal app quickly redirecting through multiple pages. Chances are that Cypress will not register the page in the middle of redirect chain. In this post I will show you ways you can deal with this.","2021-01-25","dealing-with-multiple-redirects-in-cypress",[5279,21847],"tricks",{"text":21849,"minutes":21850,"time":21851,"words":21852},"2 min read",1.435,86100,287,{"type":24,"children":21854,"toc":21977},[21855,21883,21891,21896,21905,21915,21936,21945,21972],{"type":27,"tag":28,"props":21856,"children":21857},{},[21858,21860,21867,21869,21875,21877],{"type":32,"value":21859},"Let’s take a look into ",{"type":27,"tag":172,"props":21861,"children":21864},{"href":21862,"rel":21863},"https://github.com/filiphric/multiple-redirects",[696],[21865],{"type":32,"value":21866},"our very simple app",{"type":32,"value":21868},". Clicking on out \"Let’s go!\" button will redirect us to ",{"type":27,"tag":653,"props":21870,"children":21872},{"className":21871},[],[21873],{"type":32,"value":21874},"page2.html",{"type":32,"value":21876},". This second page has an immediate redirect to ",{"type":27,"tag":653,"props":21878,"children":21880},{"className":21879},[],[21881],{"type":32,"value":21882},"page3.html",{"type":27,"tag":28,"props":21884,"children":21885},{},[21886],{"type":27,"tag":959,"props":21887,"children":21890},{"alt":21888,"src":21889},"Redirects on our app","redirects.mp4",[],{"type":27,"tag":28,"props":21892,"children":21893},{},[21894],{"type":32,"value":21895},"The whole thing happens very fast, but when you look closely you can see the redirect happening for a brief second in the address bar. Let’s now write a test for all of our redirects, so that we know that the middle one actually happens. Our test will look like this:",{"type":27,"tag":793,"props":21897,"children":21900},{"className":21898,"code":21899,"language":1513,"meta":5},[1510],"it('redirects to page2 and page3', () => {\n\n  cy\n    .visit('/')\n\n  cy\n    .get('a')\n    .click()\n\n  // test first redirect\n  cy\n    .location('href')\n    .should('eq', 'page2')\n\n  // test second redirect\n  cy\n    .location('href')\n    .should('eq', 'page3')\n\n});\n",[21901],{"type":27,"tag":653,"props":21902,"children":21903},{"__ignoreMap":5},[21904],{"type":32,"value":21899},{"type":27,"tag":28,"props":21906,"children":21907},{},[21908,21910],{"type":32,"value":21909},"When we run our test though, we can see that the test fails. Our redirect happens just too fast. Since Cypress waits for page to fully load, our assertion comes in too late and our test fails.\n",{"type":27,"tag":959,"props":21911,"children":21914},{"alt":21912,"src":21913},"Redirect not registered by Cypress","failed-test.mp4",[],{"type":27,"tag":28,"props":21916,"children":21917},{},[21918,21920,21926,21928,21934],{"type":32,"value":21919},"We see Cypress registering this redirect event, so it seems like it is something we should be able to test. And it’s true. This is one of the events we can look into in our test. Whenever a url is changed, this event is registered. Using ",{"type":27,"tag":653,"props":21921,"children":21923},{"className":21922},[],[21924],{"type":32,"value":21925},"cy.on",{"type":32,"value":21927}," command we can catch the event called ",{"type":27,"tag":653,"props":21929,"children":21931},{"className":21930},[],[21932],{"type":32,"value":21933},"url:changed",{"type":32,"value":21935},". This event returns the url which we are being redirected to, so we can feed this into an array of all our redirects and test our array instead, like this:",{"type":27,"tag":793,"props":21937,"children":21940},{"className":21938,"code":21939,"language":1513,"meta":5},[1510],"it('passing test', () => {\n\n  const urlRedirects = [];\n\n  cy\n    .visit('/')\n\n  cy\n    .on('url:changed', (url) => {\n      urlRedirects.push(url);\n    });\n\n\n  cy\n    .get('a')\n    .click()\n\n  cy\n    .then(() => {\n\n      expect(urlRedirects).to.have.length(3);\n      expect(urlRedirects[1]).to.eq(`${Cypress.config('baseUrl')}/page2`);\n      expect(urlRedirects[2]).to.eq(`${Cypress.config('baseUrl')}/page3`);\n\n    });\n\n});\n",[21941],{"type":27,"tag":653,"props":21942,"children":21943},{"__ignoreMap":5},[21944],{"type":32,"value":21939},{"type":27,"tag":28,"props":21946,"children":21947},{},[21948,21949,21955,21957,21963,21965,21970],{"type":32,"value":9042},{"type":27,"tag":653,"props":21950,"children":21952},{"className":21951},[],[21953],{"type":32,"value":21954},".location()",{"type":32,"value":21956}," command, we are now just testing our ",{"type":27,"tag":653,"props":21958,"children":21960},{"className":21959},[],[21961],{"type":32,"value":21962},"urlRedirects",{"type":32,"value":21964}," array. We need to use ",{"type":27,"tag":653,"props":21966,"children":21968},{"className":21967},[],[21969],{"type":32,"value":13338},{"type":32,"value":21971}," command, otherwise our assertion would be ran before our array is filled with values.",{"type":27,"tag":28,"props":21973,"children":21974},{},[21975],{"type":32,"value":21976},"Hope this helps!",{"title":5,"searchDepth":320,"depth":320,"links":21978},[],"content:dealing-with-multiple-redirects-in-cypress:index.md","dealing-with-multiple-redirects-in-cypress/index.md","dealing-with-multiple-redirects-in-cypress/index",{"_path":3932,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":21983,"description":21984,"slug":21985,"date":21986,"published":10,"tags":21987,"readingTime":21989,"body":21993,"_type":329,"_id":22759,"_source":331,"_file":22760,"_stem":22761,"_extension":334},"Understanding code coverage in Cypress","It’s pretty awesome that you can use e2e tests to generate your coverage data. In this blog I’m describing how the whole process works.","understanding-code-coverage","2021-01-18",[13,21988,3457],"advanced",{"text":19,"minutes":21990,"time":21991,"words":21992},8.785,527100,1757,{"type":24,"children":21994,"toc":22751},[21995,22000,22023,22029,22043,22048,22057,22084,22093,22104,22113,22127,22137,22156,22166,22172,22205,22226,22235,22270,22280,22286,22327,22339,22351,22357,22388,22393,22402,22415,22426,22431,22436,22456,22468,22477,22482,22512,22537,22555,22568,22577,22589,22595,22636,22650,22667,22692,22699,22704,22710,22715,22724,22729,22734,22739],{"type":27,"tag":28,"props":21996,"children":21997},{},[21998],{"type":32,"value":21999},"Code coverage is one of those few things that doesn’t come right out of the box with Cypress. To set up code coverage, there’s some web development knowledge that one must understand first. I struggled with this initially, but last couple of weeks I was finally able to wrap my head around it.",{"type":27,"tag":28,"props":22001,"children":22002},{},[22003,22005,22012,22014,22021],{"type":32,"value":22004},"I wrote this blog for those that are beginning to learn about how web apps are built, so if you don’t need a demonstration on what Browserify, Babel or Webpack do, you can scroll over to Part 4. If you are looking for ways how to set up code coverage with Cypress, I recommend you instead visit this ",{"type":27,"tag":172,"props":22006,"children":22009},{"href":22007,"rel":22008},"https://docs.cypress.io/guides/tooling/code-coverage.html#Introduction",[696],[22010],{"type":32,"value":22011},"great piece of documentation",{"type":32,"value":22013},", or look into at the ",{"type":27,"tag":172,"props":22015,"children":22018},{"href":22016,"rel":22017},"https://www.youtube.com/watch?v=C8g5X4vCZJA",[696],[22019],{"type":32,"value":22020},"webinar made by Cypress team",{"type":32,"value":22022}," on this topic. If you are confused by these, feel free to come back, see if it helps.",{"type":27,"tag":45,"props":22024,"children":22026},{"id":22025},"part-1-what-are-web-apps-made-of",[22027],{"type":32,"value":22028},"Part 1 - what are web apps made of?",{"type":27,"tag":28,"props":22030,"children":22031},{},[22032,22034,22041],{"type":32,"value":22033},"As it’s almost usual on this blog, ",{"type":27,"tag":172,"props":22035,"children":22038},{"href":22036,"rel":22037},"https://github.com/filiphric/understanding-coverage",[696],[22039],{"type":32,"value":22040},"there’s a repo you can clone",{"type":32,"value":22042}," and follow along this blog. Our app is super simple, it’s just a site with a button that generates a random and totally useless fact. Knowing these provides absolutely no value to your life. But pulling these facts in a conversation will certainly make you more annoying. So there’s that.",{"type":27,"tag":28,"props":22044,"children":22045},{},[22046],{"type":32,"value":22047},"This app, contains three files, as is usual for web apps:",{"type":27,"tag":793,"props":22049,"children":22052},{"className":22050,"code":22051,"language":2250,"meta":5},[2248],"index.html\napp.js\nstyle.css\n",[22053],{"type":27,"tag":653,"props":22054,"children":22055},{"__ignoreMap":5},[22056],{"type":32,"value":22051},{"type":27,"tag":28,"props":22058,"children":22059},{},[22060,22062,22068,22069,22074,22076,22082],{"type":32,"value":22061},"Each plays different part in our app. Our ",{"type":27,"tag":653,"props":22063,"children":22065},{"className":22064},[],[22066],{"type":32,"value":22067},"css",{"type":32,"value":4164},{"type":27,"tag":653,"props":22070,"children":22072},{"className":22071},[],[22073],{"type":32,"value":1513},{"type":32,"value":22075}," files are linked together in our html file, inside the ",{"type":27,"tag":653,"props":22077,"children":22079},{"className":22078},[],[22080],{"type":32,"value":22081},"\u003Chead>",{"type":32,"value":22083}," tag:",{"type":27,"tag":793,"props":22085,"children":22088},{"className":22086,"code":22087,"filename":6152,"language":7826,"meta":5},[7824],"\u003Chead>\n  \u003Clink rel=\"stylesheet\" href=\"style.css\">\n  \u003Cscript src=\"app.js\" defer>\u003C/script>\n\u003C/head>\n",[22089],{"type":27,"tag":653,"props":22090,"children":22091},{"__ignoreMap":5},[22092],{"type":32,"value":22087},{"type":27,"tag":28,"props":22094,"children":22095},{},[22096,22097,22102],{"type":32,"value":11386},{"type":27,"tag":653,"props":22098,"children":22100},{"className":22099},[],[22101],{"type":32,"value":7849},{"type":32,"value":22103}," is responsible for the \"fun\" part on our site. Clicking on the button will reveal our random fact. The file looks like this:",{"type":27,"tag":793,"props":22105,"children":22108},{"className":22106,"code":22107,"filename":7849,"language":1513,"meta":5},[1510],"const $ = document.querySelector.bind(document)\n\n$('button').addEventListener('click', () => {\n  $('p').textContent = 'A rainbow can be seen only in the morning or late afternoon. It can occur only when the sun is 40 degrees or less above the horizon.'\n})\n",[22109],{"type":27,"tag":653,"props":22110,"children":22111},{"__ignoreMap":5},[22112],{"type":32,"value":22107},{"type":27,"tag":28,"props":22114,"children":22115},{},[22116,22118,22125],{"type":32,"value":22117},"Our page contains an empty paragraph. Once we click on our button, it reveals a fact about rainbows. A single fact is not that random and makes our site boring. Or at least even more boring than it currently is. Luckily for us, there’s an ",{"type":27,"tag":172,"props":22119,"children":22122},{"href":22120,"rel":22121},"https://uselessfacts.jsph.pl/",[696],[22123],{"type":32,"value":22124},"API for random useless facts",{"type":32,"value":22126},". You read that right, I’m not kidding. So let’s use this API. To do that, I’m going to create another file, that is going to fetch our API for a random useless fact.",{"type":27,"tag":793,"props":22128,"children":22132},{"className":22129,"code":22130,"filename":22131,"language":1513,"meta":5},[1510],"const axios = require('axios')\n\nmodule.exports.randomFact = async () => {\n  const res = await axios.get('https://uselessfacts.jsph.pl/random.json?language=en')\n    .then(function (response) {\n      return response.data.text\n  })\n\n  return res;\n}\n","randomFact.js",[22133],{"type":27,"tag":653,"props":22134,"children":22135},{"__ignoreMap":5},[22136],{"type":32,"value":22130},{"type":27,"tag":28,"props":22138,"children":22139},{},[22140,22142,22147,22149,22154],{"type":32,"value":22141},"We are going to require our ",{"type":27,"tag":653,"props":22143,"children":22145},{"className":22144},[],[22146],{"type":32,"value":22131},{"type":32,"value":22148}," module inside our our ",{"type":27,"tag":653,"props":22150,"children":22152},{"className":22151},[],[22153],{"type":32,"value":7849},{"type":32,"value":22155}," and use the function to fetch a random fact from our API:",{"type":27,"tag":793,"props":22157,"children":22161},{"className":22158,"code":22159,"filename":7849,"highlights":22160,"language":1513,"meta":5},[1510],"const $ = document.querySelector.bind(document)\nconst { randomFact } = require(\"./randomFact\");\n\n$('button').addEventListener('click', () => {\n  randomFact().then( (text) => {\n      $('p').textContent = text\n  });\n})\n",[320],[22162],{"type":27,"tag":653,"props":22163,"children":22164},{"__ignoreMap":5},[22165],{"type":32,"value":22159},{"type":27,"tag":45,"props":22167,"children":22169},{"id":22168},"part-2-bundling-javascript-files",[22170],{"type":32,"value":22171},"Part 2 - bundling JavaScript files",{"type":27,"tag":28,"props":22173,"children":22174},{},[22175,22177,22183,22185,22190,22192,22197,22199,22204],{"type":32,"value":22176},"When we do this though, and open our app in browser, an error appears: ",{"type":27,"tag":653,"props":22178,"children":22180},{"className":22179},[],[22181],{"type":32,"value":22182},"require is not defined",{"type":32,"value":22184},". This is because require does not exist in browser context. This feels like we cannot include multiple files in our app, but that’s not actually the case. Since we want to use our ",{"type":27,"tag":653,"props":22186,"children":22188},{"className":22187},[],[22189],{"type":32,"value":22131},{"type":32,"value":22191},", the common solution is to bundle up our ",{"type":27,"tag":653,"props":22193,"children":22195},{"className":22194},[],[22196],{"type":32,"value":7849},{"type":32,"value":22198}," file and everything it references. We will create a single file that we are going to reference in hour html ",{"type":27,"tag":653,"props":22200,"children":22202},{"className":22201},[],[22203],{"type":32,"value":22081},{"type":32,"value":7418},{"type":27,"tag":28,"props":22206,"children":22207},{},[22208,22210,22217,22219,22224],{"type":32,"value":22209},"For doing this process, we are going to use a tool called ",{"type":27,"tag":172,"props":22211,"children":22214},{"href":22212,"rel":22213},"http://browserify.org/",[696],[22215],{"type":32,"value":22216},"Browserify",{"type":32,"value":22218},". It will convert our ",{"type":27,"tag":653,"props":22220,"children":22222},{"className":22221},[],[22223],{"type":32,"value":7849},{"type":32,"value":22225}," file to a bundle that will link everything into a single file in a browser friendly way. We’ll do that using following command:",{"type":27,"tag":793,"props":22227,"children":22230},{"className":22228,"code":22229,"language":1084,"meta":5},[1082],"npx browserify app/app.js -o app/bundle.js\n",[22231],{"type":27,"tag":653,"props":22232,"children":22233},{"__ignoreMap":5},[22234],{"type":32,"value":22229},{"type":27,"tag":28,"props":22236,"children":22237},{},[22238,22240,22246,22248,22253,22255,22261,22263,22268],{"type":32,"value":22239},"The output (",{"type":27,"tag":653,"props":22241,"children":22243},{"className":22242},[],[22244],{"type":32,"value":22245},"-o",{"type":32,"value":22247},") of our ",{"type":27,"tag":653,"props":22249,"children":22251},{"className":22250},[],[22252],{"type":32,"value":7849},{"type":32,"value":22254}," file will be a new file called ",{"type":27,"tag":653,"props":22256,"children":22258},{"className":22257},[],[22259],{"type":32,"value":22260},"bundle.js",{"type":32,"value":22262},". From now on, we will use our ",{"type":27,"tag":653,"props":22264,"children":22266},{"className":22265},[],[22267],{"type":32,"value":22260},{"type":32,"value":22269}," in our html:",{"type":27,"tag":793,"props":22271,"children":22275},{"className":22272,"code":22273,"filename":6152,"highlights":22274,"language":7826,"meta":5},[7824],"\u003Chead>\n  \u003Clink rel=\"stylesheet\" href=\"style.css\">\n  \u003Cscript src=\"bundle.js\" defer>\u003C/script>\n\u003C/head>\n",[1606],[22276],{"type":27,"tag":653,"props":22277,"children":22278},{"__ignoreMap":5},[22279],{"type":32,"value":22273},{"type":27,"tag":45,"props":22281,"children":22283},{"id":22282},"part-3-transforming-javascript-files",[22284],{"type":32,"value":22285},"Part 3 - transforming JavaScript files",{"type":27,"tag":28,"props":22287,"children":22288},{},[22289,22291,22296,22298,22303,22305,22311,22313,22319,22321,22326],{"type":32,"value":22290},"Our app is now working, which is great. If we take a look into the ",{"type":27,"tag":653,"props":22292,"children":22294},{"className":22293},[],[22295],{"type":32,"value":22260},{"type":32,"value":22297}," file, we’ll that it contains our ",{"type":27,"tag":653,"props":22299,"children":22301},{"className":22300},[],[22302],{"type":32,"value":7849},{"type":32,"value":22304}," code, our ",{"type":27,"tag":653,"props":22306,"children":22308},{"className":22307},[],[22309],{"type":32,"value":22310},"randomFact",{"type":32,"value":22312}," code and our ",{"type":27,"tag":653,"props":22314,"children":22316},{"className":22315},[],[22317],{"type":32,"value":22318},"axios",{"type":32,"value":22320}," module code which we use in our ",{"type":27,"tag":653,"props":22322,"children":22324},{"className":22323},[],[22325],{"type":32,"value":22310},{"type":32,"value":6658},{"type":27,"tag":28,"props":22328,"children":22329},{},[22330,22332,22337],{"type":32,"value":22331},"Now that we have bundled our code, we can further modify it. We could e.g. minify our bundle. In other words, we could take out all the white spaces, delete comments and use short variable names. This is especially useful for big projects with hundreds of ",{"type":27,"tag":653,"props":22333,"children":22335},{"className":22334},[],[22336],{"type":32,"value":1513},{"type":32,"value":22338}," files. Another example of such modification may be making our code compatible with older browsers. You can often find different tools accomplishing these tasks. If you ever heard of Babel, Webpack or Browserify - that’s what these tools do.",{"type":27,"tag":28,"props":22340,"children":22341},{},[22342,22344,22349],{"type":32,"value":22343},"This bundling and transforming is often referred to as ",{"type":27,"tag":79,"props":22345,"children":22346},{},[22347],{"type":32,"value":22348},"building",{"type":32,"value":22350}," the app. For code coverage, we will build a transformed version of our application. But instead of making it smaller, we are going to make it bigger.",{"type":27,"tag":45,"props":22352,"children":22354},{"id":22353},"part-4-what-is-instrumentation",[22355],{"type":32,"value":22356},"Part 4 - what is instrumentation?",{"type":27,"tag":28,"props":22358,"children":22359},{},[22360,22362,22369,22371,22377,22379,22386],{"type":32,"value":22361},"I think ",{"type":27,"tag":172,"props":22363,"children":22366},{"href":22364,"rel":22365},"https://twitter.com/amirrustam",[696],[22367],{"type":32,"value":22368},"Amir Rustamzadeh",{"type":32,"value":22370}," explained it pretty well, in the ",{"type":27,"tag":172,"props":22372,"children":22374},{"href":22016,"rel":22373},[696],[22375],{"type":32,"value":22376},"webinar about code coverage with Cypress",{"type":32,"value":22378},". Go watch it. With ",{"type":27,"tag":172,"props":22380,"children":22383},{"href":22381,"rel":22382},"https://twitter.com/bahmutov",[696],[22384],{"type":32,"value":22385},"Gleb Bahmutov",{"type":32,"value":22387},", they did a great job of explaining how the whole thing actually works.",{"type":27,"tag":28,"props":22389,"children":22390},{},[22391],{"type":32,"value":22392},"I’ll give you just a very brief explanation of how code coverage works. Let’s say we have a very simple function that adds a number, like this:",{"type":27,"tag":793,"props":22394,"children":22397},{"className":22395,"code":22396,"language":1513,"meta":5},[1510],"const addition = (a, b) => {\n  return a + b\n}\n",[22398],{"type":27,"tag":653,"props":22399,"children":22400},{"__ignoreMap":5},[22401],{"type":32,"value":22396},{"type":27,"tag":28,"props":22403,"children":22404},{},[22405,22407,22413],{"type":32,"value":22406},"With code coverage, we want to collect the data on whether this function was actually called. The most simple way to find out would be to put a counter inside our function. Whenever we now call our function, our ",{"type":27,"tag":653,"props":22408,"children":22410},{"className":22409},[],[22411],{"type":32,"value":22412},"i",{"type":32,"value":22414}," variable will increment.",{"type":27,"tag":793,"props":22416,"children":22421},{"className":22417,"code":22418,"highlights":22419,"language":1513,"meta":5},[1510],"let i = 0;\n\nconst addition = (a, b) => {\n  i++\n  return a + b\n}\n\naddition(1,2)\nconsole.log(i) // 1\n\naddition(3,5)\nconsole.log(i) // 2\n\naddition(14,8)\nconsole.log(i) // 3\n",[22420,3877],1,[22422],{"type":27,"tag":653,"props":22423,"children":22424},{"__ignoreMap":5},[22425],{"type":32,"value":22418},{"type":27,"tag":28,"props":22427,"children":22428},{},[22429],{"type":32,"value":22430},"This is the same principle on how code coverage works! It’s really that simple. It counts which functions were called and which were omitted.",{"type":27,"tag":28,"props":22432,"children":22433},{},[22434],{"type":32,"value":22435},"Adding these counters manually is of course not the way to go. That’s where the process of building our app comes in. We can actually transform our app to include these counters inside every function and condition in our app. Browserify is actually not built for this kind of task, but we can use Babelify, which is a Babel plugin for Browserify. Babelify will transform our code using Babel.",{"type":27,"tag":28,"props":22437,"children":22438},{},[22439,22441,22447,22449,22455],{"type":32,"value":22440},"Babel is actually a really powerful tool that can transform code in many ways. The most basic way of using Babel is to transform your code to be compatible with older browsers like Internet Explorer 11. This transformation is needed when you use ES6 syntax (e.g. arrow functions or ",{"type":27,"tag":653,"props":22442,"children":22444},{"className":22443},[],[22445],{"type":32,"value":22446},"async",{"type":32,"value":22448}," functions). Babel is highly configurable and supports various plugins which can help you transform code in many different ways. One plugin that will be using with babel is called ",{"type":27,"tag":653,"props":22450,"children":22452},{"className":22451},[],[22453],{"type":32,"value":22454},"babel-plugin-istanbul",{"type":32,"value":256},{"type":27,"tag":28,"props":22457,"children":22458},{},[22459,22461,22467],{"type":32,"value":22460},"To configure our Babel plugin, we’ll add following configuration to a ",{"type":27,"tag":653,"props":22462,"children":22464},{"className":22463},[],[22465],{"type":32,"value":22466},"babel.config.js",{"type":32,"value":7538},{"type":27,"tag":793,"props":22469,"children":22472},{"className":22470,"code":22471,"filename":22466,"language":1513,"meta":5},[1510],"module.exports = {\n  'presets': [\n    [\n      '@babel/preset-env'\n    ]\n  ],\n  plugins: [\n    ['babel-plugin-istanbul', {\n      extension: ['.js']\n    }]\n  ]\n};\n",[22473],{"type":27,"tag":653,"props":22474,"children":22475},{"__ignoreMap":5},[22476],{"type":32,"value":22471},{"type":27,"tag":28,"props":22478,"children":22479},{},[22480],{"type":32,"value":22481},"If your head is spinning right now, don’t worry. Imagine your code going to a couple of filters. Kind of like an item in a factory starting on one end and finishing on the other.",{"type":27,"tag":28,"props":22483,"children":22484},{},[22485,22486,22491,22493,22497,22499,22504,22506,22511],{"type":32,"value":713},{"type":27,"tag":653,"props":22487,"children":22489},{"className":22488},[],[22490],{"type":32,"value":7849},{"type":32,"value":22492}," will enter >>> ",{"type":27,"tag":79,"props":22494,"children":22495},{},[22496],{"type":32,"value":22216},{"type":32,"value":22498}," where all of the ",{"type":27,"tag":653,"props":22500,"children":22502},{"className":22501},[],[22503],{"type":32,"value":1513},{"type":32,"value":22505}," files are bundled into ",{"type":27,"tag":653,"props":22507,"children":22509},{"className":22508},[],[22510],{"type":32,"value":22260},{"type":32,"value":256},{"type":27,"tag":28,"props":22513,"children":22514},{},[22515,22517,22522,22524,22529,22531,22536],{"type":32,"value":22516},"After this is done, our ",{"type":27,"tag":653,"props":22518,"children":22520},{"className":22519},[],[22521],{"type":32,"value":22260},{"type":32,"value":22523}," file is processed by >>> ",{"type":27,"tag":79,"props":22525,"children":22526},{},[22527],{"type":32,"value":22528},"Babel",{"type":32,"value":22530},", which will transform our code using >>> ",{"type":27,"tag":653,"props":22532,"children":22534},{"className":22533},[],[22535],{"type":32,"value":22454},{"type":32,"value":256},{"type":27,"tag":28,"props":22538,"children":22539},{},[22540,22542,22547,22549,22554],{"type":32,"value":22541},"This last plugin will take our ",{"type":27,"tag":653,"props":22543,"children":22545},{"className":22544},[],[22546],{"type":32,"value":22260},{"type":32,"value":22548}," file and instruments our code (adds function call counters) into the final ",{"type":27,"tag":653,"props":22550,"children":22552},{"className":22551},[],[22553],{"type":32,"value":22260},{"type":32,"value":786},{"type":27,"tag":28,"props":22556,"children":22557},{},[22558,22560,22566],{"type":32,"value":22559},"We can actually see it quite clearly when we call our Browserify command with a transform (",{"type":27,"tag":653,"props":22561,"children":22563},{"className":22562},[],[22564],{"type":32,"value":22565},"-t",{"type":32,"value":22567},") option.",{"type":27,"tag":793,"props":22569,"children":22572},{"className":22570,"code":22571,"language":1084,"meta":5},[1082],"npx browserify app/app.js -t babelify -o app/bundle.js\n",[22573],{"type":27,"tag":653,"props":22574,"children":22575},{"__ignoreMap":5},[22576],{"type":32,"value":22571},{"type":27,"tag":28,"props":22578,"children":22579},{},[22580,22582,22587],{"type":32,"value":22581},"Take a look into ",{"type":27,"tag":653,"props":22583,"children":22585},{"className":22584},[],[22586],{"type":32,"value":22260},{"type":32,"value":22588}," file and see how different it has become since we last bundled it with Browserify.",{"type":27,"tag":45,"props":22590,"children":22592},{"id":22591},"part-5-setting-up-cypress",[22593],{"type":32,"value":22594},"Part 5 - setting up Cypress",{"type":27,"tag":28,"props":22596,"children":22597},{},[22598,22600,22605,22607,22613,22615,22620,22622,22627,22629,22634],{"type":32,"value":22599},"Believe it or not, our app is now instrumented. The data about which function has been called is now being collected. If you open the ",{"type":27,"tag":653,"props":22601,"children":22603},{"className":22602},[],[22604],{"type":32,"value":6152},{"type":32,"value":22606}," file and enter ",{"type":27,"tag":653,"props":22608,"children":22610},{"className":22609},[],[22611],{"type":32,"value":22612},"window.__coverage__",{"type":32,"value":22614}," to the browser, you can actually see that we have an object that references our ",{"type":27,"tag":653,"props":22616,"children":22618},{"className":22617},[],[22619],{"type":32,"value":7849},{"type":32,"value":22621}," and our ",{"type":27,"tag":653,"props":22623,"children":22625},{"className":22624},[],[22626],{"type":32,"value":22131},{"type":32,"value":22628}," file. This is where our coverage data is stored. In fact, when you click on our \"Give me\" button in our app and call ",{"type":27,"tag":653,"props":22630,"children":22632},{"className":22631},[],[22633],{"type":32,"value":22612},{"type":32,"value":22635}," again, you can see that data inside it changed. Not very readable, but it’s there.",{"type":27,"tag":28,"props":22637,"children":22638},{},[22639,22641,22648],{"type":32,"value":22640},"We can now tell Cypress to collect this data and generate report for us. To do that, you need to install ",{"type":27,"tag":172,"props":22642,"children":22645},{"href":22643,"rel":22644},"https://github.com/cypress-io/code-coverage",[696],[22646],{"type":32,"value":22647},"Cypress code coverage plugin",{"type":32,"value":22649},". Installation is pretty standard and readme page should be sufficient enough for explanation. As mentioned, this plugin does not instrument our code, but fortunately, we have already done that in Part 4.",{"type":27,"tag":28,"props":22651,"children":22652},{},[22653,22655,22660,22662],{"type":32,"value":22654},"When we now open Cypress and run a simple test with ",{"type":27,"tag":653,"props":22656,"children":22658},{"className":22657},[],[22659],{"type":32,"value":14057},{"type":32,"value":22661}," command inside, you can see that there are multiple new commands present in our test.\n",{"type":27,"tag":959,"props":22663,"children":22666},{"alt":22664,"src":22665},"Coverage commands in Cypress runner\"","coverage-in-cypress.png",[],{"type":27,"tag":28,"props":22668,"children":22669},{},[22670,22672,22677,22679,22684,22685,22690],{"type":32,"value":22671},"Not only that. After we ran our test, there’s a new folder called ",{"type":27,"tag":653,"props":22673,"children":22675},{"className":22674},[],[22676],{"type":32,"value":3983},{"type":32,"value":22678}," in the root of our project. This contains a HTML report of our code coverage. Just by opening our app we were able to get to 50% coverage of our app. Notice that our coverage report also shows the original names of our files, ",{"type":27,"tag":653,"props":22680,"children":22682},{"className":22681},[],[22683],{"type":32,"value":7849},{"type":32,"value":4164},{"type":27,"tag":653,"props":22686,"children":22688},{"className":22687},[],[22689],{"type":32,"value":22131},{"type":32,"value":22691},", which makes our report very good to navigate through.",{"type":27,"tag":28,"props":22693,"children":22694},{},[22695],{"type":27,"tag":959,"props":22696,"children":22698},{"alt":22664,"src":22697},"coverage-report.png",[],{"type":27,"tag":28,"props":22700,"children":22701},{},[22702],{"type":32,"value":22703},"Inside this report we can further look into each of our files and each of our lines. We can see if these lines of code were actually called by our e2e test. Since our test only opened our app, we need to add a click on our button too to cover the whole story. Adding this will make our app 100% covered, since there is not much else happening in our app.",{"type":27,"tag":45,"props":22705,"children":22707},{"id":22706},"part-6-what-does-code-coverage-tell-us",[22708],{"type":32,"value":22709},"Part 6 - what does code coverage tell us?",{"type":27,"tag":28,"props":22711,"children":22712},{},[22713],{"type":32,"value":22714},"Now that we are all set up, let’s get a little philosophical for a moment. Should we strive for 100% coverage? It sure sounds nice, but getting 100% code coverage does not mean we are bug free. Notice that there is not a single assertion in this test:",{"type":27,"tag":793,"props":22716,"children":22719},{"className":22717,"code":22718,"language":3520,"meta":5},[3517],"it('generates a random fact', () => {\n\n  cy\n    .visit('/')\n\n  cy\n    .get('button')\n    .click()\n\n});\n",[22720],{"type":27,"tag":653,"props":22721,"children":22722},{"__ignoreMap":5},[22723],{"type":32,"value":22718},{"type":27,"tag":28,"props":22725,"children":22726},{},[22727],{"type":32,"value":22728},"But there’s a ton that can happen in our app that might result in a bad user experience. Our layout might be broken, our characters would not be rendered properly our API might not work, you name it. And yet, we have achieved 100% coverage with our test.",{"type":27,"tag":28,"props":22730,"children":22731},{},[22732],{"type":32,"value":22733},"We are „taking a walk“ through our app, but not really making assertions about values it gives us back. This is a good thing to have in mind when writing tests with test coverage.",{"type":27,"tag":28,"props":22735,"children":22736},{},[22737],{"type":32,"value":22738},"Code coverage can help us navigate through project and identify places that are not covered by tests. It is a great guiding tool which does not take too long to set up and provides a tremendous value.",{"type":27,"tag":28,"props":22740,"children":22741},{},[22742,22744,22749],{"type":32,"value":22743},"Hope you liked this post, consider sharing it with your friends. If you have questions, make sure you join my ",{"type":27,"tag":172,"props":22745,"children":22747},{"href":8322,"rel":22746},[696],[22748],{"type":32,"value":15123},{"type":32,"value":22750}," where we are gathering a community of Cypress learners.",{"title":5,"searchDepth":320,"depth":320,"links":22752},[22753,22754,22755,22756,22757,22758],{"id":22025,"depth":320,"text":22028},{"id":22168,"depth":320,"text":22171},{"id":22282,"depth":320,"text":22285},{"id":22353,"depth":320,"text":22356},{"id":22591,"depth":320,"text":22594},{"id":22706,"depth":320,"text":22709},"content:understanding-code-coverage:index.md","understanding-code-coverage/index.md","understanding-code-coverage/index",{"_path":22763,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":22764,"description":22765,"date":22766,"published":10,"slug":22767,"tags":22768,"readingTime":22769,"body":22773,"_type":329,"_id":23074,"_source":331,"_file":23075,"_stem":23076,"_extension":334},"/hover-in-cypress","Hover in Cypress","Short guide on how to hover over elements in Cypress. As there are multiple ways hover actions work, there are multiple ways of achieving hover.","2021-01-11","hover-in-cypress",[5279,13407,21847],{"text":585,"minutes":22770,"time":22771,"words":22772},3.315,198900,663,{"type":24,"children":22774,"toc":23072},[22775,22798,22811,22819,22824,22832,22837,22845,22850,22859,22864,22872,22894,22921,22930,22935,22955,22968,22977,22997,23020,23029,23041,23050,23055],{"type":27,"tag":28,"props":22776,"children":22777},{},[22778,22780,22787,22789,22796],{"type":32,"value":22779},"If you looked at ",{"type":27,"tag":172,"props":22781,"children":22784},{"href":22782,"rel":22783},"https://on.cypress.io/hover",[696],[22785],{"type":32,"value":22786},"Cypress documentation",{"type":32,"value":22788}," and looked for .hover() command, you might get a little disappointed. There is no such command. Even worse, .hover() is the ",{"type":27,"tag":172,"props":22790,"children":22793},{"href":22791,"rel":22792},"https://github.com/cypress-io/cypress/issues?q=is%3Aissue+is%3Aopen+sort%3Acreated-asc",[696],[22794],{"type":32,"value":22795},"oldest issue on Cypress’ GitHub page",{"type":32,"value":22797}," that is still open. So no hover in Cypress? Well, this would be a short post if there wasn’t a solution 😃.",{"type":27,"tag":28,"props":22799,"children":22800},{},[22801,22803,22809],{"type":32,"value":22802},"I will be using my Trello clone app, so make sure you ",{"type":27,"tag":172,"props":22804,"children":22806},{"href":19041,"rel":22805},[696],[22807],{"type":32,"value":22808},"clone it on GitHub",{"type":32,"value":22810}," if you want to follow along. Let’s take a look onto our board list and see what happens when we hover over a board.",{"type":27,"tag":28,"props":22812,"children":22813},{},[22814],{"type":27,"tag":959,"props":22815,"children":22818},{"alt":22816,"src":22817},"Hovering over element","hovering-over-element.mp4",[],{"type":27,"tag":28,"props":22820,"children":22821},{},[22822],{"type":32,"value":22823},"Our board card gets darker and a star icon appears on top right corner. Upon further examination, you can see that these two changes are triggered differently. Looking at DevTools, you can see that the color change is handled by CSS, but our icon is not displayed when we force hover state via DevTools:",{"type":27,"tag":28,"props":22825,"children":22826},{},[22827],{"type":27,"tag":959,"props":22828,"children":22831},{"alt":22829,"src":22830},"trigger hover state via devtools","hover-via-devtools.png",[],{"type":27,"tag":28,"props":22833,"children":22834},{},[22835],{"type":32,"value":22836},"Instead, there is an event listener that will change the visibility of our star icon.",{"type":27,"tag":28,"props":22838,"children":22839},{},[22840],{"type":27,"tag":959,"props":22841,"children":22844},{"alt":22842,"src":22843},"Element hovered by JavaScript","hover-javascript.mp4",[],{"type":27,"tag":28,"props":22846,"children":22847},{},[22848],{"type":32,"value":22849},"User of course does not see this. The functionality here enables user to bookmark a board, so that’s what we need to focus on in our test. Let’s now try to write a test, in which we attempt to mimic this user behavior.",{"type":27,"tag":793,"props":22851,"children":22854},{"className":22852,"code":22853,"language":3520,"meta":5},[3517],"cy\n  .visit('/')\n\ncy\n  .get('[data-cy=\"star\"]')\n  .click();\n",[22855],{"type":27,"tag":653,"props":22856,"children":22857},{"__ignoreMap":5},[22858],{"type":32,"value":22853},{"type":27,"tag":28,"props":22860,"children":22861},{},[22862],{"type":32,"value":22863},"This will of course throw an error, because the element we want to click on is not visible. Cypress has some great explanatory error messages, with some recommendations on how to solve the problem.",{"type":27,"tag":28,"props":22865,"children":22866},{},[22867],{"type":27,"tag":959,"props":22868,"children":22871},{"alt":22869,"src":22870},"Cypress error message on invisible element","cypress-error.png",[],{"type":27,"tag":28,"props":22873,"children":22874},{},[22875,22877,22883,22885,22892],{"type":32,"value":22876},"As a quick fix, we can apply ",{"type":27,"tag":653,"props":22878,"children":22880},{"className":22879},[],[22881],{"type":32,"value":22882},".click({force: true})",{"type":32,"value":22884}," to skip checks that Cypress does for us before we click on an element. There’s a really good article on ",{"type":27,"tag":172,"props":22886,"children":22889},{"href":22887,"rel":22888},"https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html#Actionability",[696],[22890],{"type":32,"value":22891},"what this means in Cypress docs",{"type":32,"value":22893},". Of course, there are many reasons you might not want to skip these checks.",{"type":27,"tag":28,"props":22895,"children":22896},{},[22897,22899,22904,22906,22912,22914,22919],{"type":32,"value":22898},"If we want to avoid this, we can use ",{"type":27,"tag":653,"props":22900,"children":22902},{"className":22901},[],[22903],{"type":32,"value":18537},{"type":32,"value":22905}," command. This enables us to display a hidden element, same way you would using jQuery ",{"type":27,"tag":653,"props":22907,"children":22909},{"className":22908},[],[22910],{"type":32,"value":22911},".show()",{"type":32,"value":22913}," function. In fact, ",{"type":27,"tag":653,"props":22915,"children":22917},{"className":22916},[],[22918],{"type":32,"value":18537},{"type":32,"value":22920}," command enables you to use many of jQuery functions. The code will look something like this:",{"type":27,"tag":793,"props":22922,"children":22925},{"className":22923,"code":22924,"language":3520,"meta":5},[3517],"cy\n  .visit('/')\n\ncy\n  .get('[data-cy=\"star\"]')\n  .invoke('show')\n  .click();\n",[22926],{"type":27,"tag":653,"props":22927,"children":22928},{"__ignoreMap":5},[22929],{"type":32,"value":22924},{"type":27,"tag":28,"props":22931,"children":22932},{},[22933],{"type":32,"value":22934},"This way, we don’t need to force our click, because our icon will be visible.",{"type":27,"tag":28,"props":22936,"children":22937},{},[22938,22940,22946,22947,22953],{"type":32,"value":22939},"You may argue, that we are manipulating our application to a state that user does not really get into. And you would be right. Our icon is not displayed by user invoking a function on the icon element, but by hovering over our board item. As described above, there are event listeners bound to our item, called ",{"type":27,"tag":653,"props":22941,"children":22943},{"className":22942},[],[22944],{"type":32,"value":22945},"mouseover",{"type":32,"value":4164},{"type":27,"tag":653,"props":22948,"children":22950},{"className":22949},[],[22951],{"type":32,"value":22952},"mouseout",{"type":32,"value":22954},". Instead of forcing our icon to show, we can trigger these events onto our board item.",{"type":27,"tag":28,"props":22956,"children":22957},{},[22958,22960,22966],{"type":32,"value":22959},"With using a ",{"type":27,"tag":653,"props":22961,"children":22963},{"className":22962},[],[22964],{"type":32,"value":22965},".trigger()",{"type":32,"value":22967}," command, our code will look something like this:",{"type":27,"tag":793,"props":22969,"children":22972},{"className":22970,"code":22971,"language":3520,"meta":5},[3517],"cy\n  .visit('/');\n\ncy\n  .get('[data-cy=\"board-item\"]')\n  .trigger('mouseover')\n\ncy\n  .get('[data-cy=\"star\"]')\n  .click();\n",[22973],{"type":27,"tag":653,"props":22974,"children":22975},{"__ignoreMap":5},[22976],{"type":32,"value":22971},{"type":27,"tag":28,"props":22978,"children":22979},{},[22980,22982,22987,22989,22995],{"type":32,"value":22981},"This comes closer to our real-world scenario, as our icon is shown when a ",{"type":27,"tag":653,"props":22983,"children":22985},{"className":22984},[],[22986],{"type":32,"value":22945},{"type":32,"value":22988}," event is fired. Note that this event is not the same thing as CSS ",{"type":27,"tag":653,"props":22990,"children":22992},{"className":22991},[],[22993],{"type":32,"value":22994},":hover",{"type":32,"value":22996}," state. When you run this test, you can see that the board item does not change color. Since this is change is handled by CSS, there is no real way we can trigger this change using JavaScript.",{"type":27,"tag":28,"props":22998,"children":22999},{},[23000,23002,23009,23011,23018],{"type":32,"value":23001},"Luckily, there is a way to trigger a real hover state by accessing Chrome DevTools protocol. To make things easier, awesome ",{"type":27,"tag":172,"props":23003,"children":23006},{"href":23004,"rel":23005},"https://twitter.com/dmtrKovalenko",[696],[23007],{"type":32,"value":23008},"Dmitriy Kovalenko",{"type":32,"value":23010}," has created a ",{"type":27,"tag":172,"props":23012,"children":23015},{"href":23013,"rel":23014},"https://github.com/dmtrKovalenko/cypress-real-events",[696],[23016],{"type":32,"value":23017},"Cypress plugin",{"type":32,"value":23019}," that handles accessing this protocol. Installation of this plugin is pretty standard and readme file should get you through it. This plugin will add 4 new commands:",{"type":27,"tag":793,"props":23021,"children":23024},{"className":23022,"code":23023,"language":3520,"meta":5},[3517],"cy.realClick()\ncy.realHover()\ncy.realPress()\ncy.realType()\n",[23025],{"type":27,"tag":653,"props":23026,"children":23027},{"__ignoreMap":5},[23028],{"type":32,"value":23023},{"type":27,"tag":28,"props":23030,"children":23031},{},[23032,23033,23039],{"type":32,"value":20043},{"type":27,"tag":653,"props":23034,"children":23036},{"className":23035},[],[23037],{"type":32,"value":23038},".realHover()",{"type":32,"value":23040}," command enables us to properly hover over our board item. Our test will look something like this:",{"type":27,"tag":793,"props":23042,"children":23045},{"className":23043,"code":23044,"language":3520,"meta":5},[3517],"cy\n  .visit('/');\n\ncy\n  .get('[data-cy=\"board-item\"]')\n  .realHover()\n  .should('have.css', 'background-color', 'rgb(5, 90, 140)');\n\ncy\n  .get('[data-cy=\"star\"]')\n  .click();\n",[23046],{"type":27,"tag":653,"props":23047,"children":23048},{"__ignoreMap":5},[23049],{"type":32,"value":23044},{"type":27,"tag":28,"props":23051,"children":23052},{},[23053],{"type":32,"value":23054},"In our test, we can now actually check the changed color of our board item. That’s pretty cool. Note that this plugin only works in Chrome-based browsers, so you may need to skip this test while running in Firefox.",{"type":27,"tag":28,"props":23056,"children":23057},{},[23058,23060,23065,23066,23071],{"type":32,"value":23059},"If you enjoyed this article, consider sharing it with your friends on your favorite social network. More great content is coming, so you may want to subscribe my to newsletter or follow me on ",{"type":27,"tag":172,"props":23061,"children":23063},{"href":12181,"rel":23062},[696],[23064],{"type":32,"value":8214},{"type":32,"value":4164},{"type":27,"tag":172,"props":23067,"children":23069},{"href":5770,"rel":23068},[696],[23070],{"type":32,"value":1589},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":23073},[],"content:hover-in-cypress:index.md","hover-in-cypress/index.md","hover-in-cypress/index",{"_path":20098,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":23078,"description":23079,"date":23080,"published":10,"slug":23081,"tags":23082,"readingTime":23085,"body":23089,"_type":329,"_id":23671,"_source":331,"_file":23672,"_stem":23673,"_extension":334},"Migrating .route() to .intercept() in Cypress","With version 6.0.0 of Cypress.io, the network layer was completely rewritten. This post will guide you through the process of migration to the new experience.","2020-12-07","migrating-route-to-intercept-in-cypress",[23083,23084,13,5279],"network","guides",{"text":585,"minutes":23086,"time":23087,"words":23088},3.81,228600,762,{"type":24,"children":23090,"toc":23663},[23091,23119,23125,23179,23185,23202,23211,23220,23247,23257,23274,23301,23307,23319,23324,23333,23338,23347,23352,23358,23370,23379,23436,23445,23451,23469,23478,23483,23492,23510,23520,23538,23544,23591,23630,23651],{"type":27,"tag":28,"props":23092,"children":23093},{},[23094,23096,23101,23103,23109,23111,23117],{"type":32,"value":23095},"With version 6.0.0 a new command by the name of ",{"type":27,"tag":653,"props":23097,"children":23099},{"className":23098},[],[23100],{"type":32,"value":14731},{"type":32,"value":23102}," has been landed to Cypress.io. ",{"type":27,"tag":172,"props":23104,"children":23106},{"href":23105},"/playing-with-experimental-network-stubbing",[23107],{"type":32,"value":23108},"I have written a blog about this",{"type":32,"value":23110},"(it was available as an experimental feature). You can judge by the name, that there was clearly an intent to substitute the old command with the new one. Once the newest version came out, deprecating of ",{"type":27,"tag":653,"props":23112,"children":23114},{"className":23113},[],[23115],{"type":32,"value":23116},".route()",{"type":32,"value":23118}," command was announced. It will no longer be supported as of version 7.0.0, which means we all have some migrating to do.",{"type":27,"tag":45,"props":23120,"children":23122},{"id":23121},"reasons-to-migrate",[23123],{"type":32,"value":23124},"Reasons to migrate",{"type":27,"tag":28,"props":23126,"children":23127},{},[23128,23130,23135,23137,23142,23143,23148,23150,23157,23158,23163,23164,23169,23171,23177],{"type":32,"value":23129},"First of all, the new ",{"type":27,"tag":653,"props":23131,"children":23133},{"className":23132},[],[23134],{"type":32,"value":14731},{"type":32,"value":23136}," command is awesome. It supports fetch, it can intercept both request and response of your app API calls and so much more. The other reason why you should consider migrating your ",{"type":27,"tag":653,"props":23138,"children":23140},{"className":23139},[],[23141],{"type":32,"value":23116},{"type":32,"value":14944},{"type":27,"tag":653,"props":23144,"children":23146},{"className":23145},[],[23147],{"type":32,"value":14731},{"type":32,"value":23149}," right now becomes apparent when you look into ",{"type":27,"tag":172,"props":23151,"children":23154},{"href":23152,"rel":23153},"https://docs.cypress.io/guides/references/roadmap.html#Upcoming-features",[696],[23155],{"type":32,"value":23156},"Cypress roadmap",{"type":32,"value":12342},{"type":27,"tag":14020,"props":23159,"children":23160},{},[23161],{"type":32,"value":23162},"Cypress is currently working on multi-domain support (OMG!)",{"type":32,"value":14042},{"type":27,"tag":172,"props":23165,"children":23167},{"href":14045,"rel":23166},[696],[23168],{"type":32,"value":14049},{"type":32,"value":23170},") and plans to implement other cool features as ",{"type":27,"tag":172,"props":23172,"children":23174},{"href":23152,"rel":23173},[696],[23175],{"type":32,"value":23176},"Session API",{"type":32,"value":23178},", file download and iframe support. You definitely don’t want to miss these, so keeping up with latest version is a good idea.",{"type":27,"tag":45,"props":23180,"children":23182},{"id":23181},"syntax-differences",[23183],{"type":32,"value":23184},"Syntax differences",{"type":27,"tag":28,"props":23186,"children":23187},{},[23188,23190,23195,23196,23201],{"type":32,"value":23189},"Looking into documentation, you can find some obvious differences between ",{"type":27,"tag":653,"props":23191,"children":23193},{"className":23192},[],[23194],{"type":32,"value":23116},{"type":32,"value":4164},{"type":27,"tag":653,"props":23197,"children":23199},{"className":23198},[],[23200],{"type":32,"value":14731},{"type":32,"value":1474},{"type":27,"tag":793,"props":23203,"children":23206},{"className":23204,"code":23205,"language":3520,"meta":5},[3517],"cy.route(url)\ncy.route(url, response)\ncy.route(method, url)\ncy.route(method, url, response)\ncy.route(callbackFn)\ncy.route(options)\n",[23207],{"type":27,"tag":653,"props":23208,"children":23209},{"__ignoreMap":5},[23210],{"type":32,"value":23205},{"type":27,"tag":793,"props":23212,"children":23215},{"className":23213,"code":23214,"language":3520,"meta":5},[3517],"cy.intercept(url, routeHandler?)\ncy.intercept(method, url, routeHandler?)\ncy.intercept(routeMatcher, routeHandler?)\n",[23216],{"type":27,"tag":653,"props":23217,"children":23218},{"__ignoreMap":5},[23219],{"type":32,"value":23214},{"type":27,"tag":28,"props":23221,"children":23222},{},[23223,23225,23230,23232,23238,23239,23245],{"type":32,"value":23224},"The coolest additions to ",{"type":27,"tag":653,"props":23226,"children":23228},{"className":23227},[],[23229],{"type":32,"value":14731},{"type":32,"value":23231}," command are ",{"type":27,"tag":653,"props":23233,"children":23235},{"className":23234},[],[23236],{"type":32,"value":23237},"routeMatcher",{"type":32,"value":4164},{"type":27,"tag":653,"props":23240,"children":23242},{"className":23241},[],[23243],{"type":32,"value":23244},"routeHandler",{"type":32,"value":23246}," arguments.",{"type":27,"tag":28,"props":23248,"children":23249},{},[23250,23255],{"type":27,"tag":653,"props":23251,"children":23253},{"className":23252},[],[23254],{"type":32,"value":23237},{"type":32,"value":23256}," unleashes some amazing capabilities of matching your application API calls. Besides minimatch and RegEx, you can now specify your match by query, headers or even port or just path without query parameters. That is some power!",{"type":27,"tag":28,"props":23258,"children":23259},{},[23260,23265,23267,23272],{"type":27,"tag":653,"props":23261,"children":23263},{"className":23262},[],[23264],{"type":32,"value":23244},{"type":32,"value":23266}," gives you some amazing options on intercepting and stubbing API responses. You can use ",{"type":27,"tag":653,"props":23268,"children":23270},{"className":23269},[],[23271],{"type":32,"value":14731},{"type":32,"value":23273}," command to stub your responses, but you can do so much more! You can change headers on your API calls, dynamically change just parts of your response or your request. It really seems like there’s not much you cannot do with your network calls.",{"type":27,"tag":28,"props":23275,"children":23276},{},[23277,23279,23284,23286,23291,23293,23299],{"type":32,"value":23278},"To make these cool new features available to you, you should slowly start to migrate your ",{"type":27,"tag":653,"props":23280,"children":23282},{"className":23281},[],[23283],{"type":32,"value":23116},{"type":32,"value":23285}," commands to ",{"type":27,"tag":653,"props":23287,"children":23289},{"className":23288},[],[23290],{"type":32,"value":14731},{"type":32,"value":23292},". Once you are done, you can completely delete any ",{"type":27,"tag":653,"props":23294,"children":23296},{"className":23295},[],[23297],{"type":32,"value":23298},".server()",{"type":32,"value":23300}," command call as this is deprecated too.",{"type":27,"tag":45,"props":23302,"children":23304},{"id":23303},"simple-use-cases",[23305],{"type":32,"value":23306},"Simple use cases",{"type":27,"tag":28,"props":23308,"children":23309},{},[23310,23312,23317],{"type":32,"value":23311},"If you used ",{"type":27,"tag":653,"props":23313,"children":23315},{"className":23314},[],[23316],{"type":32,"value":23116},{"type":32,"value":23318}," command just for routing and waiting for your API calls, then the migration should be pretty easy for you.",{"type":27,"tag":28,"props":23320,"children":23321},{},[23322],{"type":32,"value":23323},"These commands:",{"type":27,"tag":793,"props":23325,"children":23328},{"className":23326,"code":23327,"language":3520,"meta":5},[3517],"cy.route('/boards').as('getBoards')\ncy.route('POST', '/lists').as('createList')\ncy.route('PATCH', '/tasks/*').as('updateTask')\n",[23329],{"type":27,"tag":653,"props":23330,"children":23331},{"__ignoreMap":5},[23332],{"type":32,"value":23327},{"type":27,"tag":28,"props":23334,"children":23335},{},[23336],{"type":32,"value":23337},"Can easily just become these:",{"type":27,"tag":793,"props":23339,"children":23342},{"className":23340,"code":23341,"language":3520,"meta":5},[3517],"cy.intercept('/boards').as('getBoards')\ncy.intercept('POST', '/lists').as('createList')\ncy.intercept('PATCH', '/tasks/*').as('updateTask')\n",[23343],{"type":27,"tag":653,"props":23344,"children":23345},{"__ignoreMap":5},[23346],{"type":32,"value":23341},{"type":27,"tag":28,"props":23348,"children":23349},{},[23350],{"type":32,"value":23351},"For cases like these, you just need to change the name of your commands. But there are some slight differences when you start testing matched API calls.",{"type":27,"tag":45,"props":23353,"children":23355},{"id":23354},"testing-routed-api-calls",[23356],{"type":32,"value":23357},"Testing routed API calls",{"type":27,"tag":28,"props":23359,"children":23360},{},[23361,23363,23368],{"type":32,"value":23362},"I often used ",{"type":27,"tag":653,"props":23364,"children":23366},{"className":23365},[],[23367],{"type":32,"value":15586},{"type":32,"value":23369}," command to check a status, response and/or request body. My assertions often looked like this:",{"type":27,"tag":793,"props":23371,"children":23374},{"className":23372,"code":23373,"language":3520,"meta":5},[3517],"cy.route('POST', '/boards').as('createBoard')\n\n// ...\n\ncy\n  .wait('@createBoard')\n  .then(({ requestBody, responseBody, status }) => {\n\n    expect(status).to.eq(201);\n    expect(requestBody.name).to.eq('new board');\n    expect(responseBody.created).to.eq(Cypress.moment().format('YYYY-MM-DD'));\n    expect(responseBody.name).to.eq('new board');\n    expect(responseBody.starred).to.be.false;\n\n  });\n",[23375],{"type":27,"tag":653,"props":23376,"children":23377},{"__ignoreMap":5},[23378],{"type":32,"value":23373},{"type":27,"tag":28,"props":23380,"children":23381},{},[23382,23383,23388,23390,23396,23398,23404,23406,23412,23414,23420,23421,23427,23429,23434],{"type":32,"value":7021},{"type":27,"tag":653,"props":23384,"children":23386},{"className":23385},[],[23387],{"type":32,"value":14731},{"type":32,"value":23389}," the yielded API call body is slightly different. The biggest change is that ",{"type":27,"tag":653,"props":23391,"children":23393},{"className":23392},[],[23394],{"type":32,"value":23395},"status",{"type":32,"value":23397}," is now ",{"type":27,"tag":653,"props":23399,"children":23401},{"className":23400},[],[23402],{"type":32,"value":23403},"statusCode",{"type":32,"value":23405}," and is part of ",{"type":27,"tag":653,"props":23407,"children":23409},{"className":23408},[],[23410],{"type":32,"value":23411},"response",{"type":32,"value":23413}," object, and there are no longer ",{"type":27,"tag":653,"props":23415,"children":23417},{"className":23416},[],[23418],{"type":32,"value":23419},"requestBody",{"type":32,"value":3372},{"type":27,"tag":653,"props":23422,"children":23424},{"className":23423},[],[23425],{"type":32,"value":23426},"responseBody",{"type":32,"value":23428}," shorthands. They were probably not widely used, but I’ll miss them. With API call matched by ",{"type":27,"tag":653,"props":23430,"children":23432},{"className":23431},[],[23433],{"type":32,"value":14731},{"type":32,"value":23435}," command, the same assertion would look something like this:",{"type":27,"tag":793,"props":23437,"children":23440},{"className":23438,"code":23439,"language":3520,"meta":5},[3517],"cy.intercept('POST', '/boards').as('createBoard')\n\n// ...\n\ncy\n  .wait('@createBoard')\n  .then(({ request, response }) => {\n\n    expect(response.statusCode).to.eq(201);\n    expect(request.body.name).to.eq('new board');\n    expect(response.body.created).to.eq(Cypress.moment().format('YYYY-MM-DD'));\n    expect(response.body.name).to.eq('new board');\n    expect(response.body.starred).to.be.false;\n\n  });\n",[23441],{"type":27,"tag":653,"props":23442,"children":23443},{"__ignoreMap":5},[23444],{"type":32,"value":23439},{"type":27,"tag":45,"props":23446,"children":23448},{"id":23447},"stubbing-your-responses",[23449],{"type":32,"value":23450},"Stubbing your responses",{"type":27,"tag":28,"props":23452,"children":23453},{},[23454,23455,23460,23462,23467],{"type":32,"value":7021},{"type":27,"tag":653,"props":23456,"children":23458},{"className":23457},[],[23459],{"type":32,"value":23116},{"type":32,"value":23461}," you could pass your JSON file as a third argument. This would effectively use this JSON as a response of routed request. With ",{"type":27,"tag":653,"props":23463,"children":23465},{"className":23464},[],[23466],{"type":32,"value":14731},{"type":32,"value":23468}," you’ll need to rewrite this command. The old route command:",{"type":27,"tag":793,"props":23470,"children":23473},{"className":23471,"code":23472,"language":3520,"meta":5},[3517],"cy.route('GET', '/boards', 'fx:boardList')\n",[23474],{"type":27,"tag":653,"props":23475,"children":23476},{"__ignoreMap":5},[23477],{"type":32,"value":23472},{"type":27,"tag":28,"props":23479,"children":23480},{},[23481],{"type":32,"value":23482},"Needs to become:",{"type":27,"tag":793,"props":23484,"children":23487},{"className":23485,"code":23486,"language":3520,"meta":5},[3517],"cy.intercept('GET', '/boards', {\n  fixture: 'boardList'\n})\n",[23488],{"type":27,"tag":653,"props":23489,"children":23490},{"__ignoreMap":5},[23491],{"type":32,"value":23486},{"type":27,"tag":28,"props":23493,"children":23494},{},[23495,23497,23502,23503,23509],{"type":32,"value":23496},"You can of course still pass an object or array as a response. In that case, you would use ",{"type":27,"tag":653,"props":23498,"children":23500},{"className":23499},[],[23501],{"type":32,"value":12139},{"type":32,"value":7446},{"type":27,"tag":653,"props":23504,"children":23506},{"className":23505},[],[23507],{"type":32,"value":23508},"fixture",{"type":32,"value":1474},{"type":27,"tag":793,"props":23511,"children":23515},{"className":23512,"code":23513,"highlights":23514,"language":3520,"meta":5},[3517],"const customResponse = [\n    {\n      \"name\": \"new board\",\n      \"id\": 2,\n      \"starred\": false,\n      \"created\": \"2020-12-07\"\n    }\n  ]\n\ncy.intercept('GET', '/boards', {\n  body: customResponse,\n  status: 500\n})\n",[3812],[23516],{"type":27,"tag":653,"props":23517,"children":23518},{"__ignoreMap":5},[23519],{"type":32,"value":23513},{"type":27,"tag":28,"props":23521,"children":23522},{},[23523,23525,23530,23532,23537],{"type":32,"value":23524},"As you can see in example, you can still change status code and even headers, just as you could with ",{"type":27,"tag":653,"props":23526,"children":23528},{"className":23527},[],[23529],{"type":32,"value":23116},{"type":32,"value":23531},". Everything you need just needs to be passed to the ",{"type":27,"tag":653,"props":23533,"children":23535},{"className":23534},[],[23536],{"type":32,"value":23244},{"type":32,"value":20614},{"type":27,"tag":45,"props":23539,"children":23541},{"id":23540},"there-is-so-much-more",[23542],{"type":32,"value":23543},"There is so much more",{"type":27,"tag":28,"props":23545,"children":23546},{},[23547,23549,23554,23555,23560,23562,23567,23569,23574,23576,23581,23583,23589],{"type":32,"value":23548},"Migrating your ",{"type":27,"tag":653,"props":23550,"children":23552},{"className":23551},[],[23553],{"type":32,"value":23116},{"type":32,"value":14944},{"type":27,"tag":653,"props":23556,"children":23558},{"className":23557},[],[23559],{"type":32,"value":14731},{"type":32,"value":23561}," is just a first step. Network handling was completely rewritten with v6 and it enables you do much more. I will be exploring the possibilities of this new command on my ",{"type":27,"tag":172,"props":23563,"children":23565},{"href":12181,"rel":23564},[696],[23566],{"type":32,"value":14574},{"type":32,"value":23568},". In the meantime, if you want to learn more, I recommend reading my ",{"type":27,"tag":172,"props":23570,"children":23571},{"href":23105},[23572],{"type":32,"value":23573},"older blog",{"type":32,"value":23575},") on capabilities of ",{"type":27,"tag":653,"props":23577,"children":23579},{"className":23578},[],[23580],{"type":32,"value":14731},{"type":32,"value":23582}," command (which was then called ",{"type":27,"tag":653,"props":23584,"children":23586},{"className":23585},[],[23587],{"type":32,"value":23588},".route2()",{"type":32,"value":23590},").",{"type":27,"tag":28,"props":23592,"children":23593},{},[23594,23596,23601,23603,23610,23612,23619,23621,23628],{"type":32,"value":23595},"I also recommend checking out blogs by ",{"type":27,"tag":172,"props":23597,"children":23599},{"href":22381,"rel":23598},[696],[23600],{"type":32,"value":22385},{"type":32,"value":23602},", who ",{"type":27,"tag":172,"props":23604,"children":23607},{"href":23605,"rel":23606},"https://glebbahmutov.com/blog/cy-route-vs-route2/",[696],[23608],{"type":32,"value":23609},"wrote a summary blog",{"type":32,"value":23611}," on differences between these commands, ",{"type":27,"tag":172,"props":23613,"children":23616},{"href":23614,"rel":23615},"https://www.youtube.com/watch?v=_wfKbYQlP_Y",[696],[23617],{"type":32,"value":23618},"made a video about it",{"type":32,"value":23620}," and again, ",{"type":27,"tag":172,"props":23622,"children":23625},{"href":23623,"rel":23624},"https://glebbahmutov.com/blog/smart-graphql-stubbing/",[696],[23626],{"type":32,"value":23627},"wrote a blog",{"type":32,"value":23629}," on how you can use this new command for stubbing GraphQL. That guy is on fire 🔥",{"type":27,"tag":28,"props":23631,"children":23632},{},[23633,23635,23642,23644,23649],{"type":32,"value":23634},"If you are on a reading spree, I also recommend reading a ",{"type":27,"tag":172,"props":23636,"children":23639},{"href":23637,"rel":23638},"https://www.cypress.io/blog/2020/11/24/introducing-cy-intercept-next-generation-network-stubbing-in-cypress-6-0/",[696],[23640],{"type":32,"value":23641},"great blog",{"type":32,"value":23643}," by ",{"type":27,"tag":172,"props":23645,"children":23647},{"href":22364,"rel":23646},[696],[23648],{"type":32,"value":22368},{"type":32,"value":23650}," that came out with Cypress v6 release.",{"type":27,"tag":28,"props":23652,"children":23653},{},[23654,23656,23662],{"type":32,"value":23655},"And don’t forget the ",{"type":27,"tag":172,"props":23657,"children":23660},{"href":23658,"rel":23659},"https://docs.cypress.io/api/commands/intercept.html#Comparison-to-cy-route",[696],[23661],{"type":32,"value":9712},{"type":32,"value":256},{"title":5,"searchDepth":320,"depth":320,"links":23664},[23665,23666,23667,23668,23669,23670],{"id":23121,"depth":320,"text":23124},{"id":23181,"depth":320,"text":23184},{"id":23303,"depth":320,"text":23306},{"id":23354,"depth":320,"text":23357},{"id":23447,"depth":320,"text":23450},{"id":23540,"depth":320,"text":23543},"content:migrating-route-to-intercept-in-cypress:index.md","migrating-route-to-intercept-in-cypress/index.md","migrating-route-to-intercept-in-cypress/index",{"_path":15399,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":23675,"description":23676,"slug":23677,"date":23678,"published":10,"tags":23679,"readingTime":23680,"body":23684,"_type":329,"_id":24473,"_source":331,"_file":24474,"_stem":24475,"_extension":334},"Working with API response data in Cypress","In this post, I’m showcasing different ways to handle data generated by server and use them in your tests.","working-with-api-response-data-in-cypress","2020-11-30",[23083,5279,11456,13],{"text":19,"minutes":23681,"time":23682,"words":23683},8.905,534300,1781,{"type":24,"children":23685,"toc":24461},[23686,23724,23729,23734,23743,23771,23777,23803,23812,23825,23835,23840,23849,23861,23867,23886,23895,23920,23929,23935,23949,23959,23971,23977,23982,23991,24002,24008,24029,24038,24050,24080,24086,24145,24153,24159,24164,24175,24187,24196,24201,24210,24237,24246,24251,24261,24310,24315,24336,24346,24351,24370,24380,24385,24397,24406,24432],{"type":27,"tag":1029,"props":23687,"children":23688},{},[23689],{"type":27,"tag":28,"props":23690,"children":23691},{},[23692,23694,23699,23701,23707,23709,23715,23717,23723],{"type":32,"value":23693},"TL;DR: Your Cypress code is executed in blocks. To work with data from, you can use ",{"type":27,"tag":653,"props":23695,"children":23697},{"className":23696},[],[23698],{"type":32,"value":13338},{"type":32,"value":23700}," command, mocha aliases, window object or environment variables. I have created a pattern using environment variables, which I’m showing in second part of this blog. My app, as well as this pattern ",{"type":27,"tag":172,"props":23702,"children":23704},{"href":19041,"rel":23703},[696],[23705],{"type":32,"value":23706},"can be found on GitHub",{"type":32,"value":23708},". To discuss, ",{"type":27,"tag":172,"props":23710,"children":23712},{"href":20722,"rel":23711},[696],[23713],{"type":32,"value":23714},"join community Discord server",{"type":32,"value":23716},", or see it in action ",{"type":27,"tag":172,"props":23718,"children":23720},{"href":12181,"rel":23719},[696],[23721],{"type":32,"value":23722},"on my YouTube",{"type":32,"value":256},{"type":27,"tag":28,"props":23725,"children":23726},{},[23727],{"type":32,"value":23728},"Situation goes like this. At the beginning of your test, you call an API endpoint. It will give you a response, which you want to use later in your test. What do you do?",{"type":27,"tag":28,"props":23730,"children":23731},{},[23732],{"type":32,"value":23733},"The obvious temptation is to store your response in a variable, something like this:",{"type":27,"tag":793,"props":23735,"children":23738},{"className":23736,"code":23737,"language":1513,"meta":5},[1510],"beforeEach( () => {\n\n  cy\n    .log('starting test')\n\n})\n\nit('creates a new board', () => {\n\n  let res\n  cy\n    .request('POST', '/api/boards', { name: 'new board' })\n    .then( ({ body }) => {\n      res = body\n    })\n\n  console.log(res)\n\n})\n",[23739],{"type":27,"tag":653,"props":23740,"children":23741},{"__ignoreMap":5},[23742],{"type":32,"value":23737},{"type":27,"tag":28,"props":23744,"children":23745},{},[23746,23748,23754,23755,23760,23762,23769],{"type":32,"value":23747},"This will not work properly though. The ",{"type":27,"tag":653,"props":23749,"children":23751},{"className":23750},[],[23752],{"type":32,"value":23753},"console.log",{"type":32,"value":18169},{"type":27,"tag":653,"props":23756,"children":23758},{"className":23757},[],[23759],{"type":32,"value":19264},{"type":32,"value":23761},". The main reason for this is that ",{"type":27,"tag":172,"props":23763,"children":23766},{"href":23764,"rel":23765},"https://docs.cypress.io/guides/core-concepts/introduction-to-cypress.html#Commands-Are-Asynchronous",[696],[23767],{"type":32,"value":23768},"Cypress commands are asynchronous",{"type":32,"value":23770},". But what does that mean in simple terms?",{"type":27,"tag":45,"props":23772,"children":23774},{"id":23773},"understanding-how-cypress-code-is-run",[23775],{"type":32,"value":23776},"Understanding how Cypress code is run",{"type":27,"tag":28,"props":23778,"children":23779},{},[23780,23782,23787,23789,23794,23796,23801],{"type":32,"value":23781},"The intuition is, that our code reads from top to bottom. This is partially true, but not entirely. It is actually ran in blocks. In our test, there are three separate blocks of code (or functions). Our ",{"type":27,"tag":653,"props":23783,"children":23785},{"className":23784},[],[23786],{"type":32,"value":7243},{"type":32,"value":23788}," block, ",{"type":27,"tag":653,"props":23790,"children":23792},{"className":23791},[],[23793],{"type":32,"value":7187},{"type":32,"value":23795}," block and ",{"type":27,"tag":653,"props":23797,"children":23799},{"className":23798},[],[23800],{"type":32,"value":13338},{"type":32,"value":23802}," block. This means that when our code is running will first run this block:",{"type":27,"tag":793,"props":23804,"children":23807},{"className":23805,"code":23737,"highlights":23806,"language":1513,"meta":5},[1510],[22420,320,1606,3877,3667,3809],[23808],{"type":27,"tag":653,"props":23809,"children":23810},{"__ignoreMap":5},[23811],{"type":32,"value":23737},{"type":27,"tag":28,"props":23813,"children":23814},{},[23815,23817,23823],{"type":32,"value":23816},"Then it will run this part (take a look at what happens with the ",{"type":27,"tag":653,"props":23818,"children":23820},{"className":23819},[],[23821],{"type":32,"value":23822},"res",{"type":32,"value":23824}," variable):",{"type":27,"tag":793,"props":23826,"children":23830},{"className":23827,"code":23828,"highlights":23829,"language":1513,"meta":5},[1510],"beforeEach( () => {\n\n  cy\n    .log('starting test')\n\n})\n\nit('creates a new board', () => {\n\n  let res\n\n  cy\n    .request('POST', '/api/boards', { name: 'new board' })\n    .then( ({ body }) => {\n      res = body\n    })\n\n  console.log(res)\n\n})\n",[3723,3746,3810,3811,3812,3852,10872,10246,13139,13176],[23831],{"type":27,"tag":653,"props":23832,"children":23833},{"__ignoreMap":5},[23834],{"type":32,"value":23828},{"type":27,"tag":28,"props":23836,"children":23837},{},[23838],{"type":32,"value":23839},"And finally this part:",{"type":27,"tag":793,"props":23841,"children":23844},{"className":23842,"code":23737,"highlights":23843,"language":1513,"meta":5},[1510],[3852,3853,3878],[23845],{"type":27,"tag":653,"props":23846,"children":23847},{"__ignoreMap":5},[23848],{"type":32,"value":23737},{"type":27,"tag":28,"props":23850,"children":23851},{},[23852,23854,23859],{"type":32,"value":23853},"This demonstrates why our ",{"type":27,"tag":653,"props":23855,"children":23857},{"className":23856},[],[23858],{"type":32,"value":5487},{"type":32,"value":23860}," is not returning the value that we want.",{"type":27,"tag":45,"props":23862,"children":23864},{"id":23863},"using-then-command",[23865],{"type":32,"value":23866},"Using .then() command",{"type":27,"tag":28,"props":23868,"children":23869},{},[23870,23872,23877,23879,23884],{"type":32,"value":23871},"If we want to work with what our ",{"type":27,"tag":653,"props":23873,"children":23875},{"className":23874},[],[23876],{"type":32,"value":12824},{"type":32,"value":23878}," command returns, then we need to write that code inside ",{"type":27,"tag":653,"props":23880,"children":23882},{"className":23881},[],[23883],{"type":32,"value":13338},{"type":32,"value":23885}," function. So if we want to create a new list inside a board, we need to write a code like this:",{"type":27,"tag":793,"props":23887,"children":23890},{"className":23888,"code":23889,"language":1513,"meta":5},[1510],"it('creates a new list within a board', () => {\n\n  cy\n    .request('POST', '/api/boards', { name: 'new board' })\n    .then((board) => {\n\n      cy\n        .request('POST', '/api/lists', {\n          title: 'new list',\n          boardId: board.body.id\n        })\n\n    })\n\n})\n\n",[23891],{"type":27,"tag":653,"props":23892,"children":23893},{"__ignoreMap":5},[23894],{"type":32,"value":23889},{"type":27,"tag":28,"props":23896,"children":23897},{},[23898,23900,23905,23907,23912,23914,23918],{"type":32,"value":23899},"This can of course lead to what is known as callback hell. Let’s say we want to create ",{"type":27,"tag":79,"props":23901,"children":23902},{},[23903],{"type":32,"value":23904},"task",{"type":32,"value":23906},", that is inside a ",{"type":27,"tag":79,"props":23908,"children":23909},{},[23910],{"type":32,"value":23911},"list",{"type":32,"value":23913},", which is on a ",{"type":27,"tag":79,"props":23915,"children":23916},{},[23917],{"type":32,"value":15361},{"type":32,"value":23919},". The code would look something like this:",{"type":27,"tag":793,"props":23921,"children":23924},{"className":23922,"code":23923,"language":1513,"meta":5},[1510],"it('creates a new task on a list within a board', () => {\n\n  cy\n    .request('POST', '/api/boards', { name: 'new board' })\n    .then((board) => {\n\n      cy\n        .request('POST', '/api/lists', {\n          title: 'new list',\n          boardId: board.body.id\n        })\n        .then((list) => {\n\n          cy\n            .request('POST', '/api/tasks', {\n              title: 'new task',\n              listId: list.body.id,\n              boardId: board.body.id\n            })\n\n        })\n\n    })\n\n})\n",[23925],{"type":27,"tag":653,"props":23926,"children":23927},{"__ignoreMap":5},[23928],{"type":32,"value":23923},{"type":27,"tag":45,"props":23930,"children":23932},{"id":23931},"using-aliases",[23933],{"type":32,"value":23934},"Using aliases",{"type":27,"tag":28,"props":23936,"children":23937},{},[23938,23940,23947],{"type":32,"value":23939},"You can already see how the code above is becoming harder to read. One way we can the avoid callback hell in Cypress is using ",{"type":27,"tag":172,"props":23941,"children":23944},{"href":23942,"rel":23943},"https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Sharing-Context",[696],[23945],{"type":32,"value":23946},"Mocha aliases",{"type":32,"value":23948},". This enables us to store data and access them during our test. This helps us shift everything basically to the same level:",{"type":27,"tag":793,"props":23950,"children":23954},{"className":23951,"code":23952,"highlights":23953,"language":1513,"meta":5},[1510],"it('creates a new task on a list within a board', function() {\n\n  cy\n    .request('POST', '/api/boards', { name: 'new board' })\n    .as('board')\n\n  cy\n    .then(() => {\n\n      cy\n        .request('POST', '/api/lists', {\n          title: 'new list',\n          boardId: this.board.body.id\n        })\n        .as('list')\n\n    })\n\n  cy\n    .then(() => {\n\n      cy\n        .request('POST', '/api/tasks', {\n          title: 'new task',\n          listId: this.list.body.id,\n          boardId: this.board.body.id\n        })\n\n    })\n\n})\n",[22420],[23955],{"type":27,"tag":653,"props":23956,"children":23957},{"__ignoreMap":5},[23958],{"type":32,"value":23952},{"type":27,"tag":28,"props":23960,"children":23961},{},[23962,23964,23969],{"type":32,"value":23963},"However, notice on line 1, that instead of arrow function, we are using regular function syntax. This is because it is not possible to use ",{"type":27,"tag":653,"props":23965,"children":23967},{"className":23966},[],[23968],{"type":32,"value":16452},{"type":32,"value":23970}," keyword with arrow functions.",{"type":27,"tag":45,"props":23972,"children":23974},{"id":23973},"window-object",[23975],{"type":32,"value":23976},"Window object",{"type":27,"tag":28,"props":23978,"children":23979},{},[23980],{"type":32,"value":23981},"Another way how you can pass data is using your browser’s window object. What this enables you to do is to share data between tests:",{"type":27,"tag":793,"props":23983,"children":23986},{"className":23984,"code":23985,"language":1513,"meta":5},[1510],"it('creates a board', () => {\n\n  cy\n    .request('POST', '/api/boards', { name: 'new board' })\n    .then((board) => {\n      window.board = board.body;\n    })\n\n})\n\nit('creates a list', () => {\n\n  cy\n    .request('POST', '/api/lists', {\n      title: 'new list',\n      boardId: window.board.id\n    })\n\n});\n",[23987],{"type":27,"tag":653,"props":23988,"children":23989},{"__ignoreMap":5},[23990],{"type":32,"value":23985},{"type":27,"tag":28,"props":23992,"children":23993},{},[23994,23996,24001],{"type":32,"value":23995},"I would not entirely recommend this approach, but it’s out there. The reason I’m not recommending it is that you should try to avoid your tests from being dependent on each other. If first test fails here, it automatically makes the other test fail too, even though it might theoretically pass. However, using window context might help when you try to collect data from your whole spec and then use it in ",{"type":27,"tag":653,"props":23997,"children":23999},{"className":23998},[],[24000],{"type":32,"value":20892},{"type":32,"value":8442},{"type":27,"tag":45,"props":24003,"children":24005},{"id":24004},"using-environment",[24006],{"type":32,"value":24007},"Using environment",{"type":27,"tag":28,"props":24009,"children":24010},{},[24011,24013,24020,24022,24027],{"type":32,"value":24012},"This approach is similar to what is often done in ",{"type":27,"tag":172,"props":24014,"children":24017},{"href":24015,"rel":24016},"https://www.postman.com/",[696],[24018],{"type":32,"value":24019},"Postman",{"type":32,"value":24021},". With Postman, you often use environment to store data from requests. I personally use ",{"type":27,"tag":653,"props":24023,"children":24025},{"className":24024},[],[24026],{"type":32,"value":9544},{"type":32,"value":24028}," to store any data that my server returns. In short, using it looks like this:",{"type":27,"tag":793,"props":24030,"children":24033},{"className":24031,"code":24032,"language":1513,"meta":5},[1510]," cy\n   .request('POST', '/api/boards', { name: 'new board' })\n   .then(({ body }) => {\n\n     Cypress.env('board', body)\n\n   })\n",[24034],{"type":27,"tag":653,"props":24035,"children":24036},{"__ignoreMap":5},[24037],{"type":32,"value":24032},{"type":27,"tag":28,"props":24039,"children":24040},{},[24041,24043,24048],{"type":32,"value":24042},"So far it does not look too different from everything else. To leverage ",{"type":27,"tag":653,"props":24044,"children":24046},{"className":24045},[],[24047],{"type":32,"value":9544},{"type":32,"value":24049}," I actually do a couple of more things. Here are the steps:",{"type":27,"tag":851,"props":24051,"children":24052},{},[24053,24065,24070,24075],{"type":27,"tag":109,"props":24054,"children":24055},{},[24056,24058,24063],{"type":32,"value":24057},"Create storage space in ",{"type":27,"tag":653,"props":24059,"children":24061},{"className":24060},[],[24062],{"type":32,"value":20691},{"type":32,"value":24064}," file",{"type":27,"tag":109,"props":24066,"children":24067},{},[24068],{"type":32,"value":24069},"Create custom command for API calls",{"type":27,"tag":109,"props":24071,"children":24072},{},[24073],{"type":32,"value":24074},"Add types for custom commands",{"type":27,"tag":109,"props":24076,"children":24077},{},[24078],{"type":32,"value":24079},"Add types for storage",{"type":27,"tag":45,"props":24081,"children":24083},{"id":24082},"creating-a-storage",[24084],{"type":32,"value":24085},"Creating a storage",{"type":27,"tag":28,"props":24087,"children":24088},{},[24089,24091,24096,24098,24103,24105,24109,24111,24116,24118,24122,24124,24129,24131,24136,24138,24143],{"type":32,"value":24090},"The inspiration for creating a „data storage“ came from when I was creating my ",{"type":27,"tag":172,"props":24092,"children":24094},{"href":19041,"rel":24093},[696],[24095],{"type":32,"value":20078},{"type":32,"value":24097},". This app is built in Vue, which uses data object, where all your app data is stored. Data can be read or retrieved, but the main point here is that you have a single storage. In this storage, you define where your data should be placed. So all ",{"type":27,"tag":79,"props":24099,"children":24100},{},[24101],{"type":32,"value":24102},"boards",{"type":32,"value":24104}," are stored in ",{"type":27,"tag":79,"props":24106,"children":24107},{},[24108],{"type":32,"value":24102},{"type":32,"value":24110}," array, ",{"type":27,"tag":79,"props":24112,"children":24113},{},[24114],{"type":32,"value":24115},"lists",{"type":32,"value":24117}," are in ",{"type":27,"tag":79,"props":24119,"children":24120},{},[24121],{"type":32,"value":24115},{"type":32,"value":24123}," array, etc. To define storage for my app, I create a ",{"type":27,"tag":653,"props":24125,"children":24127},{"className":24126},[],[24128],{"type":32,"value":7243},{"type":32,"value":24130}," hook in my ",{"type":27,"tag":653,"props":24132,"children":24134},{"className":24133},[],[24135],{"type":32,"value":20691},{"type":32,"value":24137}," file and define attributes my ",{"type":27,"tag":653,"props":24139,"children":24141},{"className":24140},[],[24142],{"type":32,"value":9544},{"type":32,"value":24144}," and their initial values:",{"type":27,"tag":793,"props":24146,"children":24148},{"className":24147,"code":21030,"filename":17310,"language":1513,"meta":5},[1510],[24149],{"type":27,"tag":653,"props":24150,"children":24151},{"__ignoreMap":5},[24152],{"type":32,"value":21030},{"type":27,"tag":45,"props":24154,"children":24156},{"id":24155},"creating-a-custom-command-for-api-calls",[24157],{"type":32,"value":24158},"Creating a custom command for API calls",{"type":27,"tag":28,"props":24160,"children":24161},{},[24162],{"type":32,"value":24163},"Next, I’ll add my request as a custom command:",{"type":27,"tag":793,"props":24165,"children":24170},{"className":24166,"code":24167,"filename":24168,"highlights":24169,"language":1513,"meta":5},[1510],"\nCypress.Commands.add('addBoardApi', (name) => {\n\n  cy\n    .request('POST', '/api/boards', { name })\n    .then(({ body }) => {\n\n      Cypress.env('boards').push(body)\n\n    })\n\n})\n","support/commands/addBoardApi.ts",[3668],[24171],{"type":27,"tag":653,"props":24172,"children":24173},{"__ignoreMap":5},[24174],{"type":32,"value":24167},{"type":27,"tag":28,"props":24176,"children":24177},{},[24178,24180,24185],{"type":32,"value":24179},"Now, whenever I call my custom command, the response of my request is going to be saved into ",{"type":27,"tag":653,"props":24181,"children":24183},{"className":24182},[],[24184],{"type":32,"value":24102},{"type":32,"value":24186}," array. Whenever I need to access this storage, I can just use it in my code like this:",{"type":27,"tag":793,"props":24188,"children":24191},{"className":24189,"code":24190,"language":1513,"meta":5},[1510],"Cypress.env('boards')[0].id\n",[24192],{"type":27,"tag":653,"props":24193,"children":24194},{"__ignoreMap":5},[24195],{"type":32,"value":24190},{"type":27,"tag":28,"props":24197,"children":24198},{},[24199],{"type":32,"value":24200},"This will effectively access my board id. This does not entirely solve the problem of callback hell however, since I will not be able to access my board id just like this:",{"type":27,"tag":793,"props":24202,"children":24205},{"className":24203,"code":24204,"language":1513,"meta":5},[1510],"it('creates a list', () => {\n\n  cy\n    .addBoardApi('new board')\n\n  cy\n    .request('POST', '/api/lists', { title: 'new list', boardId: Cypress.env('boards')[0].id })\n\n});\n",[24206],{"type":27,"tag":653,"props":24207,"children":24208},{"__ignoreMap":5},[24209],{"type":32,"value":24204},{"type":27,"tag":28,"props":24211,"children":24212},{},[24213,24215,24221,24223,24228,24230,24235],{"type":32,"value":24214},"This will throw an error, because our ",{"type":27,"tag":653,"props":24216,"children":24218},{"className":24217},[],[24219],{"type":32,"value":24220},"Cypress.env('boards')[0].id",{"type":32,"value":24222}," will still be ",{"type":27,"tag":653,"props":24224,"children":24226},{"className":24225},[],[24227],{"type":32,"value":19264},{"type":32,"value":24229},". But using a custom command is similar to using ",{"type":27,"tag":653,"props":24231,"children":24233},{"className":24232},[],[24234],{"type":32,"value":13338},{"type":32,"value":24236}," function. So we can write a custom command for our second request as well. Since we now have a storage, we can use it and look into our storage for the proper uuid:",{"type":27,"tag":793,"props":24238,"children":24241},{"className":24239,"code":24240,"language":1513,"meta":5},[1510],"Cypress.Commands.add('addListApi', ({ title, boardIndex = 0 }) => {\n\n  cy\n    .request('POST', '/api/lists', {\n      boardId: Cypress.env('boards')[boardIndex].id,\n      title,\n    }).then(({ body }) => {\n      Cypress.env('lists').push(body);\n    });\n\n});\n",[24242],{"type":27,"tag":653,"props":24243,"children":24244},{"__ignoreMap":5},[24245],{"type":32,"value":24240},{"type":27,"tag":28,"props":24247,"children":24248},{},[24249],{"type":32,"value":24250},"This way, we can reference our board using index. We can create two boards in our test and add a list just inside the second one.",{"type":27,"tag":793,"props":24252,"children":24256},{"className":24253,"code":24254,"highlights":24255,"language":1513,"meta":5},[1510],"it('creates a list', () => {\n\n  cy\n    .addBoardApi('first board')\n    .addBoardApi('second board')\n    .addListApi({ title: 'new list', boardIndex: 1})\n\n});\n",[3809],[24257],{"type":27,"tag":653,"props":24258,"children":24259},{"__ignoreMap":5},[24260],{"type":32,"value":24254},{"type":27,"tag":28,"props":24262,"children":24263},{},[24264,24266,24270,24272,24277,24279,24285,24287,24293,24295,24301,24303,24308],{"type":32,"value":24265},"This will create a ",{"type":27,"tag":79,"props":24267,"children":24268},{},[24269],{"type":32,"value":23911},{"type":32,"value":24271}," in our ",{"type":27,"tag":79,"props":24273,"children":24274},{},[24275],{"type":32,"value":24276},"second board",{"type":32,"value":24278},". Our custom ",{"type":27,"tag":653,"props":24280,"children":24282},{"className":24281},[],[24283],{"type":32,"value":24284},".addListApi()",{"type":32,"value":24286}," command defaults ",{"type":27,"tag":653,"props":24288,"children":24290},{"className":24289},[],[24291],{"type":32,"value":24292},"boardIndex",{"type":32,"value":24294}," option to ",{"type":27,"tag":653,"props":24296,"children":24298},{"className":24297},[],[24299],{"type":32,"value":24300},"0",{"type":32,"value":24302},", we don’t even have to add this option if we are just creating a single board. Compared to all the ",{"type":27,"tag":653,"props":24304,"children":24306},{"className":24305},[],[24307],{"type":32,"value":13338},{"type":32,"value":24309}," functions, this is much easier to read.",{"type":27,"tag":45,"props":24311,"children":24313},{"id":24312},"add-types-for-custom-commands",[24314],{"type":32,"value":24074},{"type":27,"tag":28,"props":24316,"children":24317},{},[24318,24320,24327,24329,24335],{"type":32,"value":24319},"You may have already noticed that I’m using TypeScript for most of my tests. I suggest you ",{"type":27,"tag":172,"props":24321,"children":24324},{"href":24322,"rel":24323},"https://docs.cypress.io/guides/tooling/typescript-support.html#Install-TypeScript",[696],[24325],{"type":32,"value":24326},"check out the documentation on TypeScript",{"type":32,"value":24328}," to get yourself up and running. One cool perk of using TypeScript is that you add your command type definition really easily. This enables Intellisense autocomplete and helps anyone who will use your custom commands in the future. To add these, I create a ",{"type":27,"tag":653,"props":24330,"children":24332},{"className":24331},[],[24333],{"type":32,"value":24334},"commands.d.ts",{"type":32,"value":786},{"type":27,"tag":793,"props":24337,"children":24341},{"className":24338,"code":24339,"filename":24340,"language":3520,"meta":5},[3517],"declare namespace Cypress {\n  interface Chainable {\n    /**\n     * creates a new board via API\n    */\n    addBoardApi(name: string): Chainable\u003CElement>\n\n    /**\n     * Adds new list via API\n    */\n    addListApi(options: {\n      title: string;\n      boardIndex?: string;\n    }): Chainable\u003CElement>\n\n  }\n}\n","support/@types/commands.d.ts",[24342],{"type":27,"tag":653,"props":24343,"children":24344},{"__ignoreMap":5},[24345],{"type":32,"value":24339},{"type":27,"tag":45,"props":24347,"children":24349},{"id":24348},"add-types-for-storage",[24350],{"type":32,"value":24079},{"type":27,"tag":28,"props":24352,"children":24353},{},[24354,24356,24361,24363,24368],{"type":32,"value":24355},"As a final touch I’m adding a code that my colleague put together for me. This enables me to add our own environment keys which will pop up whenever I reference one of my storage items in ",{"type":27,"tag":653,"props":24357,"children":24359},{"className":24358},[],[24360],{"type":32,"value":9544},{"type":32,"value":24362},". This code basically expands types for ",{"type":27,"tag":653,"props":24364,"children":24366},{"className":24365},[],[24367],{"type":32,"value":9544},{"type":32,"value":24369}," function",{"type":27,"tag":793,"props":24371,"children":24375},{"className":24372,"code":24373,"filename":24374,"language":3520,"meta":5},[3517],"export { };\n\ndeclare global {\n  namespace Cypress {\n\n    export interface Cypress {\n\n      /**\n       * Returns all environment variables set with CYPRESS_ prefix or in \"env\" object in \"cypress.config.js\"\n       *\n       * @see https://on.cypress.io/env\n       */\n      env(): Partial\u003CEnvKeys>;\n      /**\n       * Returns specific environment variable or undefined\n       * @see https://on.cypress.io/env\n       * @example\n       *    // cypress.config.js\n       *    env: { foo: \"bar\" }\n       *    Cypress.env(\"foo\") // => bar\n       */\n      env\u003CT extends keyof EnvKeys>(key: T): EnvKeys[T];\n      /**\n       * Set value for a variable.\n       * Any value you change will be permanently changed for the remainder of your tests.\n       * @see https://on.cypress.io/env\n       * @example\n       *    Cypress.env(\"host\", \"http://server.dev.local\")\n       */\n      env\u003CT extends keyof EnvKeys>(key: T, value: EnvKeys[T]): void;\n\n      /**\n       * Set values for multiple variables at once. Values are merged with existing values.\n       * @see https://on.cypress.io/env\n       * @example\n       *    Cypress.env({ host: \"http://server.dev.local\", foo: \"foo\" })\n       */\n      env(object: Partial\u003CEnvKeys>): void;\n\n    }\n\n  }\n}\n\ninterface EnvKeys {\n  'boards': Array\u003C{\n    created: string;\n    id: number;\n    name: string;\n    starred: boolean;\n    user: number;\n  }>;\n  'lists': Array\u003C{\n    boardId: number\n    title: string\n    id: number\n    created: string\n  }>;\n}\n","support/@types/env.d.ts",[24376],{"type":27,"tag":653,"props":24377,"children":24378},{"__ignoreMap":5},[24379],{"type":32,"value":24373},{"type":27,"tag":45,"props":24381,"children":24382},{"id":1820},[24383],{"type":32,"value":24384},"Putting it all together",{"type":27,"tag":28,"props":24386,"children":24387},{},[24388,24390,24395],{"type":32,"value":24389},"This pattern effectively creates a testing library, where all API endpoints have a custom command and responses are stored in my ",{"type":27,"tag":653,"props":24391,"children":24393},{"className":24392},[],[24394],{"type":32,"value":9544},{"type":32,"value":24396}," storage. I end up writing a test that looks something like this:",{"type":27,"tag":793,"props":24398,"children":24401},{"className":24399,"code":24400,"language":1513,"meta":5},[1510],"beforeEach(() => {\n\n    cy\n      .addBoardApi('hello board')\n      .addListApi({ title: 'hello list' });\n\n  });\n\n  it('create a task', () => {\n\n    cy\n      .visit(`/board/${Cypress.env('boards')[0].id}`);\n\n    cy\n      .get('.List_addTask')\n      .click();\n\n    cy\n      .get('.ListContainer .TextArea')\n      .should('be.visible')\n      .type('new task{enter}');\n\n    cy\n      .get('.Task')\n      .should('be.visible');\n\n  });\n",[24402],{"type":27,"tag":653,"props":24403,"children":24404},{"__ignoreMap":5},[24405],{"type":32,"value":24400},{"type":27,"tag":28,"props":24407,"children":24408},{},[24409,24411,24416,24418,24423,24425,24430],{"type":32,"value":24410},"I prepare my test state in ",{"type":27,"tag":653,"props":24412,"children":24414},{"className":24413},[],[24415],{"type":32,"value":7243},{"type":32,"value":24417}," hook, and to the rest in my ",{"type":27,"tag":653,"props":24419,"children":24421},{"className":24420},[],[24422],{"type":32,"value":7187},{"type":32,"value":24424}," block. This helps me getting a clear idea on what is happening before my test as well as inside my test. I would probably create a custom command for my ",{"type":27,"tag":653,"props":24426,"children":24428},{"className":24427},[],[24429],{"type":32,"value":14057},{"type":32,"value":24431}," as well since opening my board would be a very frequent action in which I need my board id. But that’s a story for another time.",{"type":27,"tag":28,"props":24433,"children":24434},{},[24435,24437,24443,24445,24451,24453,24459],{"type":32,"value":24436},"You can check this code out on ",{"type":27,"tag":172,"props":24438,"children":24440},{"href":19041,"rel":24439},[696],[24441],{"type":32,"value":24442},"my Trello clone app",{"type":32,"value":24444}," or you can ",{"type":27,"tag":172,"props":24446,"children":24448},{"href":12181,"rel":24447},[696],[24449],{"type":32,"value":24450},"join me on my YouTube channel",{"type":32,"value":24452}," to see how I work with this pattern. If you have any comments, suggestions, or just want to chat, feel free to ",{"type":27,"tag":172,"props":24454,"children":24456},{"href":20722,"rel":24455},[696],[24457],{"type":32,"value":24458},"join my Discord channel",{"type":32,"value":24460},". See you there!",{"title":5,"searchDepth":320,"depth":320,"links":24462},[24463,24464,24465,24466,24467,24468,24469,24470,24471,24472],{"id":23773,"depth":320,"text":23776},{"id":23863,"depth":320,"text":23866},{"id":23931,"depth":320,"text":23934},{"id":23973,"depth":320,"text":23976},{"id":24004,"depth":320,"text":24007},{"id":24082,"depth":320,"text":24085},{"id":24155,"depth":320,"text":24158},{"id":24312,"depth":320,"text":24074},{"id":24348,"depth":320,"text":24079},{"id":1820,"depth":320,"text":24384},"content:working-with-api-response-data-in-cypress:index.md","working-with-api-response-data-in-cypress/index.md","working-with-api-response-data-in-cypress/index",{"_path":14445,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":24477,"description":24478,"slug":24479,"date":24480,"published":10,"tags":24481,"cypressVersion":5959,"readingTime":24484,"body":24488,"_type":329,"_id":24908,"_source":331,"_file":24909,"_stem":24910,"_extension":334},"Create a configuration plugin in Cypress","There are several different ways to configure how your Cypress tests can be configured. In this post, I’m describing these ways and explain how you can view them in Cypress GUI.","create-a-configuration-plugin-in-cypress","2020-11-23",[5279,7555,24482,24483],"plugins","headless",{"text":927,"minutes":24485,"time":24486,"words":24487},4.145,248700,829,{"type":24,"children":24489,"toc":24903},[24490,24495,24501,24528,24538,24543,24551,24573,24577,24610,24619,24624,24648,24657,24670,24690,24699,24718,24727,24738,24746,24758,24767,24779,24788,24793,24802,24840,24846,24873,24882,24895],{"type":27,"tag":28,"props":24491,"children":24492},{},[24493],{"type":32,"value":24494},"Last week I have been playing with configuration in Cypress. I find configuring different environments to be especially hard topic. Especially for people that are beginning. In this blog, I would like to break configuration into small steps, that will help you navigate this topic.",{"type":27,"tag":45,"props":24496,"children":24498},{"id":24497},"basics-of-configuration",[24499],{"type":32,"value":24500},"Basics of configuration",{"type":27,"tag":28,"props":24502,"children":24503},{},[24504,24506,24511,24513,24518,24520,24526],{"type":32,"value":24505},"Every Cypress project has a ",{"type":27,"tag":653,"props":24507,"children":24509},{"className":24508},[],[24510],{"type":32,"value":6028},{"type":32,"value":24512}," file. In this file, you have an ",{"type":27,"tag":653,"props":24514,"children":24516},{"className":24515},[],[24517],{"type":32,"value":9529},{"type":32,"value":24519}," attribute, that is usually filled with environment variables. Let's say I add a variable ",{"type":27,"tag":653,"props":24521,"children":24523},{"className":24522},[],[24524],{"type":32,"value":24525},"app",{"type":32,"value":24527}," to this attribute like this:",{"type":27,"tag":793,"props":24529,"children":24533},{"className":24530,"code":24531,"filename":6028,"highlights":24532,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    env: {\n      app: \"local\"\n    }\n  }\n})\n",[3809],[24534],{"type":27,"tag":653,"props":24535,"children":24536},{"__ignoreMap":5},[24537],{"type":32,"value":24531},{"type":27,"tag":28,"props":24539,"children":24540},{},[24541],{"type":32,"value":24542},"When I now open Cypress in GUI mode, I can look into settings tab and see my config file being read, highlighted in blue.",{"type":27,"tag":28,"props":24544,"children":24545},{},[24546],{"type":27,"tag":959,"props":24547,"children":24550},{"alt":24548,"src":24549},"Cypress env variable added in cypress.config.js file","cypress-config.png",[],{"type":27,"tag":28,"props":24552,"children":24553},{},[24554,24556,24562,24564,24571],{"type":32,"value":24555},"As you can see from the screenshot, there are multiple ways to add your environment variables. You can add them with a ",{"type":27,"tag":653,"props":24557,"children":24559},{"className":24558},[],[24560],{"type":32,"value":24561},"CYPRESS_",{"type":32,"value":24563}," prefix to your environment or pass them through CLI as arguments. ",{"type":27,"tag":172,"props":24565,"children":24568},{"href":24566,"rel":24567},"https://docs.cypress.io/guides/guides/environment-variables.html#Setting",[696],[24569],{"type":32,"value":24570},"Take a peek into the documentation",{"type":32,"value":24572},", where you can find examples of all these approaches.",{"type":27,"tag":45,"props":24574,"children":24575},{"id":17455},[24576],{"type":32,"value":17458},{"type":27,"tag":28,"props":24578,"children":24579},{},[24580,24582,24587,24588,24594,24595,24601,24602,24608],{"type":32,"value":24581},"With more complicated setups, you might want to combine these approaches. Let’s say you have multiple environments, like ",{"type":27,"tag":653,"props":24583,"children":24585},{"className":24584},[],[24586],{"type":32,"value":14935},{"type":32,"value":3372},{"type":27,"tag":653,"props":24589,"children":24591},{"className":24590},[],[24592],{"type":32,"value":24593},"staging",{"type":32,"value":3372},{"type":27,"tag":653,"props":24596,"children":24598},{"className":24597},[],[24599],{"type":32,"value":24600},"preprod",{"type":32,"value":4164},{"type":27,"tag":653,"props":24603,"children":24605},{"className":24604},[],[24606],{"type":32,"value":24607},"prod",{"type":32,"value":24609},". Each of these has its own home url, its own version of api and its own redirect page. These configurations might look something like this:",{"type":27,"tag":793,"props":24611,"children":24614},{"className":24612,"code":24613,"language":1513,"meta":5},[1510],"// local\nbaseUrl: 'http://localhost:3000'\napi: 'development'\nredirect: 'https://www.product-staging.com'\n\n// staging\nbaseUrl: 'http://dashboard.product-staging.com'\napi: 'development'\nredirect: 'https://www.product-staging.com'\n\n// preprod\nbaseUrl: 'http://dashboard-preprod.product.com'\napi: 'v2'\nredirect: 'https://www.product.com'\n\n// prod\nbaseUrl: 'http://dashboard.product.com'\napi: 'v1'\nredirect: 'https://www.product.com'\n",[24615],{"type":27,"tag":653,"props":24616,"children":24617},{"__ignoreMap":5},[24618],{"type":32,"value":24613},{"type":27,"tag":28,"props":24620,"children":24621},{},[24622],{"type":32,"value":24623},"As you can see this is kind of a mess. But this often happens in real life situations. You have got different combinations of APIs and URLs that differ from environment to environment. More than that, you may want to add some key or secret from environment that should not be in the codebase. Let’s dive into how we can solve this.",{"type":27,"tag":28,"props":24625,"children":24626},{},[24627,24629,24634,24635,24640,24642],{"type":32,"value":24628},"First of all, let’s see how the ",{"type":27,"tag":653,"props":24630,"children":24632},{"className":24631},[],[24633],{"type":32,"value":11257},{"type":32,"value":14500},{"type":27,"tag":653,"props":24636,"children":24638},{"className":24637},[],[24639],{"type":32,"value":6028},{"type":32,"value":24641}," file works. Let’s show this on a simple example. We’ll add this piece of code and open Cypress via via ",{"type":27,"tag":653,"props":24643,"children":24645},{"className":24644},[],[24646],{"type":32,"value":24647},"npx cypress open",{"type":27,"tag":793,"props":24649,"children":24652},{"className":24650,"code":24651,"filename":6028,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    env: {\n      app: \"local\"\n    },\n    setupNodeEvents(on, config) {\n      console.log(config.env)\n    }\n  }\n})\n",[24653],{"type":27,"tag":653,"props":24654,"children":24655},{"__ignoreMap":5},[24656],{"type":32,"value":24651},{"type":27,"tag":28,"props":24658,"children":24659},{},[24660,24662,24668],{"type":32,"value":24661},"When we open Cypress, we will see ",{"type":27,"tag":653,"props":24663,"children":24665},{"className":24664},[],[24666],{"type":32,"value":24667},"{ app: 'local' }",{"type":32,"value":24669}," logged out to our terminal.",{"type":27,"tag":28,"props":24671,"children":24672},{},[24673,24675,24680,24682,24688],{"type":32,"value":24674},"Now let’s clear out our ",{"type":27,"tag":653,"props":24676,"children":24678},{"className":24677},[],[24679],{"type":32,"value":6028},{"type":32,"value":24681}," and instead of this let’s pass a ",{"type":27,"tag":653,"props":24683,"children":24685},{"className":24684},[],[24686],{"type":32,"value":24687},"version",{"type":32,"value":24689}," flag via CLI to our environment, like this:",{"type":27,"tag":793,"props":24691,"children":24694},{"className":24692,"code":24693,"language":1084,"meta":5},[1082],"npx cypress open --env version=\"prod\"\n",[24695],{"type":27,"tag":653,"props":24696,"children":24697},{"__ignoreMap":5},[24698],{"type":32,"value":24693},{"type":27,"tag":28,"props":24700,"children":24701},{},[24702,24704,24710,24712,24717],{"type":32,"value":24703},"Upon opening Cypress, you would see ",{"type":27,"tag":653,"props":24705,"children":24707},{"className":24706},[],[24708],{"type":32,"value":24709},"{ version: 'prod' }",{"type":32,"value":24711}," logged out to our terminal. Let's now use this information and change our config according to what we pass in ",{"type":27,"tag":653,"props":24713,"children":24715},{"className":24714},[],[24716],{"type":32,"value":24687},{"type":32,"value":3955},{"type":27,"tag":793,"props":24719,"children":24722},{"className":24720,"code":24721,"filename":6028,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n      if (config.env.version === \"prod\") {\n        config.baseUrl = 'http://dashboard.product.com'\n        config.env.api = 'v2'\n        config.env.redirect = 'https://www.product.com'\n      }\n      return config\n    }\n  }\n})\n",[24723],{"type":27,"tag":653,"props":24724,"children":24725},{"__ignoreMap":5},[24726],{"type":32,"value":24721},{"type":27,"tag":28,"props":24728,"children":24729},{},[24730,24731,24736],{"type":32,"value":14645},{"type":27,"tag":653,"props":24732,"children":24734},{"className":24733},[],[24735],{"type":32,"value":11257},{"type":32,"value":24737},", we are now adding some more variables to our env. When we look into our settings in Cypress CLI, you can see that our env variables have been added.",{"type":27,"tag":28,"props":24739,"children":24740},{},[24741],{"type":27,"tag":959,"props":24742,"children":24745},{"alt":24743,"src":24744},"Cypress env variable added through setupNodeEvents","cypress-plugin.png",[],{"type":27,"tag":28,"props":24747,"children":24748},{},[24749,24751,24756],{"type":32,"value":24750},"This helps demonstrate how creating dynamic configuration works. Let's now say that our configuration paths are stored in separate json files, for which we created a separate ",{"type":27,"tag":653,"props":24752,"children":24754},{"className":24753},[],[24755],{"type":32,"value":5957},{"type":32,"value":24757}," folder in our Cypress project:",{"type":27,"tag":793,"props":24759,"children":24762},{"className":24760,"code":24761,"language":2042,"meta":5},[2040],"cypress/\n└── config/\n    ├── local.json\n    ├── staging.json\n    ├── preprod.json\n    └── prod.json\n",[24763],{"type":27,"tag":653,"props":24764,"children":24765},{"__ignoreMap":5},[24766],{"type":32,"value":24761},{"type":27,"tag":28,"props":24768,"children":24769},{},[24770,24772,24777],{"type":32,"value":24771},"Let's now say, that for each ",{"type":27,"tag":653,"props":24773,"children":24775},{"className":24774},[],[24776],{"type":32,"value":24687},{"type":32,"value":24778}," flag, we want to load a different file and pass it into Cypress as our config and environment. In order to do this, we can rewrite our config file like this:",{"type":27,"tag":793,"props":24780,"children":24783},{"className":24781,"code":24782,"filename":6028,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n      // if version not defined, use local\n      const version = config.env.version || 'local'\n\n      // load env from json\n      const envConfig = require(`./config/${version}.json`)\n      config.env = { ...config.env, ...envConfig }\n\n      // change baseUrl\n      config.baseUrl = envConfig.baseUrl\n\n      return config\n    }\n  }\n})\n",[24784],{"type":27,"tag":653,"props":24785,"children":24786},{"__ignoreMap":5},[24787],{"type":32,"value":24782},{"type":27,"tag":28,"props":24789,"children":24790},{},[24791],{"type":32,"value":24792},"This way we can load whichever file we want and even add some more json files for different configurations. Whichever file we pass to our CLI will be read. Now we can take it a step further, and instead of using CLI flag, we will use Cypress environment variable like this:",{"type":27,"tag":793,"props":24794,"children":24797},{"className":24795,"code":24796,"language":1084,"meta":5},[1082],"cypress_version=preprod npx cypress open\n",[24798],{"type":27,"tag":653,"props":24799,"children":24800},{"__ignoreMap":5},[24801],{"type":32,"value":24796},{"type":27,"tag":28,"props":24803,"children":24804},{},[24805,24807,24813,24815,24819,24820,24825,24827,24832,24834,24839],{"type":32,"value":24806},"As mentioned, anything passed to our CLI with prefix ",{"type":27,"tag":653,"props":24808,"children":24810},{"className":24809},[],[24811],{"type":32,"value":24812},"cypress_",{"type":32,"value":24814}," will be added as environment variable to Cypress. Notice how I pass these ",{"type":27,"tag":79,"props":24816,"children":24817},{},[24818],{"type":32,"value":20752},{"type":32,"value":7660},{"type":27,"tag":653,"props":24821,"children":24823},{"className":24822},[],[24824],{"type":32,"value":11098},{"type":32,"value":24826}," as opposed to ",{"type":27,"tag":653,"props":24828,"children":24830},{"className":24829},[],[24831],{"type":32,"value":6396},{"type":32,"value":24833}," flag that is passed ",{"type":27,"tag":79,"props":24835,"children":24836},{},[24837],{"type":32,"value":24838},"after",{"type":32,"value":256},{"type":27,"tag":45,"props":24841,"children":24843},{"id":24842},"handling-secrets",[24844],{"type":32,"value":24845},"Handling secrets",{"type":27,"tag":28,"props":24847,"children":24848},{},[24849,24851,24857,24858,24863,24865,24871],{"type":32,"value":24850},"There are probably couple of keys or secrets that you don't want to keep in your code base. You usually keep them in your ",{"type":27,"tag":653,"props":24852,"children":24854},{"className":24853},[],[24855],{"type":32,"value":24856},".bashrc",{"type":32,"value":1591},{"type":27,"tag":653,"props":24859,"children":24861},{"className":24860},[],[24862],{"type":32,"value":1472},{"type":32,"value":24864}," file, depending on what shell you use. Or you may use ",{"type":27,"tag":172,"props":24866,"children":24868},{"href":9597,"rel":24867},[696],[24869],{"type":32,"value":24870},"dotenv plugin",{"type":32,"value":24872}," to store your variables per project. This works too. To read these variables and use them in your tests, you can add following to your configuration:",{"type":27,"tag":793,"props":24874,"children":24877},{"className":24875,"code":24876,"filename":6028,"language":1513,"meta":5},[1510],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  e2e: {\n    setupNodeEvents(on, config) {\n      // if version not defined, use local\n      const version = config.env.version || 'local'\n\n      // load env from json\n      const envConfig = require(`./config/${version}.json`)\n      config.env = { ...config.env, ...envConfig }\n\n      // change baseUrl\n      config.baseUrl = envConfig.baseUrl\n\n      // add a secret key\n      config.env.SECRET_KEY = process.env.SECRET_KEY\n\n      return config\n    }\n  }\n})\n",[24878],{"type":27,"tag":653,"props":24879,"children":24880},{"__ignoreMap":5},[24881],{"type":32,"value":24876},{"type":27,"tag":28,"props":24883,"children":24884},{},[24885,24887,24893],{"type":32,"value":24886},"When you open your tests now, you can see the ",{"type":27,"tag":653,"props":24888,"children":24890},{"className":24889},[],[24891],{"type":32,"value":24892},"SECRET_KEY",{"type":32,"value":24894}," being displayed in our project settings.",{"type":27,"tag":28,"props":24896,"children":24897},{},[24898],{"type":27,"tag":959,"props":24899,"children":24902},{"alt":24900,"src":24901},"Environment variable is read in Cypress","environment.png",[],{"title":5,"searchDepth":320,"depth":320,"links":24904},[24905,24906,24907],{"id":24497,"depth":320,"text":24500},{"id":17455,"depth":320,"text":17458},{"id":24842,"depth":320,"text":24845},"content:create-a-configuration-plugin-in-cypress:index.md","create-a-configuration-plugin-in-cypress/index.md","create-a-configuration-plugin-in-cypress/index",{"_path":21111,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":24912,"description":24913,"slug":24914,"date":24915,"published":10,"tags":24916,"readingTime":24917,"body":24918,"_type":329,"_id":25421,"_source":331,"_file":25422,"_stem":25423,"_extension":334},"Test grepping in Cypress using Module API","Sometimes you want to run just a subset of your tests. With Module API, you can achieve just that. Showcasing how you can grep your tests by folder.","test-grepping-in-cypress-using-module-api","2020-11-09",[9845,24483,5279],{"text":927,"minutes":11458,"time":11459,"words":11460},{"type":24,"children":24919,"toc":25419},[24920,24925,24930,24939,24973,24982,24987,25008,25017,25029,25038,25049,25058,25078,25088,25093,25103,25115,25198,25207,25226,25236,25265,25276,25286,25306,25316,25321,25330,25342,25376,25381,25394],{"type":27,"tag":28,"props":24921,"children":24922},{},[24923],{"type":32,"value":24924},"If you are running hundreds of tests in Cypress, chances are you may want to run just a subset of them. There are several ways you can do this, and in this blog, I’d like to show you mine. If you are here just for the solution, feel free to scroll down to the end end of this blog where you’ll find the code.",{"type":27,"tag":28,"props":24926,"children":24927},{},[24928],{"type":32,"value":24929},"As you probably know, to run all of your Cypress tests, you can type following command into your command line:",{"type":27,"tag":793,"props":24931,"children":24934},{"className":24932,"code":24933,"language":1084,"meta":5},[1082],"npx cypress run\n",[24935],{"type":27,"tag":653,"props":24936,"children":24937},{"__ignoreMap":5},[24938],{"type":32,"value":24933},{"type":27,"tag":28,"props":24940,"children":24941},{},[24942,24944,24950,24952,24957,24958,24964,24966,24971],{"type":32,"value":24943},"This will run all tests inside your current Cypress project. These are usually stored in ",{"type":27,"tag":653,"props":24945,"children":24947},{"className":24946},[],[24948],{"type":32,"value":24949},"integration",{"type":32,"value":24951}," folder. I usually like to create more folders inside to create separate test categories. Let’s say I have an ",{"type":27,"tag":653,"props":24953,"children":24955},{"className":24954},[],[24956],{"type":32,"value":11456},{"type":32,"value":4164},{"type":27,"tag":653,"props":24959,"children":24961},{"className":24960},[],[24962],{"type":32,"value":24963},"ui",{"type":32,"value":24965}," folder. To run each one of them, I could create a separate npm scripts, so in my ",{"type":27,"tag":653,"props":24967,"children":24969},{"className":24968},[],[24970],{"type":32,"value":734},{"type":32,"value":24972}," I’d have the following:",{"type":27,"tag":793,"props":24974,"children":24977},{"className":24975,"code":24976,"filename":734,"language":1004,"meta":5},[1002],"{\n  \"scripts\": {\n    \"cy:run\": \"npx cypress run\",\n    \"cy:run:api\": \"npx cypress run --spec ./cypress/integration/api/*.ts\",\n    \"cy:run:ui\": \"npx cypress run --spec ./cypress/integration/ui/*.ts\"\n  }\n}\n",[24978],{"type":27,"tag":653,"props":24979,"children":24980},{"__ignoreMap":5},[24981],{"type":32,"value":24976},{"type":27,"tag":28,"props":24983,"children":24984},{},[24985],{"type":32,"value":24986},"These commands of course work well, but in order to run each of my test folders, I need to run a separate command. This is not such a big deal when there are only two folders, but if you have multiple of them, things can get complicated.",{"type":27,"tag":28,"props":24988,"children":24989},{},[24990,24991,24998,25000,25006],{"type":32,"value":5425},{"type":27,"tag":172,"props":24992,"children":24995},{"href":24993,"rel":24994},"https://docs.cypress.io/guides/guides/module-api.html#Options",[696],[24996],{"type":32,"value":24997},"Module API",{"type":32,"value":24999}," comes in super handy and I’ll show you how in a second. First, let’s write our run script with Module API. We’ll create a new ",{"type":27,"tag":653,"props":25001,"children":25003},{"className":25002},[],[25004],{"type":32,"value":25005},"cypress.js",{"type":32,"value":25007}," file in the root of our project and add following code inside:",{"type":27,"tag":793,"props":25009,"children":25012},{"className":25010,"code":25011,"filename":25005,"language":1513,"meta":5},[1510],"const cypress = require('cypress');\n\ncypress.run();\n\n",[25013],{"type":27,"tag":653,"props":25014,"children":25015},{"__ignoreMap":5},[25016],{"type":32,"value":25011},{"type":27,"tag":28,"props":25018,"children":25019},{},[25020,25022,25027],{"type":32,"value":25021},"This is pretty much the same thing as if we ran our ",{"type":27,"tag":653,"props":25023,"children":25025},{"className":25024},[],[25026],{"type":32,"value":10038},{"type":32,"value":25028}," command. But instead of this, we will run our command by typing this to our terminal:",{"type":27,"tag":793,"props":25030,"children":25033},{"className":25031,"code":25032,"language":1084,"meta":5},[1082],"node cypress.js\n",[25034],{"type":27,"tag":653,"props":25035,"children":25036},{"__ignoreMap":5},[25037],{"type":32,"value":25032},{"type":27,"tag":28,"props":25039,"children":25040},{},[25041,25043,25048],{"type":32,"value":25042},"To make things easier for us, let’s add this to our ",{"type":27,"tag":653,"props":25044,"children":25046},{"className":25045},[],[25047],{"type":32,"value":734},{"type":32,"value":1440},{"type":27,"tag":793,"props":25050,"children":25053},{"className":25051,"code":25052,"filename":734,"language":1004,"meta":5},[1002],"{\n  \"scripts\": {\n    \"cy:run\": \"node cypress.js\"\n  }\n}\n",[25054],{"type":27,"tag":653,"props":25055,"children":25056},{"__ignoreMap":5},[25057],{"type":32,"value":25052},{"type":27,"tag":28,"props":25059,"children":25060},{},[25061,25062,25068,25070,25076],{"type":32,"value":11386},{"type":27,"tag":653,"props":25063,"children":25065},{"className":25064},[],[25066],{"type":32,"value":25067},"cypress.run()",{"type":32,"value":25069}," function can also take an options parameter. This way we can which tests should be run, similarly as we did with the ",{"type":27,"tag":653,"props":25071,"children":25073},{"className":25072},[],[25074],{"type":32,"value":25075},"--spec",{"type":32,"value":25077}," flag in our previous example. So let’s add options inside our function and specify a spec folder to run:",{"type":27,"tag":793,"props":25079,"children":25083},{"className":25080,"code":25081,"highlights":25082,"language":1513,"meta":5},[1510],"const cypress = require('cypress');\n\ncypress.run({\n  spec: './cypress/integration/api/*.ts',\n});\n",[3877],[25084],{"type":27,"tag":653,"props":25085,"children":25086},{"__ignoreMap":5},[25087],{"type":32,"value":25081},{"type":27,"tag":28,"props":25089,"children":25090},{},[25091],{"type":32,"value":25092},"This property can also be an array, so we can run more folders and specify which ones we want to run:",{"type":27,"tag":793,"props":25094,"children":25098},{"className":25095,"code":25096,"highlights":25097,"language":1513,"meta":5},[1510],"const cypress = require('cypress');\n\ncypress.run({\n  spec: ['./cypress/integration/api/*.ts', './cypress/integration/ui/*.ts'],\n});\n",[3877],[25099],{"type":27,"tag":653,"props":25100,"children":25101},{"__ignoreMap":5},[25102],{"type":32,"value":25096},{"type":27,"tag":28,"props":25104,"children":25105},{},[25106,25108,25113],{"type":32,"value":25107},"Now that we know all this, we can play inside our ",{"type":27,"tag":653,"props":25109,"children":25111},{"className":25110},[],[25112],{"type":32,"value":25005},{"type":32,"value":25114}," file and apply any kind of logic we like.",{"type":27,"tag":28,"props":25116,"children":25117},{},[25118,25120,25125,25126,25131,25133,25138,25139,25145,25146,25152,25153,25158,25159,25165,25167,25174,25176,25182,25184,25189,25191,25196],{"type":32,"value":25119},"Let’s say that instead of ",{"type":27,"tag":653,"props":25121,"children":25123},{"className":25122},[],[25124],{"type":32,"value":11456},{"type":32,"value":4164},{"type":27,"tag":653,"props":25127,"children":25129},{"className":25128},[],[25130],{"type":32,"value":24963},{"type":32,"value":25132}," folder, I have folders named: ",{"type":27,"tag":653,"props":25134,"children":25136},{"className":25135},[],[25137],{"type":32,"value":23911},{"type":32,"value":3372},{"type":27,"tag":653,"props":25140,"children":25142},{"className":25141},[],[25143],{"type":32,"value":25144},"detail",{"type":32,"value":3372},{"type":27,"tag":653,"props":25147,"children":25149},{"className":25148},[],[25150],{"type":32,"value":25151},"settings",{"type":32,"value":3372},{"type":27,"tag":653,"props":25154,"children":25156},{"className":25155},[],[25157],{"type":32,"value":4129},{"type":32,"value":4164},{"type":27,"tag":653,"props":25160,"children":25162},{"className":25161},[],[25163],{"type":32,"value":25164},"signup",{"type":32,"value":25166},". I want to be able to pick any number or combination of these, and at the same time be able to run all of them. To do this, we will add a module called ",{"type":27,"tag":172,"props":25168,"children":25171},{"href":25169,"rel":25170},"https://www.npmjs.com/package/yargs",[696],[25172],{"type":32,"value":25173},"yargs",{"type":32,"value":25175},". This package enables us to create and work with our own command line options. We are going to add a ",{"type":27,"tag":653,"props":25177,"children":25179},{"className":25178},[],[25180],{"type":32,"value":25181},"--grep",{"type":32,"value":25183}," option, so that if we just want to run tests inside ",{"type":27,"tag":653,"props":25185,"children":25187},{"className":25186},[],[25188],{"type":32,"value":25151},{"type":32,"value":25190}," and ",{"type":27,"tag":653,"props":25192,"children":25194},{"className":25193},[],[25195],{"type":32,"value":4129},{"type":32,"value":25197}," folders, we will call a script like this:",{"type":27,"tag":793,"props":25199,"children":25202},{"className":25200,"code":25201,"language":1084,"meta":5},[1082],"npm run cy:run -- --grep settings login\n",[25203],{"type":27,"tag":653,"props":25204,"children":25205},{"__ignoreMap":5},[25206],{"type":32,"value":25201},{"type":27,"tag":28,"props":25208,"children":25209},{},[25210,25212,25217,25219,25224],{"type":32,"value":25211},"To define our ",{"type":27,"tag":653,"props":25213,"children":25215},{"className":25214},[],[25216],{"type":32,"value":25181},{"type":32,"value":25218}," option, we will add following to our ",{"type":27,"tag":653,"props":25220,"children":25222},{"className":25221},[],[25223],{"type":32,"value":25005},{"type":32,"value":25225}," file:",{"type":27,"tag":793,"props":25227,"children":25231},{"className":25228,"code":25229,"filename":25005,"highlights":25230,"language":1513,"meta":5},[1510],"const yargs = require('yargs');\n\nconst { grep } = yargs\n  .option('grep', {\n    type: 'array'\n  }).argv;\n",[3667],[25232],{"type":27,"tag":653,"props":25233,"children":25234},{"__ignoreMap":5},[25235],{"type":32,"value":25229},{"type":27,"tag":28,"props":25237,"children":25238},{},[25239,25241,25246,25248,25254,25256,25263],{"type":32,"value":25240},"This will digest out ",{"type":27,"tag":653,"props":25242,"children":25244},{"className":25243},[],[25245],{"type":32,"value":25181},{"type":32,"value":25247}," flag. In order to give it multiple arguments, we need do specify the type of the input as highlighted on line 5. If you are unfamiliar with the ",{"type":27,"tag":653,"props":25249,"children":25251},{"className":25250},[],[25252],{"type":32,"value":25253},"{ grep }",{"type":32,"value":25255}," syntax, go and ",{"type":27,"tag":172,"props":25257,"children":25260},{"href":25258,"rel":25259},"https://filiphric.com/using-destructuring-in-cypress",[696],[25261],{"type":32,"value":25262},"check out my blog on destructuring",{"type":32,"value":25264},", where I explain this in more detail.",{"type":27,"tag":28,"props":25266,"children":25267},{},[25268,25270,25275],{"type":32,"value":25269},"Let’s finalize our script and pass these options to our ",{"type":27,"tag":653,"props":25271,"children":25273},{"className":25272},[],[25274],{"type":32,"value":25067},{"type":32,"value":19455},{"type":27,"tag":793,"props":25277,"children":25281},{"className":25278,"code":25279,"filename":25005,"highlights":25280,"language":1513,"meta":5},[1510],"const cypress = require('cypress');\nconst yargs = require('yargs');\n\nconst { grep } = yargs\n  .option('grep', {\n    type: 'array',\n    default: ['*']\n  }).argv;\n\ncypress.run({\n  spec: grep.map(folder => `./cypress/integration/${folder}/*.ts`),\n});\n\n",[3811],[25282],{"type":27,"tag":653,"props":25283,"children":25284},{"__ignoreMap":5},[25285],{"type":32,"value":25279},{"type":27,"tag":28,"props":25287,"children":25288},{},[25289,25291,25297,25299,25304],{"type":32,"value":25290},"On line 11, we are mapping out all the folder names, so that when we call ",{"type":27,"tag":653,"props":25292,"children":25294},{"className":25293},[],[25295],{"type":32,"value":25296},"npm run cy:run -- --grep settings login",{"type":32,"value":25298}," our ",{"type":27,"tag":653,"props":25300,"children":25302},{"className":25301},[],[25303],{"type":32,"value":21096},{"type":32,"value":25305}," variable will be assigned the value of:",{"type":27,"tag":793,"props":25307,"children":25311},{"className":25308,"code":25310,"language":32,"meta":5},[25309],"language-text","[\"settings\", \"login\"]\n",[25312],{"type":27,"tag":653,"props":25313,"children":25314},{"__ignoreMap":5},[25315],{"type":32,"value":25310},{"type":27,"tag":28,"props":25317,"children":25318},{},[25319],{"type":32,"value":25320},"and our spec attribute will have the value of:",{"type":27,"tag":793,"props":25322,"children":25325},{"className":25323,"code":25324,"language":32,"meta":5},[25309],"[\"./cypress/integration/settings/*.ts\", \"./cypress/integration/login/*.ts\"]\n",[25326],{"type":27,"tag":653,"props":25327,"children":25328},{"__ignoreMap":5},[25329],{"type":32,"value":25324},{"type":27,"tag":28,"props":25331,"children":25332},{},[25333,25335,25340],{"type":32,"value":25334},"This way we can either pass names of our folders to our ",{"type":27,"tag":653,"props":25336,"children":25338},{"className":25337},[],[25339],{"type":32,"value":25181},{"type":32,"value":25341}," argument, or we can omit the argument and run all of our tests.",{"type":27,"tag":28,"props":25343,"children":25344},{},[25345,25347,25352,25354,25360,25362,25367,25368,25374],{"type":32,"value":25346},"It’s all just JavaScript so we can apply any logic we want. Instead of ",{"type":27,"tag":653,"props":25348,"children":25350},{"className":25349},[],[25351],{"type":32,"value":25181},{"type":32,"value":25353}," we could maybe use ",{"type":27,"tag":653,"props":25355,"children":25357},{"className":25356},[],[25358],{"type":32,"value":25359},"--folder",{"type":32,"value":25361}," as the name of our parameter. We can go even further and create both ",{"type":27,"tag":653,"props":25363,"children":25365},{"className":25364},[],[25366],{"type":32,"value":25359},{"type":32,"value":4164},{"type":27,"tag":653,"props":25369,"children":25371},{"className":25370},[],[25372],{"type":32,"value":25373},"--testFile",{"type":32,"value":25375}," flags to make our pick even more specific.",{"type":27,"tag":28,"props":25377,"children":25378},{},[25379],{"type":32,"value":25380},"This has proven to be incredibly useful in my case. I can run just those tests I need to be ran instead of waiting for the whole test suite, but still maintain the option to run everything. Several CI providers enable you to run your pipeline on demand and specify a pipeline variable, which can be used exactly for setting up which tests you want to run.",{"type":27,"tag":28,"props":25382,"children":25383},{},[25384,25386,25392],{"type":32,"value":25385},"If you liked this article, be sure to subscribe down below. I write blogs like these every week and whenever I publish one, I send out an email, so you don’t miss it. You can also ",{"type":27,"tag":172,"props":25387,"children":25389},{"href":1893,"rel":25388},[696],[25390],{"type":32,"value":25391},"follow me on Twitter",{"type":32,"value":25393}," and reach out to me if you have any questions.",{"type":27,"tag":1029,"props":25395,"children":25396},{},[25397],{"type":27,"tag":28,"props":25398,"children":25399},{},[25400,25402,25409,25410,25417],{"type":32,"value":25401},"EDIT: When talking about selecting tests, I suggest you check out solution by ",{"type":27,"tag":172,"props":25403,"children":25406},{"href":25404,"rel":25405},"https://twitter.com/NetanelBasal",[696],[25407],{"type":32,"value":25408},"Netanel Basal",{"type":32,"value":12342},{"type":27,"tag":172,"props":25411,"children":25414},{"href":25412,"rel":25413},"https://github.com/NetanelBasal/cyrun",[696],[25415],{"type":32,"value":25416},"With his plugin",{"type":32,"value":25418},", you can select tests or folders to run.",{"title":5,"searchDepth":320,"depth":320,"links":25420},[],"content:test-grepping-in-cypress-using-module-api:index.md","test-grepping-in-cypress-using-module-api/index.md","test-grepping-in-cypress-using-module-api/index",{"_path":15387,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":25425,"description":25426,"slug":17916,"date":25427,"published":10,"tags":25428,"readingTime":25430,"body":25434,"_type":329,"_id":25687,"_source":331,"_file":25688,"_stem":25689,"_extension":334},"Using destructuring in Cypress","Short explanation on how to do destructuring in JavaScript and how you can use it to simplify your Cypress tests.","2020-11-02",[13407,2679,5279,25429],"beginner",{"text":585,"minutes":25431,"time":25432,"words":25433},3.28,196800,656,{"type":24,"children":25435,"toc":25685},[25436,25441,25446,25451,25461,25474,25494,25504,25523,25549,25559,25580,25589,25601,25612,25622,25650,25660,25680],{"type":27,"tag":28,"props":25437,"children":25438},{},[25439],{"type":32,"value":25440},"I often learn something new because I need to solve some kind of problem. Once that is solved, I move on to the next one. I spend a lot of time reading, thinking, trying stuff and trying to make sense of it all. One downside of my learning style is that sometimes I miss some basic stuff. This is a blog about one particular topic. Destructuring.",{"type":27,"tag":28,"props":25442,"children":25443},{},[25444],{"type":32,"value":25445},"I have seen destructuring many times when reading someone else’s code, and I had a hard time wrapping my head around it. Mostly because I had no use for it when writing tests in Cypress, which is something I do most of the time. If you are using Cypress as well and you are still learning JavaScript, this blog may be for you.",{"type":27,"tag":28,"props":25447,"children":25448},{},[25449],{"type":32,"value":25450},"Let’s say you have a simple object like this:",{"type":27,"tag":793,"props":25452,"children":25456},{"className":25453,"code":25454,"highlights":25455,"language":1513,"meta":5},[1510],"const car = {\n  color: 'red',\n  type: 'combi',\n  autopilot: false\n}\n\nconst color = car.color\n\nconsole.log(color) // 'red'\n\n",[3668],[25457],{"type":27,"tag":653,"props":25458,"children":25459},{"__ignoreMap":5},[25460],{"type":32,"value":25454},{"type":27,"tag":28,"props":25462,"children":25463},{},[25464,25466,25472],{"type":32,"value":25465},"In your code, you may want to use properties of this object. Let’s say you are going to use the color ",{"type":27,"tag":653,"props":25467,"children":25469},{"className":25468},[],[25470],{"type":32,"value":25471},"red",{"type":32,"value":25473}," a lot in your code. You might want to assign it to a separate variable like you can see on line 7.",{"type":27,"tag":28,"props":25475,"children":25476},{},[25477,25479,25485,25487,25492],{"type":32,"value":25478},"Whenever you now reference the ",{"type":27,"tag":653,"props":25480,"children":25482},{"className":25481},[],[25483],{"type":32,"value":25484},"color",{"type":32,"value":25486}," variable, it will return ",{"type":27,"tag":653,"props":25488,"children":25490},{"className":25489},[],[25491],{"type":32,"value":25471},{"type":32,"value":25493},". Notice that the name of our newly declared variable is the same as the name of the key of our object. To do the same thing, but using destructuring, you can do following:",{"type":27,"tag":793,"props":25495,"children":25499},{"className":25496,"code":25497,"highlights":25498,"language":1513,"meta":5},[1510],"const car = {\n  color: 'red',\n  type: 'combi',\n  autopilot: false\n}\n\nconst { color } = car\n\nconsole.log(color) // 'red'\n",[3668],[25500],{"type":27,"tag":653,"props":25501,"children":25502},{"__ignoreMap":5},[25503],{"type":32,"value":25497},{"type":27,"tag":28,"props":25505,"children":25506},{},[25507,25509,25514,25516,25521],{"type":32,"value":25508},"Notice how the result is exactly the same. We have created a variable with a name ",{"type":27,"tag":653,"props":25510,"children":25512},{"className":25511},[],[25513],{"type":32,"value":25484},{"type":32,"value":25515},", that will have a value of ",{"type":27,"tag":653,"props":25517,"children":25519},{"className":25518},[],[25520],{"type":32,"value":25471},{"type":32,"value":25522}," when we call it.",{"type":27,"tag":28,"props":25524,"children":25525},{},[25526,25528,25533,25535,25540,25541,25547],{"type":32,"value":25527},"Now let’s say we want to create a function, that will take a single argument. Inside this function, we have a simple ",{"type":27,"tag":653,"props":25529,"children":25531},{"className":25530},[],[25532],{"type":32,"value":23753},{"type":32,"value":25534},", that will output ",{"type":27,"tag":653,"props":25536,"children":25538},{"className":25537},[],[25539],{"type":32,"value":25484},{"type":32,"value":4164},{"type":27,"tag":653,"props":25542,"children":25544},{"className":25543},[],[25545],{"type":32,"value":25546},"type",{"type":32,"value":25548}," attributes of a given object. The function looks like this:",{"type":27,"tag":793,"props":25550,"children":25554},{"className":25551,"code":25552,"highlights":25553,"language":1513,"meta":5},[1510],"const func = (param) => {\n  console.log(param.color, param.type)\n}\n\nfunc(car) // red combi\n",[3667],[25555],{"type":27,"tag":653,"props":25556,"children":25557},{"__ignoreMap":5},[25558],{"type":32,"value":25552},{"type":27,"tag":28,"props":25560,"children":25561},{},[25562,25564,25570,25572,25578],{"type":32,"value":25563},"We’ll pass our ",{"type":27,"tag":653,"props":25565,"children":25567},{"className":25566},[],[25568],{"type":32,"value":25569},"car",{"type":32,"value":25571}," object from before to this function, and you can notice the result on line 5. Notice how we create a ",{"type":27,"tag":653,"props":25573,"children":25575},{"className":25574},[],[25576],{"type":32,"value":25577},"param",{"type":32,"value":25579}," for our function, which is later used inside the body of the function. Now that we know how destructuring works for objects, we can use it and refactor our function like this:",{"type":27,"tag":793,"props":25581,"children":25584},{"className":25582,"code":25583,"language":1513,"meta":5},[1510],"const func = ({ color, type }) => {\n  console.log(color, type)\n}\n\nfunc(car) // red combi\n",[25585],{"type":27,"tag":653,"props":25586,"children":25587},{"__ignoreMap":5},[25588],{"type":32,"value":25583},{"type":27,"tag":28,"props":25590,"children":25591},{},[25592,25594,25599],{"type":32,"value":25593},"We did the exact thing as in our first example, but this time we used destructuring inside our function parameter. Go back and look at how similar these examples are. The cool thing about using destructuring like this is that we can avoid creating a generic parameter like ",{"type":27,"tag":653,"props":25595,"children":25597},{"className":25596},[],[25598],{"type":32,"value":25577},{"type":32,"value":25600}," in our function.",{"type":27,"tag":28,"props":25602,"children":25603},{},[25604,25606,25611],{"type":32,"value":25605},"The most common use for destructuring in Cypress tests is for situations where I’m passing a yielded object to a ",{"type":27,"tag":653,"props":25607,"children":25609},{"className":25608},[],[25610],{"type":32,"value":13338},{"type":32,"value":19455},{"type":27,"tag":793,"props":25613,"children":25617},{"className":25614,"code":25615,"highlights":25616,"language":1513,"meta":5},[1510],"it('creates a todo', () => {\n\n  cy.intercept('POST', '/todos').as('createTodo')\n  cy.visit('/')\n  cy.addTodo('buy milk') // create a todo via UI using custom command\n  cy.wait('@createTodo').then( todos => {\n      expect(todos.response.statusCode).to.eq(201)\n      expect(todos.response.body).to.deep.eq({ title: 'buy milk' })\n    })\n\n})\n",[3809,3668,3723,3746],[25618],{"type":27,"tag":653,"props":25619,"children":25620},{"__ignoreMap":5},[25621],{"type":32,"value":25615},{"type":27,"tag":28,"props":25623,"children":25624},{},[25625,25627,25633,25635,25641,25643,25648],{"type":32,"value":25626},"Notice how I create a ",{"type":27,"tag":653,"props":25628,"children":25630},{"className":25629},[],[25631],{"type":32,"value":25632},"todos",{"type":32,"value":25634}," parameter, similar to our ",{"type":27,"tag":653,"props":25636,"children":25638},{"className":25637},[],[25639],{"type":32,"value":25640},"params",{"type":32,"value":25642}," from previous example. I use that to make my assertions inside ",{"type":27,"tag":653,"props":25644,"children":25646},{"className":25645},[],[25647],{"type":32,"value":13338},{"type":32,"value":25649}," command. Instead of doing this, I can refactor this code using destructuring:",{"type":27,"tag":793,"props":25651,"children":25655},{"className":25652,"code":25653,"highlights":25654,"language":1513,"meta":5},[1510],"it('creates a todo', () => {\n\n  cy.intercept('POST', '/todos').as('createTodo')\n  cy.visit('/')\n  cy.addTodo('buy milk') // create a todo via UI using custom command\n  cy.wait('@createTodo').then( ({ response }) => {\n      expect(response?.statusCode).to.eq(201)\n      expect(response?.body).to.deep.eq({ title: 'buy milk' })\n    })\n\n})\n",[3809,3668,3723,3746],[25656],{"type":27,"tag":653,"props":25657,"children":25658},{"__ignoreMap":5},[25659],{"type":32,"value":25653},{"type":27,"tag":28,"props":25661,"children":25662},{},[25663,25665,25670,25672,25678],{"type":32,"value":25664},"I think this is pretty neat. I don’t use extra parameters in my code and I can clearly see what I am testing inside my ",{"type":27,"tag":653,"props":25666,"children":25668},{"className":25667},[],[25669],{"type":32,"value":13338},{"type":32,"value":25671}," command. I bet there are tons of great use cases for destructuring in Cypress that I’m still about to discover. If you know of any, don’t forget to ",{"type":27,"tag":172,"props":25673,"children":25675},{"href":1893,"rel":25674},[696],[25676],{"type":32,"value":25677},"let me know on Twitter",{"type":32,"value":25679},", I’d love for you to share your knowledge with me.",{"type":27,"tag":28,"props":25681,"children":25682},{},[25683],{"type":32,"value":25684},"Hope this helps. Check out my other blogs and if you like these, feel free to subscribe. You’ll get notified whenever I publish another blog.",{"title":5,"searchDepth":320,"depth":320,"links":25686},[],"content:using-destructuring-in-cypress:index.md","using-destructuring-in-cypress/index.md","using-destructuring-in-cypress/index",{"_path":10412,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":25691,"description":25692,"slug":25693,"date":25694,"tags":25695,"published":10,"readingTime":25697,"body":25701,"_type":329,"_id":25926,"_source":331,"_file":25927,"_stem":25928,"_extension":334},"Cypress basics: Where did my cookies disappear?","Cypress clears the browser state before each of your tests. This means that sometimes you might need to think about how you need to handle cookies.","cypress-basics-where-did-my-cookies-disappear","2020-10-27",[5279,25696,13407,25429],"cookies",{"text":585,"minutes":25698,"time":25699,"words":25700},3.395,203700,679,{"type":24,"children":25702,"toc":25924},[25703,25713,25718,25738,25752,25760,25773,25783,25793,25804,25815,25844,25855,25867,25902,25912],{"type":27,"tag":1029,"props":25704,"children":25705},{},[25706,25710],{"type":27,"tag":28,"props":25707,"children":25708},{},[25709],{"type":32,"value":5973},{"type":27,"tag":5975,"props":25711,"children":25712},{},[],{"type":27,"tag":28,"props":25714,"children":25715},{},[25716],{"type":32,"value":25717},"You successfully logged into your application. You’ve got your first test! Now to the next one. Click! And now you are logged out. Why is Cypress logging you out after each test?!",{"type":27,"tag":28,"props":25719,"children":25720},{},[25721,25723,25729,25731,25736],{"type":32,"value":25722},"The reason is actually simple. ",{"type":27,"tag":172,"props":25724,"children":25727},{"href":25725,"rel":25726},"https://docs.cypress.io/api/commands/clearcookies.html#Syntax",[696],[25728],{"type":32,"value":20828},{"type":32,"value":25730}," in between tests. All your cookies are deleted from your app between each ",{"type":27,"tag":653,"props":25732,"children":25734},{"className":25733},[],[25735],{"type":32,"value":7187},{"type":32,"value":25737}," block. The reason why you are now logged out may be because your app uses a cookie to store authentication token. When your app makes an http request to your server, that cookie will be sent along with it. This way your server knows that you your authentication is valid and that you have the rights to read or write data. If the cookie is not present, server evaluates this as an unauthorized request and your app will typically log you out.",{"type":27,"tag":28,"props":25739,"children":25740},{},[25741,25743,25750],{"type":32,"value":25742},"Clearing the state of the browser is actually a good thing, but you might be in a situation where you want to work around this. E.g. you want to group multiple tests in one spec, where each test requires you to be logged in. Let’s look into what are the options. As is often the case with my blogs, ",{"type":27,"tag":172,"props":25744,"children":25747},{"href":25745,"rel":25746},"https://github.com/filiphric/cypress-cookies",[696],[25748],{"type":32,"value":25749},"I have a repo set up, so make sure you clone it",{"type":32,"value":25751}," and play with the code yourself. In our repo, we have a simple app, that lists all of the cookies that are present.",{"type":27,"tag":28,"props":25753,"children":25754},{},[25755],{"type":27,"tag":959,"props":25756,"children":25759},{"alt":25757,"src":25758},"Cookies application we will be testing with Cypress","cypress-cookie-app.png",[],{"type":27,"tag":28,"props":25761,"children":25762},{},[25763,25765,25771],{"type":32,"value":25764},"In our first piece of code you can see that we are setting a cookie to our app using ",{"type":27,"tag":653,"props":25766,"children":25768},{"className":25767},[],[25769],{"type":32,"value":25770},".setCookie()",{"type":32,"value":25772}," command, but in our second test this cookie is not present anymore.",{"type":27,"tag":793,"props":25774,"children":25778},{"className":25775,"code":25776,"filename":25777,"language":1513,"meta":5},[1510],"it('should show cookie', () => {\n  cy.setCookie('authentication', 'top_secret');\n  cy.visit('../../app/index.html');\n});\n\nit('opens a page', () => {\n  cy.visit('../../app/index.html');\n});\n","/cypress/integration/twoCookieTests.ts",[25779],{"type":27,"tag":653,"props":25780,"children":25781},{"__ignoreMap":5},[25782],{"type":32,"value":25776},{"type":27,"tag":28,"props":25784,"children":25785},{},[25786,25788],{"type":32,"value":25787},"Once you’ll run this spec, you’ll see that our page shows \"no cookies were found\" on our second spec. See video from the test run:\n",{"type":27,"tag":959,"props":25789,"children":25792},{"alt":25790,"src":25791},"Cookies deleted in between tests","two-cookie-tests.mp4",[],{"type":27,"tag":28,"props":25794,"children":25795},{},[25796,25798,25803],{"type":32,"value":25797},"To make sure that our cookies are set in each test, you can set up your cookies before each test using ",{"type":27,"tag":653,"props":25799,"children":25801},{"className":25800},[],[25802],{"type":32,"value":20753},{"type":32,"value":8442},{"type":27,"tag":793,"props":25805,"children":25810},{"className":25806,"code":25807,"filename":25808,"highlights":25809,"language":1513,"meta":5},[1510],"beforeEach(() => {\n  cy.setCookie('authentication', 'top_secret');\n});\n\nit('first test', () => {\n  cy.visit('../../app/index.html');\n});\n\nit('second test', () => {\n  cy.visit('../../app/index.html');\n});\n","/cypress/integration/beforeEach.ts",[22420,320,1606],[25811],{"type":27,"tag":653,"props":25812,"children":25813},{"__ignoreMap":5},[25814],{"type":32,"value":25807},{"type":27,"tag":28,"props":25816,"children":25817},{},[25818,25820,25825,25827,25834,25836,25842],{"type":32,"value":25819},"This however may get a little annoying, so instead of using ",{"type":27,"tag":653,"props":25821,"children":25823},{"className":25822},[],[25824],{"type":32,"value":20753},{"type":32,"value":25826}," hook we can use another approach. Using ",{"type":27,"tag":172,"props":25828,"children":25831},{"href":25829,"rel":25830},"https://docs.cypress.io/api/cypress-api/cookies.html#Defaults",[696],[25832],{"type":32,"value":25833},"Cypress’ Cookies API",{"type":32,"value":25835}," we can set up cookies that we never want to delete. With ",{"type":27,"tag":653,"props":25837,"children":25839},{"className":25838},[],[25840],{"type":32,"value":25841},"Cypress.Cookies.defaults",{"type":32,"value":25843}," we can define which cookies we want to prevent from being cleared before each test:",{"type":27,"tag":793,"props":25845,"children":25850},{"className":25846,"code":25847,"filename":25848,"highlights":25849,"language":1513,"meta":5},[1510],"Cypress.Cookies.defaults({\n  preserve: 'authentication'\n})\n\nit('first test', () => {\n  cy.setCookie('authentication', 'top_secret');\n  cy.visit('../../app/index.html');\n});\n\nit('second test', () => {\n  cy.visit('../../app/index.html');\n});\n\n","/cypress/integration/cypressApi.ts",[22420,320,1606],[25851],{"type":27,"tag":653,"props":25852,"children":25853},{"__ignoreMap":5},[25854],{"type":32,"value":25847},{"type":27,"tag":28,"props":25856,"children":25857},{},[25858,25860,25865],{"type":32,"value":25859},"Instead of using the api within out spec, I’d recommend declaring this in ",{"type":27,"tag":653,"props":25861,"children":25863},{"className":25862},[],[25864],{"type":32,"value":17310},{"type":32,"value":25866}," file in Cypress project. This way you’ll make sure that your cookie is preserved throughout all of your tests. But maybe you don’t want to do that. Instead of keeping the cookie for the whole test suite, you might just want to preserve it for a single spec file.",{"type":27,"tag":28,"props":25868,"children":25869},{},[25870,25872,25878,25880,25885,25887,25892,25894,25900],{"type":32,"value":25871},"For this, you can use Cookies API too. Using ",{"type":27,"tag":653,"props":25873,"children":25875},{"className":25874},[],[25876],{"type":32,"value":25877},"Cypress.Cookies.preserveOnce",{"type":32,"value":25879}," will enable you to keep your cookies for your spec. In our following test, we are using ",{"type":27,"tag":653,"props":25881,"children":25883},{"className":25882},[],[25884],{"type":32,"value":8434},{"type":32,"value":25886}," hook to set up our cookie and then ",{"type":27,"tag":653,"props":25888,"children":25890},{"className":25889},[],[25891],{"type":32,"value":7243},{"type":32,"value":25893}," hook to call our ",{"type":27,"tag":653,"props":25895,"children":25897},{"className":25896},[],[25898],{"type":32,"value":25899},"preserveOnce",{"type":32,"value":25901}," function to keep that cookie present for each test:",{"type":27,"tag":793,"props":25903,"children":25907},{"className":25904,"code":25905,"highlights":25906,"language":1513,"meta":5},[1510],"before(() => {\n  cy.setCookie('authentication', 'top_secret');\n});\n\nbeforeEach(() => {\n  Cypress.Cookies.preserveOnce('authentication')\n});\n\nit('first test', () => {\n  cy.visit('../../app/index.html');\n});\n\nit('second test', () => {\n  cy.visit('../../app/index.html');\n});\n",[3667,3809,3668],[25908],{"type":27,"tag":653,"props":25909,"children":25910},{"__ignoreMap":5},[25911],{"type":32,"value":25905},{"type":27,"tag":28,"props":25913,"children":25914},{},[25915,25917,25922],{"type":32,"value":25916},"This may not seem too different from just using ",{"type":27,"tag":653,"props":25918,"children":25920},{"className":25919},[],[25921],{"type":32,"value":7243},{"type":32,"value":25923}," to add our cookies before tests. However, this example is quite oversimplified. Most of the time the case is not just setting a cookie inside our browser, but actually logging in with a custom command or via API. This may be a more expensive operation, and avoiding repetition might save us some time. I’ll talk about different ways of authenticating to our app in some future blog, so make sure you subscribe to the newsletter to be notified when that comes out.",{"title":5,"searchDepth":320,"depth":320,"links":25925},[],"content:cypress-basics-where-did-my-cookies-disappear:index.md","cypress-basics-where-did-my-cookies-disappear/index.md","cypress-basics-where-did-my-cookies-disappear/index",{"_path":25930,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":25931,"description":25932,"slug":25933,"date":25934,"tags":25935,"published":10,"readingTime":25940,"body":25944,"_type":329,"_id":26223,"_source":331,"_file":26224,"_stem":26225,"_extension":334},"/testing-websocket-application-with-cypress","Testing a websocket application with Cypress","Learn how to test websocket-enabled applications using Cypress.io, including handling real-time updates and verifying socket messages.","testing-websocket-application-with-cypress","2020-10-19",[25936,25937,25938,5279,25939],"test","websockets","socket.io","cypress.io",{"text":4032,"minutes":25941,"time":25942,"words":25943},5.075,304500,1015,{"type":24,"children":25945,"toc":26219},[25946,25958,25966,25971,25979,25984,25990,26004,26009,26032,26037,26046,26051,26060,26080,26086,26098,26118,26128,26147,26156,26179,26189,26194,26214],{"type":27,"tag":28,"props":25947,"children":25948},{},[25949,25951,25956],{"type":32,"value":25950},"Websockets enable you to have an uninterrupted communication with your server. Typically, you can see websockets in action when using a chat app. Without needing to refresh, you can see your friend’s messages arrive. Websocket connection is typically created when you open your application. In my ",{"type":27,"tag":172,"props":25952,"children":25954},{"href":19041,"rel":25953},[696],[25955],{"type":32,"value":20078},{"type":32,"value":25957},", you can see a websocket connection being created upon refreshing our application:",{"type":27,"tag":28,"props":25959,"children":25960},{},[25961],{"type":27,"tag":959,"props":25962,"children":25965},{"alt":25963,"src":25964},"Websockets shown in devtools","websockets.mp4",[],{"type":27,"tag":28,"props":25967,"children":25968},{},[25969],{"type":32,"value":25970},"As you can see, we can access our websockets in the Chrome DevTools network panel. Websockets are sort of permanent connection between client and server. Communication happens through websocket messages. These messages can be both sent and received. To observe these messages, you can again look into Chrome DevTools. In our example, there are two windows open. See how a message appears in the websocket detail panel when we create a new board in a second window:",{"type":27,"tag":28,"props":25972,"children":25973},{},[25974],{"type":27,"tag":959,"props":25975,"children":25978},{"alt":25976,"src":25977},"Websocket message appears on board creation","websocket_message.mp4",[],{"type":27,"tag":28,"props":25980,"children":25981},{},[25982],{"type":32,"value":25983},"The websocket communication can go two ways, sort of like a chat would. Websocket messages can either be sent to the server, or they can be received from the server. Our Trello application only does the latter. There are no websockets being sent, only received. When creating a new board, an http request is made, and server then emits a websocket message to all opened clients (apps). That means that all instances of our Trello application will receive the websocket message, digest it and change state of our application. In our case, you can see that our newly created board appears in second window, without needing to refresh the application.",{"type":27,"tag":45,"props":25985,"children":25987},{"id":25986},"lets-test-websocket-behavior",[25988],{"type":32,"value":25989},"Let’s test websocket behavior",{"type":27,"tag":28,"props":25991,"children":25992},{},[25993,25995,26002],{"type":32,"value":25994},"Let’s say we want to write a test for what we just saw in the animation. We want to create a test that checks that a websocket message that arrives when a new board is created in other window. In Cypress, it is not possible to open a second tab or a window for that. (",{"type":27,"tag":172,"props":25996,"children":25999},{"href":25997,"rel":25998},"https://filiphric.com/opening-a-new-tab-in-cypress",[696],[26000],{"type":32,"value":26001},"I recommend checking out my blog on what you can do when dealing with tabs",{"type":32,"value":26003},"). The fact of the matter is, that you don’t need a second tab. Your app is not aware of other browser window being opened, so you can test your app effectively without trying to do so.",{"type":27,"tag":28,"props":26005,"children":26006},{},[26007],{"type":32,"value":26008},"Instead, let’s look into more detail of what happens when a board is created in another window:",{"type":27,"tag":851,"props":26010,"children":26011},{},[26012,26017,26022,26027],{"type":27,"tag":109,"props":26013,"children":26014},{},[26015],{"type":32,"value":26016},"User clicks on a button to create a new board",{"type":27,"tag":109,"props":26018,"children":26019},{},[26020],{"type":32,"value":26021},"Fills an input field with the name of the board",{"type":27,"tag":109,"props":26023,"children":26024},{},[26025],{"type":32,"value":26026},"Our app takes that input and sends it in the body of our http request",{"type":27,"tag":109,"props":26028,"children":26029},{},[26030],{"type":32,"value":26031},"Our app takes the user to the board",{"type":27,"tag":28,"props":26033,"children":26034},{},[26035],{"type":32,"value":26036},"In our first window, we can see the new board appearing at step #3. So it seems we can really just replicate what happens in this step. A simple request will do exactly the same thing our app in first window expects.",{"type":27,"tag":793,"props":26038,"children":26041},{"className":26039,"code":26040,"language":1513,"meta":5},[1510],"cy\n  .visit('/')\n\ncy\n  .request('POST', '/api/boards', { name: 'new board' })\n",[26042],{"type":27,"tag":653,"props":26043,"children":26044},{"__ignoreMap":5},[26045],{"type":32,"value":26040},{"type":27,"tag":28,"props":26047,"children":26048},{},[26049],{"type":32,"value":26050},"When this test runs, the exact same thing as in our previous gif happens. New board magically appears in our board list. Arguably, to test if our websockets work, we can just check that our application renders our new board:",{"type":27,"tag":793,"props":26052,"children":26055},{"className":26053,"code":26054,"language":1513,"meta":5},[1510],"cy\n  .get('.board_item')\n  .should('be.visible')\n  .and('have.length', 1);\n",[26056],{"type":27,"tag":653,"props":26057,"children":26058},{"__ignoreMap":5},[26059],{"type":32,"value":26054},{"type":27,"tag":28,"props":26061,"children":26062},{},[26063,26065,26071,26073,26078],{"type":32,"value":26064},"This will provide a good insight into correct functioning of our websockets. However, upon examining our websocket message we can see that the name of our new board is not the only thing that is sent. There are couple of things that may be hard to check via UI, like ",{"type":27,"tag":653,"props":26066,"children":26068},{"className":26067},[],[26069],{"type":32,"value":26070},"boardId",{"type":32,"value":26072}," or id of the user that created the board. ",{"type":27,"tag":653,"props":26074,"children":26076},{"className":26075},[],[26077],{"type":32,"value":26070},{"type":32,"value":26079}," is actually very important for our app, since it is used for redirecting user after clicking on the board.",{"type":27,"tag":45,"props":26081,"children":26083},{"id":26082},"diving-deeper",[26084],{"type":32,"value":26085},"Diving deeper",{"type":27,"tag":28,"props":26087,"children":26088},{},[26089,26091,26096],{"type":32,"value":26090},"As of Cypress v5.4.0, there is no way we can spy on incoming/outgoing websocket in similar fashion as for xhr, fetch requests or static assets (I write about routing ",{"type":27,"tag":172,"props":26092,"children":26093},{"href":23105},[26094],{"type":32,"value":26095},"fetch requests and static assets here",{"type":32,"value":26097},"). What we can do however, is leveraging the fact our tests run in the same context as our app. We can look inside our application and look into whether our application actually digests our websocket message properly.",{"type":27,"tag":28,"props":26099,"children":26100},{},[26101,26103,26109,26111,26116],{"type":32,"value":26102},"Our application is written in Vue and if you have ever written an application in Vue, you may have used ",{"type":27,"tag":172,"props":26104,"children":26107},{"href":26105,"rel":26106},"https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd?hl=en",[696],[26108],{"type":32,"value":5370},{"type":32,"value":26110},". These provide insight into components, application store and so much more. Using Vue.js DevTools you can see how our app adds our newly created board into store once websocket message arrives.\n",{"type":27,"tag":959,"props":26112,"children":26115},{"alt":26113,"src":26114},"vue.js devtools","vuejs_devtools.mp4",[],{"type":32,"value":26117},"\nHow cool would it be if we could test against these DevTools? Well, you kind of can, although the magic does not happen inside the extension, but in your browser console. We can expose our Vue app to the context of our window and have direct access into our app from within tests. Notice how we expose it only from within context of Cypress window.",{"type":27,"tag":793,"props":26119,"children":26123},{"className":26120,"code":26121,"highlights":26122,"language":1513,"meta":5},[1510],"const app = new Vue({\n  // ...\n}).$mount('#trello-app');\n\nif (window.Cypress) {\n  window.app = app;\n}\n",[3667,3809,3668],[26124],{"type":27,"tag":653,"props":26125,"children":26126},{"__ignoreMap":5},[26127],{"type":32,"value":26121},{"type":27,"tag":28,"props":26129,"children":26130},{},[26131,26133,26139,26141,26146],{"type":32,"value":26132},"Once we have added this code into our app, we can access our app via ",{"type":27,"tag":653,"props":26134,"children":26136},{"className":26135},[],[26137],{"type":32,"value":26138},".window()",{"type":32,"value":26140}," command in Cypress. To see it in our console, we can just use ",{"type":27,"tag":653,"props":26142,"children":26144},{"className":26143},[],[26145],{"type":32,"value":5487},{"type":32,"value":14476},{"type":27,"tag":793,"props":26148,"children":26151},{"className":26149,"code":26150,"language":1513,"meta":5},[1510],"cy\n  .window()\n  .then(({ app }) => {\n    console.log(app);\n  });\n",[26152],{"type":27,"tag":653,"props":26153,"children":26154},{"__ignoreMap":5},[26155],{"type":32,"value":26150},{"type":27,"tag":28,"props":26157,"children":26158},{},[26159,26164,26166,26171,26172,26177],{"type":27,"tag":959,"props":26160,"children":26163},{"alt":26161,"src":26162},"Vue app exposed in console","vue.mp4",[],{"type":32,"value":26165},"\nThis is really cool way we can look into our application state and observe whether it reacts accordingly to our websocket message. This way we can go one level deeper and check for various attributes that are received via websocket message. Notice how we use ",{"type":27,"tag":653,"props":26167,"children":26169},{"className":26168},[],[26170],{"type":32,"value":12439},{"type":32,"value":7446},{"type":27,"tag":653,"props":26173,"children":26175},{"className":26174},[],[26176],{"type":32,"value":13338},{"type":32,"value":26178}," command on line 3. This applies Cypress’ retry logic to our assertion, so that we can account for delay between our request and actual arrival of our websocket message.",{"type":27,"tag":793,"props":26180,"children":26184},{"className":26181,"code":26182,"highlights":26183,"language":1513,"meta":5},[1510],"cy\n  .window()\n  .should(({ app }) => {\n    // find our component by component name\n    const boardCollection = app.$children.find(e => e.$options.name === 'board-collection');\n    expect(boardCollection.boards).to.have.length(1);\n    expect(boardCollection.boards[0].id).to.exist;\n    expect(boardCollection.boards[0].starred).to.be.false;\n    expect(boardCollection.boards[0].user).to.eq(0);\n  });\n\n",[1606],[26185],{"type":27,"tag":653,"props":26186,"children":26187},{"__ignoreMap":5},[26188],{"type":32,"value":26182},{"type":27,"tag":28,"props":26190,"children":26191},{},[26192],{"type":32,"value":26193},"The cool part here, is that we can actually test parts of our app that we are not able to see. That way we have not only tested that our websockets have actually arrived, but more importantly, that these websocket messages were properly handled.",{"type":27,"tag":28,"props":26195,"children":26196},{},[26197,26199,26204,26206,26212],{"type":32,"value":26198},"I strongly encourage you to try this on your own. You can use my ",{"type":27,"tag":172,"props":26200,"children":26202},{"href":19041,"rel":26201},[696],[26203],{"type":32,"value":20078},{"type":32,"value":26205},", or on some other. While you are out there checking out apps, I suggest looking into ",{"type":27,"tag":172,"props":26207,"children":26209},{"href":14280,"rel":26208},[696],[26210],{"type":32,"value":26211},"Real World app by Cypress",{"type":32,"value":26213},". It has tons of cool examples.",{"type":27,"tag":28,"props":26215,"children":26216},{},[26217],{"type":32,"value":26218},"Don’t forget to share this blog with a friend.",{"title":5,"searchDepth":320,"depth":320,"links":26220},[26221,26222],{"id":25986,"depth":320,"text":25989},{"id":26082,"depth":320,"text":26085},"content:testing-websocket-application-with-cypress:index.md","testing-websocket-application-with-cypress/index.md","testing-websocket-application-with-cypress/index",{"_path":17738,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":26227,"description":26228,"slug":26229,"date":26230,"tags":26231,"published":10,"readingTime":26233,"body":26237,"_type":329,"_id":26492,"_source":331,"_file":26493,"_stem":26494,"_extension":334},"Opening a new tab in Cypress","Spoiler alert: you don’t. But there are tons of things you can do to test your link redirects properly.","opening-a-new-tab-in-cypress","2020-10-08",[26232,5279],"tabs",{"text":585,"minutes":26234,"time":26235,"words":26236},3.16,189600,632,{"type":24,"children":26238,"toc":26487},[26239,26253,26259,26264,26273,26300,26309,26329,26335,26363,26372,26384,26394,26420,26426,26444,26453],{"type":27,"tag":28,"props":26240,"children":26241},{},[26242,26244,26251],{"type":32,"value":26243},"Cypress has its ",{"type":27,"tag":172,"props":26245,"children":26248},{"href":26246,"rel":26247},"https://docs.cypress.io/guides/references/trade-offs.html#Multiple-tabs",[696],[26249],{"type":32,"value":26250},"trade-offs",{"type":32,"value":26252},". Lack of multiple tabs support may be annoying, especially when you are starting to test an application that opens stuff in new tabs all the time. In this article, I would like to show you how I work around this limitation. Although, is it a limitation? Let’s look at the solutions and you’ll see for yourself.",{"type":27,"tag":45,"props":26254,"children":26256},{"id":26255},"removing-target_blank-attribute",[26257],{"type":32,"value":26258},"Removing target=\"_blank\" attribute",{"type":27,"tag":28,"props":26260,"children":26261},{},[26262],{"type":32,"value":26263},"Let’s say you have a following anchor link in your application:",{"type":27,"tag":793,"props":26265,"children":26268},{"className":26266,"code":26267,"language":7826,"meta":5},[7824],"\u003Ca href=\"/about.html\" target=\"_blank\">Click me!\u003C/a>\n",[26269],{"type":27,"tag":653,"props":26270,"children":26271},{"__ignoreMap":5},[26272],{"type":32,"value":26267},{"type":27,"tag":28,"props":26274,"children":26275},{},[26276,26278,26284,26286,26291,26293,26298],{"type":32,"value":26277},"User clicking on this link will be taken to a new tab. That ",{"type":27,"tag":653,"props":26279,"children":26281},{"className":26280},[],[26282],{"type":32,"value":26283},"target=\"_blank\"",{"type":32,"value":26285}," attribute will tell the browser to open a new tab and visit the anchored location. With Cypress, simply using ",{"type":27,"tag":653,"props":26287,"children":26289},{"className":26288},[],[26290],{"type":32,"value":12806},{"type":32,"value":26292}," command would result in the same behavior. This of course means that as we leave our current tab, we are leaving our Cypress script as well. Our test will not continue in our new tab. The solution here would be removing our ",{"type":27,"tag":653,"props":26294,"children":26296},{"className":26295},[],[26297],{"type":32,"value":26283},{"type":32,"value":26299}," attribute for the test purposes. Since Cypress runs right inside browser, we have access to our DOM. This enables us to manipulate it using following code:",{"type":27,"tag":793,"props":26301,"children":26304},{"className":26302,"code":26303,"language":1513,"meta":5},[1510],"cy\n  .get('a')\n  .invoke('removeAttr', 'target')\n",[26305],{"type":27,"tag":653,"props":26306,"children":26307},{"__ignoreMap":5},[26308],{"type":32,"value":26303},{"type":27,"tag":28,"props":26310,"children":26311},{},[26312,26314,26319,26321,26327],{"type":32,"value":26313},"This is basically the same thing as if you would open developer tools, opened elements tab and edited it. However, we are now changing the behavior of our application. This may not be a good solution for you if you may want to make sure that the anchor has the proper attributes. Our ",{"type":27,"tag":653,"props":26315,"children":26317},{"className":26316},[],[26318],{"type":32,"value":18537},{"type":32,"value":26320}," command passes even if there is no ",{"type":27,"tag":653,"props":26322,"children":26324},{"className":26323},[],[26325],{"type":32,"value":26326},"target",{"type":32,"value":26328}," attribute present. Let’s look into what more we can do with our link.",{"type":27,"tag":45,"props":26330,"children":26332},{"id":26331},"testing-link-attributes",[26333],{"type":32,"value":26334},"Testing link attributes",{"type":27,"tag":28,"props":26336,"children":26337},{},[26338,26340,26345,26347,26353,26355,26362],{"type":32,"value":26339},"Instead of manipulating our element, we may want to choose not to directly click the element but test its attributes. Let’s now check that our link points to the right location and it actually has our ",{"type":27,"tag":653,"props":26341,"children":26343},{"className":26342},[],[26344],{"type":32,"value":26283},{"type":32,"value":26346}," attribute too. While you’re at it, check for ",{"type":27,"tag":653,"props":26348,"children":26350},{"className":26349},[],[26351],{"type":32,"value":26352},"rel=\"noopener noreferrer\"",{"type":32,"value":26354}," attributes so that you are check that your page has no ",{"type":27,"tag":172,"props":26356,"children":26359},{"href":26357,"rel":26358},"https://blog.bolajiayodeji.com/the-security-vulnerabilities-of-the-target_blank-attribute",[696],[26360],{"type":32,"value":26361},"security vulnerabilities",{"type":32,"value":256},{"type":27,"tag":793,"props":26364,"children":26367},{"className":26365,"code":26366,"language":1513,"meta":5},[1510],"cy\n  .get('a')\n  .should('have.attr', 'href', '/about')\n  .should('have.attr', 'target', '_blank')\n  .should('have.attr', 'rel', 'noopener noreferrer');\n",[26368],{"type":27,"tag":653,"props":26369,"children":26370},{"__ignoreMap":5},[26371],{"type":32,"value":26366},{"type":27,"tag":28,"props":26373,"children":26374},{},[26375,26377,26382],{"type":32,"value":26376},"This checks the existence of our link, but it does not check if the link is actually live. To check that, we can use ",{"type":27,"tag":653,"props":26378,"children":26380},{"className":26379},[],[26381],{"type":32,"value":12824},{"type":32,"value":26383}," command, and just call our link as an http request.",{"type":27,"tag":793,"props":26385,"children":26389},{"className":26386,"code":26387,"highlights":26388,"language":1513,"meta":5},[1510],"cy\n  .get('a')\n  .then(link => {\n\n    cy\n      .request(link.prop('href'))\n      .its('status')\n      .should('eq', 200);\n\n  });\n",[3809],[26390],{"type":27,"tag":653,"props":26391,"children":26392},{"__ignoreMap":5},[26393],{"type":32,"value":26387},{"type":27,"tag":28,"props":26395,"children":26396},{},[26397,26399,26404,26406,26411,26413,26418],{"type":32,"value":26398},"On line 6, we are getting our ",{"type":27,"tag":653,"props":26400,"children":26402},{"className":26401},[],[26403],{"type":32,"value":17560},{"type":32,"value":26405}," property and we are using it in our request. If that link returns a status code 200, our link works. With this approach, we will probably write two tests. One for our index page and one for the other page. Within each test, we would check that the links that are on each of our pages actually work. We don’t have to do that necessarily. Instead of using ",{"type":27,"tag":653,"props":26407,"children":26409},{"className":26408},[],[26410],{"type":32,"value":12824},{"type":32,"value":26412}," we can actually use ",{"type":27,"tag":653,"props":26414,"children":26416},{"className":26415},[],[26417],{"type":32,"value":14057},{"type":32,"value":26419}," command and join these two test into one.",{"type":27,"tag":45,"props":26421,"children":26423},{"id":26422},"redirects-made-by-javascript",[26424],{"type":32,"value":26425},"Redirects made by JavaScript",{"type":27,"tag":28,"props":26427,"children":26428},{},[26429,26431,26436,26437,26442],{"type":32,"value":26430},"In some cases, redirect is not made by html attribute, but by JavaScript. In that case, there’s no href attribute we can open or send a request to. The only option is to click the link. But that link may point to an external site. ",{"type":27,"tag":14020,"props":26432,"children":26433},{},[26434],{"type":32,"value":26435},"As Cypress allows you to visit only a single superdomain",{"type":32,"value":14042},{"type":27,"tag":172,"props":26438,"children":26440},{"href":14045,"rel":26439},[696],[26441],{"type":32,"value":14049},{"type":32,"value":26443},"), you may feel limited when you want to test e.g. a feedback form that is located on Google docs. Visiting the link or sending a request are not viable options here. However, you can choose to spy on a function that is opening our browser window like this:",{"type":27,"tag":793,"props":26445,"children":26448},{"className":26446,"code":26447,"language":1513,"meta":5},[1510],"cy\n  .visit('./index.html');\n\ncy\n  .window().then((win) => {\n    cy.spy(win, 'open').as('redirect');\n  });\n\ncy\n  .get('a')\n  .click();\n\ncy\n  .get('@redirect')\n  .should('be.calledWith', '_blank', '/about');\n",[26449],{"type":27,"tag":653,"props":26450,"children":26451},{"__ignoreMap":5},[26452],{"type":32,"value":26447},{"type":27,"tag":28,"props":26454,"children":26455},{},[26456,26458,26463,26465,26470,26472,26478,26479,26485],{"type":32,"value":26457},"At the beginning of our test, we are registering our spy method, that going to spy for an ",{"type":27,"tag":653,"props":26459,"children":26461},{"className":26460},[],[26462],{"type":32,"value":7437},{"type":32,"value":26464}," function on our ",{"type":27,"tag":653,"props":26466,"children":26468},{"className":26467},[],[26469],{"type":32,"value":11688},{"type":32,"value":26471}," object. In our test, we are checking that our function is called with proper arguments, ",{"type":27,"tag":653,"props":26473,"children":26475},{"className":26474},[],[26476],{"type":32,"value":26477},"_blank",{"type":32,"value":4164},{"type":27,"tag":653,"props":26480,"children":26482},{"className":26481},[],[26483],{"type":32,"value":26484},"/about",{"type":32,"value":26486},". This way, we are checking the redirect even when we cannot see the link of our anchor element inside DOM.",{"title":5,"searchDepth":320,"depth":320,"links":26488},[26489,26490,26491],{"id":26255,"depth":320,"text":26258},{"id":26331,"depth":320,"text":26334},{"id":26422,"depth":320,"text":26425},"content:opening-a-new-tab-in-cypress:index.md","opening-a-new-tab-in-cypress/index.md","opening-a-new-tab-in-cypress/index",{"_path":12302,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":26496,"description":26497,"date":26498,"published":10,"slug":26499,"tags":26500,"readingTime":26501,"body":26505,"_type":329,"_id":27198,"_source":331,"_file":27199,"_stem":27200,"_extension":334},"Cypress basics: Selecting elements","Cypress is using query selectors to find elements on your page. But there are couple of really powerful ways to select elements on page using Cypress commands","2020-10-05","cypress-basics-selecting-elements",[5279,8651,18999],{"text":1933,"minutes":26502,"time":26503,"words":26504},6.855,411300,1371,{"type":24,"children":26506,"toc":27188},[26507,26517,26522,26536,26542,26547,26556,26591,26596,26605,26618,26627,26676,26682,26687,26697,26718,26726,26739,26748,26774,26779,26788,26822,26856,26862,26872,26878,26899,26908,26928,26937,26943,26969,26978,26998,27007,27012,27021,27027,27048,27057,27070,27079,27085,27090,27099,27111,27120,27126,27148,27160,27169,27174,27183],{"type":27,"tag":1029,"props":26508,"children":26509},{},[26510,26514],{"type":27,"tag":28,"props":26511,"children":26512},{},[26513],{"type":32,"value":5973},{"type":27,"tag":5975,"props":26515,"children":26516},{},[],{"type":27,"tag":28,"props":26518,"children":26519},{},[26520],{"type":32,"value":26521},"Selectors can be painful. Especially when you are starting with test automation. During my recent Cypress workshop, I saw some people struggle with selectors and the reason was, that they were using a different approach for selecting elements on page. In this blog, I would like to showcase some basics on how to select elements on page using Cypress.",{"type":27,"tag":28,"props":26523,"children":26524},{},[26525,26527,26534],{"type":32,"value":26526},"If you want to follow along this article, ",{"type":27,"tag":172,"props":26528,"children":26531},{"href":26529,"rel":26530},"https://github.com/filiphric/cypress-selectors",[696],[26532],{"type":32,"value":26533},"there’s a repo on my GitHub page",{"type":32,"value":26535}," where you’ll find all the examples.",{"type":27,"tag":45,"props":26537,"children":26539},{"id":26538},"selecting-a-single-element",[26540],{"type":32,"value":26541},"Selecting a single element",{"type":27,"tag":28,"props":26543,"children":26544},{},[26545],{"type":32,"value":26546},"In Cypress, you select elements using this syntax:",{"type":27,"tag":793,"props":26548,"children":26551},{"className":26549,"code":26550,"language":1513,"meta":5},[1510],"cy.get('.selector')\n",[26552],{"type":27,"tag":653,"props":26553,"children":26554},{"__ignoreMap":5},[26555],{"type":32,"value":26550},{"type":27,"tag":28,"props":26557,"children":26558},{},[26559,26561,26567,26569,26576,26578,26584,26586],{"type":32,"value":26560},"For starters, let’s look into what goes into the ",{"type":27,"tag":653,"props":26562,"children":26564},{"className":26563},[],[26565],{"type":32,"value":26566},".selector",{"type":32,"value":26568}," part. Cypress is selecting elements by ",{"type":27,"tag":172,"props":26570,"children":26573},{"href":26571,"rel":26572},"https://www.w3schools.com/cssref/css_selectors.asp",[696],[26574],{"type":32,"value":26575},"querying DOM",{"type":32,"value":26577},". You may be already familiar with such selectors if you have ever played with CSS or used jQuery or if you are familiar with ",{"type":27,"tag":653,"props":26579,"children":26581},{"className":26580},[],[26582],{"type":32,"value":26583},"document.querySelector",{"type":32,"value":26585}," command in JavaScript. Let’s see  what does this mean. As an example we can look into a page that looks something like this:\n",{"type":27,"tag":959,"props":26587,"children":26590},{"alt":26588,"src":26589},"Selecting different shapes with Cypress","shapes.png",[],{"type":27,"tag":28,"props":26592,"children":26593},{},[26594],{"type":32,"value":26595},"To select elements, it is of course vital to see into the page. Markup of our page looks like this:",{"type":27,"tag":793,"props":26597,"children":26600},{"className":26598,"code":26599,"language":7826,"meta":5},[7824],"\u003Ch1>Shapes:\u003C/h1>\n\u003Cdiv class=\"square\">\u003C/div>\n\u003Cdiv id=\"circle\">\u003C/div>\n\u003Cdiv shape=\"triangle\">\u003C/div>\n",[26601],{"type":27,"tag":653,"props":26602,"children":26603},{"__ignoreMap":5},[26604],{"type":32,"value":26599},{"type":27,"tag":28,"props":26606,"children":26607},{},[26608,26610,26616],{"type":32,"value":26609},"We can select an element using ",{"type":27,"tag":653,"props":26611,"children":26613},{"className":26612},[],[26614],{"type":32,"value":26615},"h1",{"type":32,"value":26617}," tag. If we want to select one of our shapes, we can select a single element using either class, id or an attribute.",{"type":27,"tag":793,"props":26619,"children":26622},{"className":26620,"code":26621,"language":1513,"meta":5},[1510],"cy\n  .get('h1') // select by tag\n  .get('.square') // select by class\n  .get('#circle') // select by id\n  .get('[shape=\"triangle\"]'); // select by attribute\n",[26623],{"type":27,"tag":653,"props":26624,"children":26625},{"__ignoreMap":5},[26626],{"type":32,"value":26621},{"type":27,"tag":28,"props":26628,"children":26629},{},[26630,26632,26637,26639,26645,26647,26653,26655,26661,26663,26668,26670],{"type":32,"value":26631},"To select an element by class you need to use ",{"type":27,"tag":653,"props":26633,"children":26635},{"className":26634},[],[26636],{"type":32,"value":256},{"type":32,"value":26638}," prefix and to select an element by its id, you should prefix id with ",{"type":27,"tag":653,"props":26640,"children":26642},{"className":26641},[],[26643],{"type":32,"value":26644},"#",{"type":32,"value":26646},". The most common attribute you might find on your page would be a ",{"type":27,"tag":653,"props":26648,"children":26650},{"className":26649},[],[26651],{"type":32,"value":26652},"placeholder",{"type":32,"value":26654}," for an input or even a ",{"type":27,"tag":653,"props":26656,"children":26658},{"className":26657},[],[26659],{"type":32,"value":26660},"test-id",{"type":32,"value":26662}," where your selector starts and ends with square brackets. If choose to we select an element that is found multiple times on our page, such as our ",{"type":27,"tag":653,"props":26664,"children":26666},{"className":26665},[],[26667],{"type":32,"value":18418},{"type":32,"value":26669}," element, Cypress will select all three of them. ",{"type":27,"tag":172,"props":26671,"children":26673},{"href":26529,"rel":26672},[696],[26674],{"type":32,"value":26675},"Try it for yourself in the code!",{"type":27,"tag":45,"props":26677,"children":26679},{"id":26678},"selecting-child-elements",[26680],{"type":32,"value":26681},"Selecting child elements",{"type":27,"tag":28,"props":26683,"children":26684},{},[26685],{"type":32,"value":26686},"When working with nested elements, these are often being referred to as child elements. The logic of nesting is simple. Child is nested by a parent. Each element can be both parent or child, depending on the relationship with some other element. In this next example, we have an html page where its structure looks something like this:",{"type":27,"tag":793,"props":26688,"children":26692},{"className":26689,"code":26690,"highlights":26691,"language":7826,"meta":5},[7824],"\u003Cdiv class=\"square-big red\">\n  \u003Cdiv class=\"circle green\">\u003C/div>\n\u003C/div>\n\n\u003Cdiv class=\"square-big green\">\n  \u003Cdiv class=\"circle red\">\u003C/div>\n\u003C/div>\n\n\u003Cdiv class=\"square-big green\">\n  \u003Cdiv class=\"square-small red\">\n    \u003Cdiv class=\"circle green\">\u003C/div>\n  \u003C/div>\n\u003C/div>\n",[320],[26693],{"type":27,"tag":653,"props":26694,"children":26695},{"__ignoreMap":5},[26696],{"type":32,"value":26690},{"type":27,"tag":28,"props":26698,"children":26699},{},[26700,26702,26708,26710,26716],{"type":32,"value":26701},"The structure is pretty simple and hopefully readable. All the green elements have a class ",{"type":27,"tag":653,"props":26703,"children":26705},{"className":26704},[],[26706],{"type":32,"value":26707},".green",{"type":32,"value":26709}," on them, and all the circle elements have a class ",{"type":27,"tag":653,"props":26711,"children":26713},{"className":26712},[],[26714],{"type":32,"value":26715},".circle",{"type":32,"value":26717}," on them. On the line 2, we have an element that has both of these classes. This will render a green circle. I made a screenshot of the actual page. Give yourself a moment and try to see how the html code corresponds with the rendered page (BTW, I left out the headings in the html code):",{"type":27,"tag":28,"props":26719,"children":26720},{},[26721],{"type":27,"tag":959,"props":26722,"children":26725},{"alt":26723,"src":26724},"Selecting various elements on page","squares.png",[],{"type":27,"tag":28,"props":26727,"children":26728},{},[26729,26731,26737],{"type":32,"value":26730},"Let’s focus only the inner circles for now. Selecting our circle by class, using ",{"type":27,"tag":653,"props":26732,"children":26734},{"className":26733},[],[26735],{"type":32,"value":26736},"cy.get('.circle')",{"type":32,"value":26738}," would return all 3 elements. But we may want to narrow down our selection though. We can do that by specifying our selector. Which element(s) would you guess will be returned by this selector?",{"type":27,"tag":793,"props":26740,"children":26743},{"className":26741,"code":26742,"language":1513,"meta":5},[1510],"cy\n  .get('.green .circle')\n",[26744],{"type":27,"tag":653,"props":26745,"children":26746},{"__ignoreMap":5},[26747],{"type":32,"value":26742},{"type":27,"tag":28,"props":26749,"children":26750},{},[26751,26753,26758,26760,26765,26767,26772],{"type":32,"value":26752},"The correct answer is circles in square #2 and square #3. This selector will look for all ",{"type":27,"tag":653,"props":26754,"children":26756},{"className":26755},[],[26757],{"type":32,"value":26715},{"type":32,"value":26759}," elements, that are inside any ",{"type":27,"tag":653,"props":26761,"children":26763},{"className":26762},[],[26764],{"type":32,"value":26707},{"type":32,"value":26766}," element. As we can see on our page, both of these are nested in a ",{"type":27,"tag":653,"props":26768,"children":26770},{"className":26769},[],[26771],{"type":32,"value":26707},{"type":32,"value":26773}," element, our big green square.",{"type":27,"tag":28,"props":26775,"children":26776},{},[26777],{"type":32,"value":26778},"Now let’s say we want to select only the circle inside square #2. In other words, if the circle is inside a red square, we want to ignore it. In our case, we would do it like this:",{"type":27,"tag":793,"props":26780,"children":26783},{"className":26781,"code":26782,"language":1513,"meta":5},[1510],"cy\n  .get('.green > .circle');\n\n",[26784],{"type":27,"tag":653,"props":26785,"children":26786},{"__ignoreMap":5},[26787],{"type":32,"value":26782},{"type":27,"tag":28,"props":26789,"children":26790},{},[26791,26793,26798,26800,26805,26807,26812,26814,26820],{"type":32,"value":26792},"This selector will only select those ",{"type":27,"tag":653,"props":26794,"children":26796},{"className":26795},[],[26797],{"type":32,"value":26715},{"type":32,"value":26799}," elements, where a ",{"type":27,"tag":653,"props":26801,"children":26803},{"className":26802},[],[26804],{"type":32,"value":26707},{"type":32,"value":26806}," element is one level above. Since in square #3, our ",{"type":27,"tag":653,"props":26808,"children":26810},{"className":26809},[],[26811],{"type":32,"value":26715},{"type":32,"value":26813}," element is nested inside a ",{"type":27,"tag":653,"props":26815,"children":26817},{"className":26816},[],[26818],{"type":32,"value":26819},".red",{"type":32,"value":26821}," element, it will not be selected.",{"type":27,"tag":28,"props":26823,"children":26824},{},[26825,26827,26832,26834,26839,26840,26846,26848,26854],{"type":32,"value":26826},"There are tons of ways we can select elements, and ",{"type":27,"tag":653,"props":26828,"children":26830},{"className":26829},[],[26831],{"type":32,"value":12748},{"type":32,"value":26833}," command works well with most of them. (I say most of them, since it is not possible to use pseudo selectors, such as ",{"type":27,"tag":653,"props":26835,"children":26837},{"className":26836},[],[26838],{"type":32,"value":22994},{"type":32,"value":1591},{"type":27,"tag":653,"props":26841,"children":26843},{"className":26842},[],[26844],{"type":32,"value":26845},":visited",{"type":32,"value":26847}," etc.) You can find a whole variety of selectors on ",{"type":27,"tag":172,"props":26849,"children":26851},{"href":26571,"rel":26850},[696],[26852],{"type":32,"value":26853},"W3 schools page",{"type":32,"value":26855},". Mastering these will help you immensely with writing your tests and understanding DOM structure of your page.",{"type":27,"tag":45,"props":26857,"children":26859},{"id":26858},"cypress-commands-for-selecting-elements",[26860],{"type":32,"value":26861},"Cypress commands for selecting elements",{"type":27,"tag":28,"props":26863,"children":26864},{},[26865,26867],{"type":32,"value":26866},"While mastering various CSS selectors is definitely useful, there are ton of ways you can select elements on page using Cypress commands. More importantly, these commands provide a better readability to for tests. In this example, we will be testing this lovely rainbow page:\n",{"type":27,"tag":959,"props":26868,"children":26871},{"alt":26869,"src":26870},"Testing rainbow with Cypress commands","rainbow.png",[],{"type":27,"tag":45,"props":26873,"children":26875},{"id":26874},"select-by-text",[26876],{"type":32,"value":26877},"Select by text",{"type":27,"tag":28,"props":26879,"children":26880},{},[26881,26883,26888,26890,26897],{"type":32,"value":26882},"To select our element its containing text we can use ",{"type":27,"tag":653,"props":26884,"children":26886},{"className":26885},[],[26887],{"type":32,"value":13507},{"type":32,"value":26889}," command. This is very ",{"type":27,"tag":172,"props":26891,"children":26894},{"href":26892,"rel":26893},"https://api.jquery.com/jQuery.contains/#jQuery-contains-container-contained",[696],[26895],{"type":32,"value":26896},"similar to a jQuery method with the same name",{"type":32,"value":26898},". This command can be used in various ways:",{"type":27,"tag":793,"props":26900,"children":26903},{"className":26901,"code":26902,"language":1513,"meta":5},[1510],"// select an element with the text \"indigo\"\ncy\n  .contains('indigo')\n\n// select an h1 element, that contains the text \"Rainbow\"\ncy\n  .contains('h1', 'Rainbow')\n",[26904],{"type":27,"tag":653,"props":26905,"children":26906},{"__ignoreMap":5},[26907],{"type":32,"value":26902},{"type":27,"tag":28,"props":26909,"children":26910},{},[26911,26913,26918,26920,26926],{"type":32,"value":26912},"You can even chain your commands together and create what is in my opinion quite self-explanatory code. Following code will look for a ",{"type":27,"tag":653,"props":26914,"children":26916},{"className":26915},[],[26917],{"type":32,"value":109},{"type":32,"value":26919}," (list item) element inside our ",{"type":27,"tag":653,"props":26921,"children":26923},{"className":26922},[],[26924],{"type":32,"value":26925},".list",{"type":32,"value":26927},". It will find multiple elements and from these, we will find the one that has the text \"purple\" inside it.",{"type":27,"tag":793,"props":26929,"children":26932},{"className":26930,"code":26931,"language":1513,"meta":5},[1510],"cy\n  .get('.list')\n  .find('li') // returns 7 li elements\n  .contains('violet') // returns a single element\n",[26933],{"type":27,"tag":653,"props":26934,"children":26935},{"__ignoreMap":5},[26936],{"type":32,"value":26931},{"type":27,"tag":45,"props":26938,"children":26940},{"id":26939},"select-by-position-in-list",[26941],{"type":32,"value":26942},"Select by position in list",{"type":27,"tag":28,"props":26944,"children":26945},{},[26946,26948,26954,26955,26961,26962,26967],{"type":32,"value":26947},"Inside our list, we can select elements based on their position in the list, using ",{"type":27,"tag":653,"props":26949,"children":26951},{"className":26950},[],[26952],{"type":32,"value":26953},".first()",{"type":32,"value":3372},{"type":27,"tag":653,"props":26956,"children":26958},{"className":26957},[],[26959],{"type":32,"value":26960},".last()",{"type":32,"value":1591},{"type":27,"tag":653,"props":26963,"children":26965},{"className":26964},[],[26966],{"type":32,"value":13728},{"type":32,"value":26968}," selector.",{"type":27,"tag":793,"props":26970,"children":26973},{"className":26971,"code":26972,"language":1513,"meta":5},[1510],"cy\n  .get('li')\n  .first(); // select \"red\"\n\ncy\n  .get('li')\n  .last(); // select \"violet\"\n\ncy\n  .get('li')\n  .eq(2); // select \"yellow\"\n",[26974],{"type":27,"tag":653,"props":26975,"children":26976},{"__ignoreMap":5},[26977],{"type":32,"value":26972},{"type":27,"tag":28,"props":26979,"children":26980},{},[26981,26983,26989,26991,26997],{"type":32,"value":26982},"You can also select an element relative to a selected element. For example, we can select a ",{"type":27,"tag":653,"props":26984,"children":26986},{"className":26985},[],[26987],{"type":32,"value":26988},".blue",{"type":32,"value":26990}," element by using ",{"type":27,"tag":653,"props":26992,"children":26994},{"className":26993},[],[26995],{"type":32,"value":26996},".next()",{"type":32,"value":7968},{"type":27,"tag":793,"props":26999,"children":27002},{"className":27000,"code":27001,"language":1513,"meta":5},[1510],"cy\n  .get('.green')\n  .next(); // will select the element .blue\n",[27003],{"type":27,"tag":653,"props":27004,"children":27005},{"__ignoreMap":5},[27006],{"type":32,"value":27001},{"type":27,"tag":28,"props":27008,"children":27009},{},[27010],{"type":32,"value":27011},"And of course, you can go the other way around:",{"type":27,"tag":793,"props":27013,"children":27016},{"className":27014,"code":27015,"language":1513,"meta":5},[1510],"cy\n  .get('.green')\n  .prev(); // will select the element .yellow\n",[27017],{"type":27,"tag":653,"props":27018,"children":27019},{"__ignoreMap":5},[27020],{"type":32,"value":27015},{"type":27,"tag":45,"props":27022,"children":27024},{"id":27023},"select-elements-by-filtering",[27025],{"type":32,"value":27026},"Select elements by filtering",{"type":27,"tag":28,"props":27028,"children":27029},{},[27030,27032,27038,27040,27046],{"type":32,"value":27031},"Once you select multiple elements (e.g. by ",{"type":27,"tag":653,"props":27033,"children":27035},{"className":27034},[],[27036],{"type":32,"value":27037},".get('li')",{"type":32,"value":27039}," command, which returns 7 elements), you can filter within these  based on another selector. Following code will only select the colors red, green and blue, since these are primary colors and have a class ",{"type":27,"tag":653,"props":27041,"children":27043},{"className":27042},[],[27044],{"type":32,"value":27045},".primary",{"type":32,"value":27047}," on them.",{"type":27,"tag":793,"props":27049,"children":27052},{"className":27050,"code":27051,"language":1513,"meta":5},[1510],"cy\n  .get('li')\n  .filter('.primary') // select all elements with the class .primary\n",[27053],{"type":27,"tag":653,"props":27054,"children":27055},{"__ignoreMap":5},[27056],{"type":32,"value":27051},{"type":27,"tag":28,"props":27058,"children":27059},{},[27060,27062,27068],{"type":32,"value":27061},"To do the exact opposite, you can use ",{"type":27,"tag":653,"props":27063,"children":27065},{"className":27064},[],[27066],{"type":32,"value":27067},".not()",{"type":32,"value":27069}," command. With this command you will select all the colors except red green and blue.",{"type":27,"tag":793,"props":27071,"children":27074},{"className":27072,"code":27073,"language":1513,"meta":5},[1510],"cy\n  .get('li')\n  .not('.primary') // select all elements without the class .primary\n",[27075],{"type":27,"tag":653,"props":27076,"children":27077},{"__ignoreMap":5},[27078],{"type":32,"value":27073},{"type":27,"tag":45,"props":27080,"children":27082},{"id":27081},"finding-elements",[27083],{"type":32,"value":27084},"Finding elements",{"type":27,"tag":28,"props":27086,"children":27087},{},[27088],{"type":32,"value":27089},"You can specify your selector by first selecting an element you want to search within, and then look down the DOM structure to find a specific element you are looking for.",{"type":27,"tag":793,"props":27091,"children":27094},{"className":27092,"code":27093,"language":1513,"meta":5},[1510],"cy\n  .get('.list')\n  .find('.violet') // finds an element with class .violet inside .list element\n",[27095],{"type":27,"tag":653,"props":27096,"children":27097},{"__ignoreMap":5},[27098],{"type":32,"value":27093},{"type":27,"tag":28,"props":27100,"children":27101},{},[27102,27104,27109],{"type":32,"value":27103},"Instead of looking down the DOM structure and finding an element within another element, we can look up. In this example, we first select our list item, and then try to find an element with a ",{"type":27,"tag":653,"props":27105,"children":27107},{"className":27106},[],[27108],{"type":32,"value":26925},{"type":32,"value":27110}," class",{"type":27,"tag":793,"props":27112,"children":27115},{"className":27113,"code":27114,"language":1513,"meta":5},[1510],"cy\n  .get('.violet')\n  .parent('.list') // finds an element with class .list that is above our .violet element\n",[27116],{"type":27,"tag":653,"props":27117,"children":27118},{"__ignoreMap":5},[27119],{"type":32,"value":27114},{"type":27,"tag":45,"props":27121,"children":27123},{"id":27122},"going-further",[27124],{"type":32,"value":27125},"Going further",{"type":27,"tag":28,"props":27127,"children":27128},{},[27129,27131,27137,27139,27146],{"type":32,"value":27130},"You can combine these commands any way you want to get to your element. However, you don’t want to overdo it. The cleanest way to select elements in Cypress is to make sure that your application actually contains the selectors you need. It is a good practice to add your own ",{"type":27,"tag":653,"props":27132,"children":27134},{"className":27133},[],[27135],{"type":32,"value":27136},"data-test",{"type":32,"value":27138}," attributes to those elements in your app, that you want to interact with. Moreover, if you then use the",{"type":27,"tag":172,"props":27140,"children":27143},{"href":27141,"rel":27142},"https://docs.cypress.io/guides/core-concepts/test-runner.html#Selector-Playground",[696],[27144],{"type":32,"value":27145},"Cypress Selector playground",{"type":32,"value":27147},", you may find your selectors more easily. This is because Cypress favors these selectors over classes, ids or other attributes. But you can easily customize which selectors should the Selector Playground utility prefer.",{"type":27,"tag":28,"props":27149,"children":27150},{},[27151,27153,27158],{"type":32,"value":27152},"If you are already using attributes to mark your elements, here’s a tip for you. You can create a custom command, that will select your element by e.g. ",{"type":27,"tag":653,"props":27154,"children":27156},{"className":27155},[],[27157],{"type":32,"value":8674},{"type":32,"value":27159}," attribute:",{"type":27,"tag":793,"props":27161,"children":27164},{"className":27162,"code":27163,"language":1513,"meta":5},[1510],"Cypress.Commands.add('getById', (input) => {\n\n  cy\n    .get(`[data-cy=${input}]`)\n\n})\n",[27165],{"type":27,"tag":653,"props":27166,"children":27167},{"__ignoreMap":5},[27168],{"type":32,"value":27163},{"type":27,"tag":28,"props":27170,"children":27171},{},[27172],{"type":32,"value":27173},"which you can later use in your test like this:",{"type":27,"tag":793,"props":27175,"children":27178},{"className":27176,"code":27177,"language":1513,"meta":5},[1510],"cy\n  .getById('indigo')\n",[27179],{"type":27,"tag":653,"props":27180,"children":27181},{"__ignoreMap":5},[27182],{"type":32,"value":27177},{"type":27,"tag":28,"props":27184,"children":27185},{},[27186],{"type":32,"value":27187},"Selecting your elements can definitely be a painful task when you are starting and don’t know what’s what. I hope this guide will help you navigate through your application DOM. If you are a PRO already, share the link with your friend. They might still be struggling 😅",{"title":5,"searchDepth":320,"depth":320,"links":27189},[27190,27191,27192,27193,27194,27195,27196,27197],{"id":26538,"depth":320,"text":26541},{"id":26678,"depth":320,"text":26681},{"id":26858,"depth":320,"text":26861},{"id":26874,"depth":320,"text":26877},{"id":26939,"depth":320,"text":26942},{"id":27023,"depth":320,"text":27026},{"id":27081,"depth":320,"text":27084},{"id":27122,"depth":320,"text":27125},"content:cypress-basics-selecting-elements:index.md","cypress-basics-selecting-elements/index.md","cypress-basics-selecting-elements/index",{"_path":23105,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":27202,"description":27203,"date":27204,"published":10,"slug":27205,"author":21092,"tags":27206,"readingTime":27208,"body":27212,"_type":329,"_id":27608,"_source":331,"_file":27609,"_stem":27610,"_extension":334},"Playing with experimental network stubbing feature in Cypress","A short exploration of the capabilities of new .route2() command that was released with Cypress version 5.1.0.","2020-09-28","playing-with-experimental-network-stubbing",[27207,23083,5279],"intercept",{"text":927,"minutes":27209,"time":27210,"words":27211},4.13,247800,826,{"type":24,"children":27213,"toc":27604},[27214,27263,27294,27299,27308,27336,27342,27354,27363,27383,27393,27405,27410,27418,27424,27435,27445,27450,27460,27472,27482,27502,27512,27524,27533,27541,27561],{"type":27,"tag":1029,"props":27215,"children":27216},{},[27217],{"type":27,"tag":28,"props":27218,"children":27219},{},[27220,27222,27227,27229,27234,27236,27241,27243,27248,27249,27254,27256,27261],{"type":32,"value":27221},"EDIT: Experimental network stubbing was released with v6 and ",{"type":27,"tag":653,"props":27223,"children":27225},{"className":27224},[],[27226],{"type":32,"value":23588},{"type":32,"value":27228}," command was renamed to ",{"type":27,"tag":653,"props":27230,"children":27232},{"className":27231},[],[27233],{"type":32,"value":14731},{"type":32,"value":27235},". Also, ",{"type":27,"tag":653,"props":27237,"children":27239},{"className":27238},[],[27240],{"type":32,"value":23116},{"type":32,"value":27242}," command was deprecated in this version. To see how you can migrate your ",{"type":27,"tag":653,"props":27244,"children":27246},{"className":27245},[],[27247],{"type":32,"value":23116},{"type":32,"value":23285},{"type":27,"tag":653,"props":27250,"children":27252},{"className":27251},[],[27253],{"type":32,"value":14731},{"type":32,"value":27255},", I recommend ",{"type":27,"tag":172,"props":27257,"children":27258},{"href":20098},[27259],{"type":32,"value":27260},"reading my blogpost",{"type":32,"value":27262}," on it. You’ll find examples and many useful links there.",{"type":27,"tag":28,"props":27264,"children":27265},{},[27266,27268,27274,27276,27283,27285,27292],{"type":32,"value":27267},"In the beginning of September, Cypress released a new experimental feature called ",{"type":27,"tag":653,"props":27269,"children":27271},{"className":27270},[],[27272],{"type":32,"value":27273},"experimentalNetworkStubbing",{"type":32,"value":27275},". I was watching development on Github for a while and was really excited when I started seeing some ",{"type":27,"tag":172,"props":27277,"children":27280},{"href":27278,"rel":27279},"https://github.com/cypress-io/cypress/issues/687",[696],[27281],{"type":32,"value":27282},"rapid movement on the issue",{"type":32,"value":27284},". I decided to have a closer look into what it does. I share code examples here, but if you want to play with this I have put together a ",{"type":27,"tag":172,"props":27286,"children":27289},{"href":27287,"rel":27288},"https://github.com/filiphric/route2-showcase",[696],[27290],{"type":32,"value":27291},"quick and dirty repo",{"type":32,"value":27293},". Clone → npm install, → npm start → npx cypress open and you’re good to go.",{"type":27,"tag":28,"props":27295,"children":27296},{},[27297],{"type":32,"value":27298},"With version 5.1.0 or higher, you can enable this feature by adding following line into your cypress.json file:",{"type":27,"tag":793,"props":27300,"children":27303},{"className":27301,"code":27302,"language":1004,"meta":5},[1002],"{\n    \"experimentalNetworkStubbing\": true\n}\n",[27304],{"type":27,"tag":653,"props":27305,"children":27306},{"__ignoreMap":5},[27307],{"type":32,"value":27302},{"type":27,"tag":28,"props":27309,"children":27310},{},[27311,27313,27318,27320,27327,27329,27334],{"type":32,"value":27312},"This enables you to use ",{"type":27,"tag":653,"props":27314,"children":27316},{"className":27315},[],[27317],{"type":32,"value":23588},{"type":32,"value":27319}," command which is ",{"type":27,"tag":172,"props":27321,"children":27324},{"href":27322,"rel":27323},"https://docs.cypress.io/api/commands/intercept.html",[696],[27325],{"type":32,"value":27326},"described in Cypress documentation",{"type":32,"value":27328},". Imagine  ",{"type":27,"tag":653,"props":27330,"children":27332},{"className":27331},[],[27333],{"type":32,"value":23116},{"type":32,"value":27335}," command, but on steroids.  You’ll see in a minute.",{"type":27,"tag":45,"props":27337,"children":27339},{"id":27338},"the-power-of-route",[27340],{"type":32,"value":27341},"The power of .route()",{"type":27,"tag":28,"props":27343,"children":27344},{},[27345,27347,27352],{"type":32,"value":27346},"If anyone ever asked me about my favourite command in Cypress, it would be ",{"type":27,"tag":653,"props":27348,"children":27350},{"className":27349},[],[27351],{"type":32,"value":23116},{"type":32,"value":27353},". With a simple syntax you can watch your api call being made:",{"type":27,"tag":793,"props":27355,"children":27358},{"className":27356,"code":27357,"language":1513,"meta":5},[1510],"cy\n  .server()\n  .route({\n    method: 'GET',\n    url: '/todos'\n  }).as('todoslist');\n\ncy\n  .visit('/'); // open page\n\ncy\n  .wait('@todoslist'); // items load from server via api\n",[27359],{"type":27,"tag":653,"props":27360,"children":27361},{"__ignoreMap":5},[27362],{"type":32,"value":27357},{"type":27,"tag":28,"props":27364,"children":27365},{},[27366,27368,27373,27375,27381],{"type":32,"value":27367},"After our app is opened, it loads a list of items from database. To do that, it calls a ",{"type":27,"tag":653,"props":27369,"children":27371},{"className":27370},[],[27372],{"type":32,"value":7797},{"type":32,"value":27374}," request to the  ",{"type":27,"tag":653,"props":27376,"children":27378},{"className":27377},[],[27379],{"type":32,"value":27380},"/todos",{"type":32,"value":27382}," url. Response comes back as a simple json file which is then rendered in our app. If you want to change this response and provide your app with your own json list of items, just add another parameter:",{"type":27,"tag":793,"props":27384,"children":27388},{"className":27385,"code":27386,"highlights":27387,"language":1513,"meta":5},[1510],"cy\n  .server()\n  .route({\n    method: 'GET',\n    url: '/todos',\n    response: 'fx:items' // fixtures/items.json\n  }).as('todoslist');\n\ncy\n  .visit('/'); // open page\n\ncy\n  .wait('@todoslist'); // items load from server via api\n",[3809],[27389],{"type":27,"tag":653,"props":27390,"children":27391},{"__ignoreMap":5},[27392],{"type":32,"value":27386},{"type":27,"tag":28,"props":27394,"children":27395},{},[27396,27398,27403],{"type":32,"value":27397},"This is simple, yet very powerful thing you do to test your application. ",{"type":27,"tag":653,"props":27399,"children":27401},{"className":27400},[],[27402],{"type":32,"value":23116},{"type":32,"value":27404}," command enables you to look into any xhr request your application makes and test it. You can combine your api and ui tests into one.",{"type":27,"tag":28,"props":27406,"children":27407},{},[27408],{"type":32,"value":27409},"The problem with this command though, is that you can only work with xhr requests. This rules out fetch requests, or other assets loaded via network. If you tried to route fetch request, you would end up like this:",{"type":27,"tag":28,"props":27411,"children":27412},{},[27413],{"type":27,"tag":959,"props":27414,"children":27417},{"alt":27415,"src":27416},"Fetch requests not working in Cypress","fetch_requests_not_working_in_cypress.mp4",[],{"type":27,"tag":45,"props":27419,"children":27421},{"id":27420},"the-power-of-route2",[27422],{"type":32,"value":27423},"The power of .route2()",{"type":27,"tag":28,"props":27425,"children":27426},{},[27427,27428,27433],{"type":32,"value":7021},{"type":27,"tag":653,"props":27429,"children":27431},{"className":27430},[],[27432],{"type":32,"value":23588},{"type":32,"value":27434}," command you can route fetch requests just as you would do with XHR. Pretty neat.",{"type":27,"tag":793,"props":27436,"children":27440},{"className":27437,"code":27438,"highlights":27439,"language":1513,"meta":5},[1510],"cy\n  .route2({\n    method: 'POST',\n    path: '/todos'\n  })\n  .as('createTodo');\n\ncy\n  .visit('/');\n\ncy\n  .addItem('new todo item'); // fetch request fired when adding item\n\ncy\n  .wait('@createTodo'); // it works!!\n",[3878],[27441],{"type":27,"tag":653,"props":27442,"children":27443},{"__ignoreMap":5},[27444],{"type":32,"value":27438},{"type":27,"tag":28,"props":27446,"children":27447},{},[27448],{"type":32,"value":27449},"That’s not all though. You can route static files such as css or images. This can become super handy if you want to test a website with lazy loaded images:",{"type":27,"tag":793,"props":27451,"children":27455},{"className":27452,"code":27453,"highlights":27454,"language":1513,"meta":5},[1510],"cy\n  .route2('/vendor/index.css')\n  .as('css');\n\ncy\n  .route2('/vendor/cypress-icon.png')\n  .as('logo');\n\ncy\n  .visit('/');\n\ncy\n  .wait('@css')\n  .wait('@logo');\n",[320,3809],[27456],{"type":27,"tag":653,"props":27457,"children":27458},{"__ignoreMap":5},[27459],{"type":32,"value":27453},{"type":27,"tag":28,"props":27461,"children":27462},{},[27463,27465,27470],{"type":32,"value":27464},"But there’s more! With ",{"type":27,"tag":653,"props":27466,"children":27468},{"className":27467},[],[27469],{"type":32,"value":23588},{"type":32,"value":27471}," command you can not only change response of your api call, but also request itself. Let’s say we want to add a custom header to our request to let the server know that these are coming from application that is being tested at the moment. You can manipulate your request headers like this:",{"type":27,"tag":793,"props":27473,"children":27477},{"className":27474,"code":27475,"highlights":27476,"language":1513,"meta":5},[1510],"cy\n  .route2({\n    method: 'POST',\n    path: '/todos'\n  }, (req) => {\n    req.headers['Mr-Meeseeks'] = 'Look at me!!';\n  })\n  .as('createTodo');\n\ncy\n  .visit('/');\n\ncy\n  .addItem('new todo item');\n",[3809],[27478],{"type":27,"tag":653,"props":27479,"children":27480},{"__ignoreMap":5},[27481],{"type":32,"value":27475},{"type":27,"tag":28,"props":27483,"children":27484},{},[27485,27487,27492,27494,27500],{"type":32,"value":27486},"This new header cannot be observed in network panel in DevTools, because the request manipulation actually happens outside of browser - before the request is sent to the server. Because of that, DevTools show the original request headers. But in the terminal where you ran your ",{"type":27,"tag":653,"props":27488,"children":27490},{"className":27489},[],[27491],{"type":32,"value":10030},{"type":32,"value":27493}," command, you can see that I’m logging all request headers for ",{"type":27,"tag":653,"props":27495,"children":27497},{"className":27496},[],[27498],{"type":32,"value":27499},"POST /todos",{"type":32,"value":27501}," request and our newly added header is visible there:",{"type":27,"tag":793,"props":27503,"children":27507},{"className":27504,"code":27505,"highlights":27506,"language":1084,"meta":5},[1082],"{\n  connection: 'keep-alive',\n  host: 'localhost:3000',\n  'proxy-connection': 'keep-alive',\n  'content-length': '61',\n  'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36',\n  'content-type': 'application/json',\n  accept: '*/*',\n  origin: 'http://localhost:3000',\n  'sec-fetch-site': 'same-origin',\n  'sec-fetch-mode': 'cors',\n  'sec-fetch-dest': 'empty',\n  referer: 'http://localhost:3000/',\n  'accept-encoding': 'gzip',\n  'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8',\n  'mr-meeseeks': 'Look at me!!'\n}\n",[3879],[27508],{"type":27,"tag":653,"props":27509,"children":27510},{"__ignoreMap":5},[27511],{"type":32,"value":27505},{"type":27,"tag":28,"props":27513,"children":27514},{},[27515,27517,27522],{"type":32,"value":27516},"In the same way we have changed our headers, we are able to change request body. Our application takes title of todo item from our text input. Once we hit enter key, that input is sent via fetch request to our server. When using ",{"type":27,"tag":653,"props":27518,"children":27520},{"className":27519},[],[27521],{"type":32,"value":23588},{"type":32,"value":27523}," we can actually change what is being sent to server, or even add our own data.",{"type":27,"tag":793,"props":27525,"children":27528},{"className":27526,"code":27527,"language":1513,"meta":5},[1510],"cy\n  .route2({\n    method: 'POST',\n    path: '/todos'\n  }, (req) => {\n    const requestBody = JSON.parse(req.body);\n\n    req.body = JSON.stringify({\n      ...requestBody,\n      title: 'Wubba Lubba Dub Dub!'\n    });\n  })\n  .as('createTodo');\n\ncy\n  .visit('/');\n\ncy\n  .addItem('new todo item');\n",[27529],{"type":27,"tag":653,"props":27530,"children":27531},{"__ignoreMap":5},[27532],{"type":32,"value":27527},{"type":27,"tag":28,"props":27534,"children":27535},{},[27536],{"type":27,"tag":959,"props":27537,"children":27540},{"alt":27538,"src":27539},"Changing network request body in Cypress","changing_network_request_body_in_cypress.mp4",[],{"type":27,"tag":28,"props":27542,"children":27543},{},[27544,27546,27552,27554,27559],{"type":32,"value":27545},"All these examples can be found in a ",{"type":27,"tag":172,"props":27547,"children":27549},{"href":27287,"rel":27548},[696],[27550],{"type":32,"value":27551},"repo that I have put together for this blog",{"type":32,"value":27553},". Feel free to play around with it and ",{"type":27,"tag":172,"props":27555,"children":27557},{"href":5770,"rel":27556},[696],[27558],{"type":32,"value":25677},{"type":32,"value":27560},", what you think of this new feature. In my perspective Cypress team has done amazing job here, and I’m excited about possibilities this change will bring.",{"type":27,"tag":1029,"props":27562,"children":27563},{},[27564],{"type":27,"tag":28,"props":27565,"children":27566},{},[27567,27569,27574,27576,27582,27584,27589,27590,27595,27597,27602],{"type":32,"value":27568},"EDIT: ",{"type":27,"tag":172,"props":27570,"children":27572},{"href":22381,"rel":27571},[696],[27573],{"type":32,"value":22385},{"type":32,"value":27575}," from Cypress ",{"type":27,"tag":172,"props":27577,"children":27579},{"href":23605,"rel":27578},[696],[27580],{"type":32,"value":27581},"wrote a really cool blog",{"type":32,"value":27583}," on differences between ",{"type":27,"tag":653,"props":27585,"children":27587},{"className":27586},[],[27588],{"type":32,"value":23116},{"type":32,"value":4164},{"type":27,"tag":653,"props":27591,"children":27593},{"className":27592},[],[27594],{"type":32,"value":23588},{"type":32,"value":27596}," commands, where he demonstrates some cool stuff you can do with ",{"type":27,"tag":653,"props":27598,"children":27600},{"className":27599},[],[27601],{"type":32,"value":23588},{"type":32,"value":27603},". You should definitely check it out.",{"title":5,"searchDepth":320,"depth":320,"links":27605},[27606,27607],{"id":27338,"depth":320,"text":27341},{"id":27420,"depth":320,"text":27423},"content:playing-with-experimental-network-stubbing:index.md","playing-with-experimental-network-stubbing/index.md","playing-with-experimental-network-stubbing/index",{"_path":27612,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":27613,"description":27614,"date":27615,"published":10,"slug":27616,"tags":27617,"readingTime":27618,"body":27622,"_type":329,"_id":27766,"_source":331,"_file":27767,"_stem":27768,"_extension":334},"/lesser-known-cypress-tricks","Lesser known Cypress.io tricks","I often come across undocumented or not so widely used features that you might find helpful. Here’s a list of couple of those.","2020-09-21","lesser-known-cypress-tricks",[21847,5279,18668,23083],{"text":3229,"minutes":27619,"time":27620,"words":27621},2.125,127500,425,{"type":24,"children":27623,"toc":27759},[27624,27629,27635,27640,27650,27655,27665,27671,27676,27690,27700,27706,27720,27729,27735,27740,27749,27755],{"type":27,"tag":28,"props":27625,"children":27626},{},[27627],{"type":32,"value":27628},"As I create my courses and use Cypress on my own, I often come across undocumented or not so widely used features that you might find helpful. Let’s jump into them.",{"type":27,"tag":45,"props":27630,"children":27632},{"id":27631},"routing-numbered-route",[27633],{"type":32,"value":27634},"Routing numbered route",{"type":27,"tag":28,"props":27636,"children":27637},{},[27638],{"type":32,"value":27639},"When using .route() command to match your path, you can use wildcards to math the exact api call you need. But sometimes it is just not enough. Your app may call the exact same endpoint twice. To write your test for these types of situations, you can select them both using an array, like this:",{"type":27,"tag":793,"props":27641,"children":27645},{"className":27642,"code":27643,"highlights":27644,"language":1513,"meta":5},[1510],"cy\n  .server()\n  .route('POST', '/todos')\n  .as('createTodo')\n\ncy\n  .visit('/')\n\n// create 2 todos via UI using custom command\ncy\n  .addTodo()\n  .addTodo()\n\ncy\n  .wait(['@createTodo', '@createTodo']).then( todos => {\n\n    expect(todos[0].status).to.eq(201)\n    expect(todos[1].status).to.eq(201)\n\n  })\n\n",[3878],[27646],{"type":27,"tag":653,"props":27647,"children":27648},{"__ignoreMap":5},[27649],{"type":32,"value":27643},{"type":27,"tag":28,"props":27651,"children":27652},{},[27653],{"type":32,"value":27654},"Instead of using an array though, you can select just the second instance of routed network request. This can be done by appending the index number on to the alias itself, like this:",{"type":27,"tag":793,"props":27656,"children":27660},{"className":27657,"code":27658,"highlights":27659,"language":1513,"meta":5},[1510],"cy\n  .server()\n  .route('POST', '/todos')\n  .as('createTodo')\n\ncy\n  .visit('/')\n\n// create 2 todos via UI using custom command\ncy\n  .addTodo()\n  .addTodo() // we will wait for a request that happens after this action\n\ncy\n  .wait('@createTodo.2').then( todos => {\n    expect(todos.status).to.eq(201)\n  })\n\n",[3878],[27661],{"type":27,"tag":653,"props":27662,"children":27663},{"__ignoreMap":5},[27664],{"type":32,"value":27658},{"type":27,"tag":45,"props":27666,"children":27668},{"id":27667},"aliasing-dom-element",[27669],{"type":32,"value":27670},"Aliasing DOM element",{"type":27,"tag":28,"props":27672,"children":27673},{},[27674],{"type":32,"value":27675},"Routing your network calls is one of the most powerful features in Cypress. To alias them, you can use .as() command, and then use .wait() or .get() command to write a test for that network request.",{"type":27,"tag":28,"props":27677,"children":27678},{},[27679,27681,27688],{"type":32,"value":27680},"You can alias your DOM elements in the same way and then use .get() command to select that element later in your test. This is especially useful when you have a list of items on the same level. For example, when using ",{"type":27,"tag":172,"props":27682,"children":27685},{"href":27683,"rel":27684},"https://github.com/4teamwork/cypress-drag-drop",[696],[27686],{"type":32,"value":27687},"cypress-drag-drop",{"type":32,"value":27689}," plugin to drag an element onto another.",{"type":27,"tag":793,"props":27691,"children":27695},{"className":27692,"code":27693,"highlights":27694,"language":1513,"meta":5},[1510],"cy\n  .get('.todo')\n  .eq(2)\n  .as('third')\n\ncy\n  .get('.todo')\n  .eq(3)\n  .as('fourth')\n\ncy\n  .get('@third')\n  .drag('@fourth')\n\n",[3877,3746],[27696],{"type":27,"tag":653,"props":27697,"children":27698},{"__ignoreMap":5},[27699],{"type":32,"value":27693},{"type":27,"tag":45,"props":27701,"children":27703},{"id":27702},"custom-formatting-of-log-messages",[27704],{"type":32,"value":27705},"Custom formatting of .log() messages",{"type":27,"tag":28,"props":27707,"children":27708},{},[27709,27711,27718],{"type":32,"value":27710},"I believe that you should write end to end tests as user stories. That is why it is vital that I understand where that story got interrupted on failed test. I have been playing around with ",{"type":27,"tag":172,"props":27712,"children":27715},{"href":27713,"rel":27714},"https://link.medium.com/l0dRBovSX9",[696],[27716],{"type":32,"value":27717},"ways to create a custom error message",{"type":32,"value":27719}," for some time now. Mainly because it helps a lot when debugging a failed test from screenshot, or basically captioning an end to end test. There is a way you can customize these messages using markdown formatting syntax.",{"type":27,"tag":793,"props":27721,"children":27724},{"className":27722,"code":27723,"language":1513,"meta":5},[1510],"cy.log('normal')\ncy.log('**bold**')\ncy.log('_italic_')\ncy.log('[blue](\u003Chttp://example.com>)')\n",[27725],{"type":27,"tag":653,"props":27726,"children":27727},{"__ignoreMap":5},[27728],{"type":32,"value":27723},{"type":27,"tag":45,"props":27730,"children":27732},{"id":27731},"custom-error-messages",[27733],{"type":32,"value":27734},"Custom error messages",{"type":27,"tag":28,"props":27736,"children":27737},{},[27738],{"type":32,"value":27739},"Beside custom .log() messages, you can customize your error messages too. I was thrilled to find out that expect() function can actually take a second parameter, which will become an error message on failure.",{"type":27,"tag":793,"props":27741,"children":27744},{"className":27742,"code":27743,"language":1513,"meta":5},[1510],"cy\n  .get('.todo')\n  .then( todo => {\n    expect(todo, 'Milk was not found').to.contain.text('Buy milk')\n})\n",[27745],{"type":27,"tag":653,"props":27746,"children":27747},{"__ignoreMap":5},[27748],{"type":32,"value":27743},{"type":27,"tag":45,"props":27750,"children":27752},{"id":27751},"bonus-tip-make-your-devtools-open-automatically-in-cypress-gui",[27753],{"type":32,"value":27754},"Bonus tip: Make your DevTools open automatically in Cypress GUI:",{"type":27,"tag":5872,"props":27756,"children":27758},{"id":27757},"1240700715854487553",[],{"title":5,"searchDepth":320,"depth":320,"links":27760},[27761,27762,27763,27764,27765],{"id":27631,"depth":320,"text":27634},{"id":27667,"depth":320,"text":27670},{"id":27702,"depth":320,"text":27705},{"id":27731,"depth":320,"text":27734},{"id":27751,"depth":320,"text":27754},"content:lesser-known-cypress-tricks:index.md","lesser-known-cypress-tricks/index.md","lesser-known-cypress-tricks/index",{"_path":27770,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":27771,"description":27772,"date":27773,"published":10,"slug":27774,"tags":27775,"cypressVersion":5959,"readingTime":27778,"body":27782,"_type":329,"_id":27967,"_source":331,"_file":27968,"_stem":27969,"_extension":334},"/my-favourite-vs-code-extensions","My favourite VS Code extensions for writing tests in Cypress","VS Code provides a variety of cool exenstions. Here are some of those that have proven to be useful for my workflow when writin tests in Cypress.","2020-09-11","my-favourite-vs-code-extensions",[5279,24482,27776,27777],"extensions","tips",{"text":21849,"minutes":27779,"time":27780,"words":27781},1.495,89700,299,{"type":24,"children":27783,"toc":27960},[27784,27789,27800,27812,27820,27831,27851,27859,27870,27883,27891,27902,27915,27923,27931,27941,27952],{"type":27,"tag":28,"props":27785,"children":27786},{},[27787],{"type":32,"value":27788},"That’s what we needed, right? Another VS code extension blog 😂 But I guess I got your attention, so why not share a little bit of my workflow? This one is for all you testers and developers writing tests in Cypress.",{"type":27,"tag":45,"props":27790,"children":27792},{"id":27791},"add-only",[27793],{"type":27,"tag":172,"props":27794,"children":27797},{"href":27795,"rel":27796},"https://marketplace.visualstudio.com/items?itemName=ub1que.add-only",[696],[27798],{"type":32,"value":27799},"Add .only",{"type":27,"tag":28,"props":27801,"children":27802},{},[27803,27805,27810],{"type":32,"value":27804},"Here’s to the lazy ones among us 🍻 ",{"type":27,"tag":172,"props":27806,"children":27808},{"href":27795,"rel":27807},[696],[27809],{"type":32,"value":27799},{"type":32,"value":27811}," extension does exactly what it claims to do. It filters your test by adding .only keyword to your it() block. It’s about 1 second faster than typing it yourself. Use that time well.",{"type":27,"tag":28,"props":27813,"children":27814},{},[27815],{"type":27,"tag":959,"props":27816,"children":27819},{"alt":27817,"src":27818},"Add .only to Cypress test","add_only_to_cypress_test.mp4",[],{"type":27,"tag":45,"props":27821,"children":27823},{"id":27822},"es6-mocha-snippets",[27824],{"type":27,"tag":172,"props":27825,"children":27828},{"href":27826,"rel":27827},"https://marketplace.visualstudio.com/items?itemName=spoonscen.es6-mocha-snippets",[696],[27829],{"type":32,"value":27830},"ES6 Mocha Snippets",{"type":27,"tag":28,"props":27832,"children":27833},{},[27834,27836,27842,27844],{"type":32,"value":27835},"This plugin actually saves you more than 1 second, and maybe even a couple of minutes. Depends on how many tests you write. ",{"type":27,"tag":172,"props":27837,"children":27839},{"href":27826,"rel":27838},[696],[27840],{"type":32,"value":27841},"These snippets",{"type":32,"value":27843}," help you create a quick describe() or it() block, and can create a combination of these two for you. You can quickly create a before() or beforeEach() hook. And after() too. ",{"type":27,"tag":172,"props":27845,"children":27848},{"href":27846,"rel":27847},"https://docs.cypress.io/guides/references/best-practices.html#Using-after-or-afterEach-hooks",[696],[27849],{"type":32,"value":27850},"But don’t use that one.",{"type":27,"tag":28,"props":27852,"children":27853},{},[27854],{"type":27,"tag":959,"props":27855,"children":27858},{"alt":27856,"src":27857},"Add Mocha snippet into your test","add_mocha_snippet_into_your_test.mp4",[],{"type":27,"tag":45,"props":27860,"children":27862},{"id":27861},"fold-plus",[27863],{"type":27,"tag":172,"props":27864,"children":27867},{"href":27865,"rel":27866},"https://marketplace.visualstudio.com/items?itemName=dakara.dakara-foldplus",[696],[27868],{"type":32,"value":27869},"Fold plus",{"type":27,"tag":28,"props":27871,"children":27872},{},[27873,27875,27881],{"type":32,"value":27874},"With many tests in one spec, you might want to look into just the names of your tests. If you wan’t to fold all your it() blocks, you might find ",{"type":27,"tag":172,"props":27876,"children":27878},{"href":27865,"rel":27877},[696],[27879],{"type":32,"value":27880},"Fold plus extension",{"type":32,"value":27882}," really useful. Works something like this.",{"type":27,"tag":28,"props":27884,"children":27885},{},[27886],{"type":27,"tag":959,"props":27887,"children":27890},{"alt":27888,"src":27889},"Fold all your tests","fold_all_your_tests.mp4",[],{"type":27,"tag":45,"props":27892,"children":27894},{"id":27893},"bracket-pair-colorizer",[27895],{"type":27,"tag":172,"props":27896,"children":27899},{"href":27897,"rel":27898},"https://marketplace.visualstudio.com/items?itemName=CoenraadS.bracket-pair-colorizer-2",[696],[27900],{"type":32,"value":27901},"Bracket pair colorizer",{"type":27,"tag":28,"props":27903,"children":27904},{},[27905,27907,27913],{"type":32,"value":27906},"With all the different code blocks, it is easy to get lost in all the brackets. If you are like me, and like to move stuff around, chances are you will forget a bracket pair spend couple of days trying to find out which one is missing (or extra). With ",{"type":27,"tag":172,"props":27908,"children":27910},{"href":27897,"rel":27909},[696],[27911],{"type":32,"value":27912},"highlighting code blocks",{"type":32,"value":27914},", it may take a little less.",{"type":27,"tag":28,"props":27916,"children":27917},{},[27918],{"type":27,"tag":959,"props":27919,"children":27922},{"alt":27920,"src":27921},"Bracket colors","bracket_colors.mp4",[],{"type":27,"tag":28,"props":27924,"children":27925},{},[27926],{"type":27,"tag":302,"props":27927,"children":27928},{},[27929],{"type":32,"value":27930},"EDIT: With newer versions of VS Code, you get colorizing brackets feature out of the box which makes this extension deprecated.",{"type":27,"tag":45,"props":27932,"children":27934},{"id":27933},"vscode-icons",[27935],{"type":27,"tag":172,"props":27936,"children":27939},{"href":27937,"rel":27938},"https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons",[696],[27940],{"type":32,"value":27933},{"type":27,"tag":28,"props":27942,"children":27943},{},[27944,27950],{"type":27,"tag":172,"props":27945,"children":27947},{"href":27937,"rel":27946},[696],[27948],{"type":32,"value":27949},"Just a nice little addition",{"type":32,"value":27951}," to your VS code. To keep things organized different folders get different icons. Cypress-related files and folder get a nice little Cypress icon, so you can find them more easily.",{"type":27,"tag":28,"props":27953,"children":27954},{},[27955],{"type":27,"tag":959,"props":27956,"children":27959},{"alt":27957,"src":27958},"VS Code icons","vs_code_icons.png",[],{"title":5,"searchDepth":320,"depth":320,"links":27961},[27962,27963,27964,27965,27966],{"id":27791,"depth":320,"text":27799},{"id":27822,"depth":320,"text":27830},{"id":27861,"depth":320,"text":27869},{"id":27893,"depth":320,"text":27901},{"id":27933,"depth":320,"text":27933},"content:my-favourite-vs-code-extensions:index.md","my-favourite-vs-code-extensions/index.md","my-favourite-vs-code-extensions/index",{"_path":12690,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":27971,"description":27972,"date":27973,"published":10,"slug":19827,"tags":27974,"readingTime":27976,"body":27980,"_type":329,"_id":28341,"_source":331,"_file":28342,"_stem":28343,"_extension":334},"Testing lists of items","Short description of various ways of testing list of items using Cypress, utilizing Cypress’ retryability.","2020-04-06",[5279,8996,27975],"retryability",{"text":585,"minutes":27977,"time":27978,"words":27979},3.475,208500,695,{"type":24,"children":27981,"toc":28336},[27982,27988,28015,28032,28046,28063,28072,28080,28117,28126,28134,28140,28171,28180,28190,28196,28201,28206,28215,28223,28258,28267,28275,28314,28323,28331],{"type":27,"tag":45,"props":27983,"children":27985},{"id":27984},"tldr",[27986],{"type":32,"value":27987},"TL;DR:",{"type":27,"tag":105,"props":27989,"children":27990},{},[27991,27999,28007],{"type":27,"tag":109,"props":27992,"children":27993},{},[27994],{"type":27,"tag":302,"props":27995,"children":27996},{},[27997],{"type":32,"value":27998},"you can test your lists using .then() or .each()",{"type":27,"tag":109,"props":28000,"children":28001},{},[28002],{"type":27,"tag":302,"props":28003,"children":28004},{},[28005],{"type":32,"value":28006},"Cypress retries can help you test changes in your app",{"type":27,"tag":109,"props":28008,"children":28009},{},[28010],{"type":27,"tag":302,"props":28011,"children":28012},{},[28013],{"type":32,"value":28014},"you can pass a function to .should() command",{"type":27,"tag":1029,"props":28016,"children":28017},{},[28018],{"type":27,"tag":28,"props":28019,"children":28020},{},[28021,28023,28030],{"type":32,"value":28022},"You can try out all the examples in this blog by ",{"type":27,"tag":172,"props":28024,"children":28027},{"href":28025,"rel":28026},"https://github.com/filiphric/testing-lists",[696],[28028],{"type":32,"value":28029},"cloning my repo",{"type":32,"value":28031},". The app is there too.",{"type":27,"tag":28,"props":28033,"children":28034},{},[28035,28037,28044],{"type":32,"value":28036},"Hello everyone 👋 We are going to test a list of todo items today. In my job as QA in ",{"type":27,"tag":172,"props":28038,"children":28041},{"href":28039,"rel":28040},"https://www.sli.do/",[696],[28042],{"type":32,"value":28043},"Slido",{"type":32,"value":28045},", I test lists a lot. In this blog I’m sharing some of my tips on how to that.",{"type":27,"tag":28,"props":28047,"children":28048},{},[28049,28051,28055,28057,28061],{"type":32,"value":28050},"We can start by testing a list with two items. In a situation where we test a couple of items, the testing flow can be pretty straightforward. In our first test, we use ",{"type":27,"tag":79,"props":28052,"children":28053},{},[28054],{"type":32,"value":12748},{"type":32,"value":28056}," to select our todo items and then ",{"type":27,"tag":79,"props":28058,"children":28059},{},[28060],{"type":32,"value":13728},{"type":32,"value":28062}," to filter the item we want to work with. Code looks something like this:",{"type":27,"tag":793,"props":28064,"children":28067},{"className":28065,"code":28066,"language":1513,"meta":5},[1510],"const todos = require('../fixtures/twoTodos')\n\nbeforeEach( () => {\n  cy\n    .request('POST', '/todos/seed', todos)\n  cy\n    .visit('localhost:3000');\n});\n\nit('Checks texts of todo items', () => {\n  cy\n    .get('.todo')\n    .eq(0)\n    .should('contain.text', 'buy milk');\n  cy\n    .get('.todo')\n    .eq(1)\n    .should('contain.text', 'wash dishes');\n});\n\n",[28068],{"type":27,"tag":653,"props":28069,"children":28070},{"__ignoreMap":5},[28071],{"type":32,"value":28066},{"type":27,"tag":28,"props":28073,"children":28074},{},[28075],{"type":27,"tag":959,"props":28076,"children":28079},{"alt":28077,"src":28078},"Selecting each item individually","selecting_each_item_individually.mp4",[],{"type":27,"tag":28,"props":28081,"children":28082},{},[28083,28085,28091,28093,28097,28099,28109,28111,28115],{"type":32,"value":28084},"We can make this code more compact. Instead of selecting each element individually, we can select them both and make a single assertion using ",{"type":27,"tag":172,"props":28086,"children":28089},{"href":28087,"rel":28088},"https://docs.cypress.io/api/commands/then.html",[696],[28090],{"type":32,"value":13338},{"type":32,"value":28092},". When ",{"type":27,"tag":79,"props":28094,"children":28095},{},[28096],{"type":32,"value":12748},{"type":32,"value":28098}," command finds multiple elements it returns an array. This means we can reference each item as we would in an array, using ",{"type":27,"tag":79,"props":28100,"children":28101},{},[28102,28104],{"type":32,"value":28103},"items",{"type":27,"tag":28105,"props":28106,"children":28107},"span",{},[28108],{"type":32,"value":22412},{"type":32,"value":28110}," where ",{"type":27,"tag":79,"props":28112,"children":28113},{},[28114],{"type":32,"value":22412},{"type":32,"value":28116}," is the index number.",{"type":27,"tag":793,"props":28118,"children":28121},{"className":28119,"code":28120,"language":1513,"meta":5},[1510],"it('Checks texts of todos items', () => {\n\n  cy\n    .get('.todo').then( items => {\n\n      expect(items[0]).to.contain.text('buy milk')\n      expect(items[1]).to.contain.text('wash dishes')\n\n    })\n\n});\n",[28122],{"type":27,"tag":653,"props":28123,"children":28124},{"__ignoreMap":5},[28125],{"type":32,"value":28120},{"type":27,"tag":28,"props":28127,"children":28128},{},[28129],{"type":27,"tag":959,"props":28130,"children":28133},{"alt":28131,"src":28132},"Selecting both items and making a single assertion","selecting_both_items_and_making_a_single_assertion.mp4",[],{"type":27,"tag":45,"props":28135,"children":28137},{"id":28136},"testing-longer-lists",[28138],{"type":32,"value":28139},"Testing longer lists",{"type":27,"tag":28,"props":28141,"children":28142},{},[28143,28145,28152,28154,28164,28166,28170],{"type":32,"value":28144},"While this approach is nice, we might get into a situation where want to test longer lists. With 10 or more items, our code might get repetitive. So instead, we can select our todo items, and then use ",{"type":27,"tag":172,"props":28146,"children":28149},{"href":28147,"rel":28148},"https://docs.cypress.io/api/commands/each.html#Syntax",[696],[28150],{"type":32,"value":28151},".each()",{"type":32,"value":28153}," command. This command works very similarly to a ",{"type":27,"tag":79,"props":28155,"children":28156},{},[28157],{"type":27,"tag":172,"props":28158,"children":28161},{"href":28159,"rel":28160},"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach",[696],[28162],{"type":32,"value":28163},"array.forEach()",{"type":32,"value":28165}," function and it enables us to work with items that are yielded via ",{"type":27,"tag":79,"props":28167,"children":28168},{},[28169],{"type":32,"value":12748},{"type":32,"value":14476},{"type":27,"tag":793,"props":28172,"children":28175},{"className":28173,"code":28174,"language":1513,"meta":5},[1510],"it('Checks texts of todos item', () => {\n\n  const todosTitles = [\"buy milk\", \"wash dishes\", \"clean windows\", \"clean up bedroom\", \"wash clothes\"]\n\n  cy\n    .get('.todo').each( (item, index) => {\n\n      cy\n        .wrap(item)\n       .should('contain.text', todosTitles[index])\n\n    })\n\n});\n\n",[28176],{"type":27,"tag":653,"props":28177,"children":28178},{"__ignoreMap":5},[28179],{"type":32,"value":28174},{"type":27,"tag":28,"props":28181,"children":28182},{},[28183,28188],{"type":27,"tag":959,"props":28184,"children":28187},{"alt":28185,"src":28186},"Testing a longer todo list using .each(","testing_a_longer_todo_list_using_each_command.mp4",[],{"type":32,"value":28189}," command)",{"type":27,"tag":45,"props":28191,"children":28193},{"id":28192},"checking-position-of-a-certain-todo-item",[28194],{"type":32,"value":28195},"Checking position of a certain todo item",{"type":27,"tag":28,"props":28197,"children":28198},{},[28199],{"type":32,"value":28200},"Now let’s say, we want to check an item being on a first position, but our test starts with a different state. Imagine this is a live collaborative todo list and that we want to test a scenario where another user changes the todo list.",{"type":27,"tag":28,"props":28202,"children":28203},{},[28204],{"type":32,"value":28205},"Here we have a test, that should have an item with the text „wash dishes“ on a first position. Our test starts with the item on second position, and during the test, we delete the first item.",{"type":27,"tag":793,"props":28207,"children":28210},{"className":28208,"code":28209,"language":1513,"meta":5},[1510],"it('Has first todo item with text \"wash dishes\"', () => {\n\n  cy\n    .get('.todo')\n    .eq(0)\n    .should('contain.text', 'wash dishes');\n\n});\n",[28211],{"type":27,"tag":653,"props":28212,"children":28213},{"__ignoreMap":5},[28214],{"type":32,"value":28209},{"type":27,"tag":28,"props":28216,"children":28217},{},[28218],{"type":27,"tag":959,"props":28219,"children":28222},{"alt":28220,"src":28221},"Failed test","failed_test.mp4",[],{"type":27,"tag":28,"props":28224,"children":28225},{},[28226,28228,28232,28234,28239,28241,28250,28252,28256],{"type":32,"value":28227},"But this test fails! The reason is that Cypress’ automatic retries don’t query the whole command chain, only the last command. In other words, our ",{"type":27,"tag":79,"props":28229,"children":28230},{},[28231],{"type":32,"value":12611},{"type":32,"value":28233}," is retried, but our ",{"type":27,"tag":79,"props":28235,"children":28236},{},[28237],{"type":32,"value":28238},".get('.todo')",{"type":32,"value":28240}," command is not. This means we are stuck with 3 todo items even after we delete the first one. See how the little blue “number 3\" does not change after we delete the first todo item. There is a great article about this in ",{"type":27,"tag":79,"props":28242,"children":28243},{},[28244],{"type":27,"tag":172,"props":28245,"children":28248},{"href":28246,"rel":28247},"https://docs.cypress.io/guides/core-concepts/retry-ability.html#Only-the-last-command-is-retried",[696],[28249],{"type":32,"value":22786},{"type":32,"value":28251},". To solve this problem, we can add an assertion for the length after our ",{"type":27,"tag":79,"props":28253,"children":28254},{},[28255],{"type":32,"value":28238},{"type":32,"value":28257}," command, so that we first assert the correct number of todo items and then assert the text of the first one.",{"type":27,"tag":793,"props":28259,"children":28262},{"className":28260,"code":28261,"language":1513,"meta":5},[1510],"it('Has first todo item with text \"wash dishes\"', () => {\n\n  cy\n  .get('.todo')\n  .should('have.length', 2)\n  .eq(0)\n  .should('contain.text', 'wash dishes');\n\n});\n",[28263],{"type":27,"tag":653,"props":28264,"children":28265},{"__ignoreMap":5},[28266],{"type":32,"value":28261},{"type":27,"tag":28,"props":28268,"children":28269},{},[28270],{"type":27,"tag":959,"props":28271,"children":28274},{"alt":28272,"src":28273},"Our test moves to next command only after assertion passes","our_test_moves_to_next_command_only_after_assertion_passes.mp4",[],{"type":27,"tag":28,"props":28276,"children":28277},{},[28278,28280,28285,28287,28291,28293,28300,28302,28306,28308,28312],{"type":32,"value":28279},"This solution is not really satisfying, is it? We may be facing a situation where the ",{"type":27,"tag":302,"props":28281,"children":28282},{},[28283],{"type":32,"value":28284},"number of our items does not change",{"type":32,"value":28286},", but only the order of our items changes. Because we can use drag and drop in our app 😎. In that case, we can use ",{"type":27,"tag":79,"props":28288,"children":28289},{},[28290],{"type":32,"value":12439},{"type":32,"value":28292}," command and pass a function into it. There are many ",{"type":27,"tag":172,"props":28294,"children":28297},{"href":28295,"rel":28296},"https://docs.cypress.io/api/commands/should.html#Function",[696],[28298],{"type":32,"value":28299},"cool examples in the documentation",{"type":32,"value":28301}," on this. The final code looks very similar to when we are using .then(). The main difference is, that ",{"type":27,"tag":79,"props":28303,"children":28304},{},[28305],{"type":32,"value":12439},{"type":32,"value":28307}," commands uses retries logic, but ",{"type":27,"tag":79,"props":28309,"children":28310},{},[28311],{"type":32,"value":13338},{"type":32,"value":28313}," not use retry.",{"type":27,"tag":793,"props":28315,"children":28318},{"className":28316,"code":28317,"language":1513,"meta":5},[1510],"it('Has first todo item with text \"wash dishes\"', () => {\n\n  cy\n    .get('.todo').should( items => {\n\n      expect(items[0]).to.contain.text('wash dishes')\n      expect(items[1]).to.contain.text('buy milk')\n\n    })\n\n});\n",[28319],{"type":27,"tag":653,"props":28320,"children":28321},{"__ignoreMap":5},[28322],{"type":32,"value":28317},{"type":27,"tag":28,"props":28324,"children":28325},{},[28326],{"type":27,"tag":959,"props":28327,"children":28330},{"alt":28328,"src":28329},"Passing test","passing_test.mp4",[],{"type":27,"tag":28,"props":28332,"children":28333},{},[28334],{"type":32,"value":28335},"That’s it. Hope you enjoyed this.",{"title":5,"searchDepth":320,"depth":320,"links":28337},[28338,28339,28340],{"id":27984,"depth":320,"text":27987},{"id":28136,"depth":320,"text":28139},{"id":28192,"depth":320,"text":28195},"content:testing-lists-of-items:index.md","testing-lists-of-items/index.md","testing-lists-of-items/index",{"_path":19736,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":28345,"description":28346,"date":28347,"published":10,"slug":28348,"tags":28349,"readingTime":28352,"body":28356,"_type":329,"_id":28494,"_source":331,"_file":28495,"_stem":28496,"_extension":334},"Improve your error screenshots in Cypress","Explanation of how you can use cy.log() command to make your screenshots more readable and your debugging experience even faster.","2020-03-09","improve-your-error-screenshots-in-cypress",[5279,21847,28350,28351],"screenshots","errors",{"text":3229,"minutes":28353,"time":28354,"words":28355},2.71,162600,542,{"type":24,"children":28357,"toc":28492},[28358,28363,28374,28379,28395,28400,28405,28410,28429,28434,28446,28451,28460,28465,28473,28478,28487],{"type":27,"tag":28,"props":28359,"children":28360},{},[28361],{"type":32,"value":28362},"Cypress makes it incredibly easy to read their code. Even people that don’t work with Cypress here at Slido can easily understand what is going on in the test code.",{"type":27,"tag":28,"props":28364,"children":28365},{},[28366,28368,28372],{"type":32,"value":28367},"That’s why I overlooked the ",{"type":27,"tag":79,"props":28369,"children":28370},{},[28371],{"type":32,"value":19153},{"type":32,"value":28373}," command. It seemed kind of arbitrary and didn’t make too much sense to add extra log into what is already pretty readable flow. Cypress pretty much nailed it when it comes to readability of code. Not only that, readability of test reports is amazing as well.",{"type":27,"tag":28,"props":28375,"children":28376},{},[28377],{"type":32,"value":28378},"When working with headless mode, you can actually see what your test does, since Cypress records video for your headless runs. If you need to examine a failed run, you can easily look into what your test did before it failed.",{"type":27,"tag":1029,"props":28380,"children":28381},{},[28382],{"type":27,"tag":28,"props":28383,"children":28384},{},[28385,28387,28393],{"type":32,"value":28386},"Extra tip: Did you know, you may choose the option to upload video only on failed runs? Save some extra seconds by adding ",{"type":27,"tag":653,"props":28388,"children":28390},{"className":28389},[],[28391],{"type":32,"value":28392},"videoUploadOnPasses: false",{"type":32,"value":28394}," to your cypress.config.js file.",{"type":27,"tag":28,"props":28396,"children":28397},{},[28398],{"type":32,"value":28399},"Situation changes rapidly as your test suite grows. With over 1000 tests here in Slido we sometimes face a situation where we sometimes have to look into failures of multiple tests. It takes slightly more time to look into video than to look on a screenshot. Although video shows the whole test run, screenshots can be more efficient if done properly. I don’t know about you, but I’d rather go through 10 screenshots than through 10 videos to find out why a test failed.",{"type":27,"tag":28,"props":28401,"children":28402},{},[28403],{"type":32,"value":28404},"But - on an error screenshot, you are limited to screenshot height so you probably see only a couple of commands. This is where I found out cy.log() might be handy. With cy.log() I can caption my screenshots, and give more context to what the test was doing before it failed. I started adding cy.log() commands to each step of my test, to describe action that is being executed.",{"type":27,"tag":28,"props":28406,"children":28407},{},[28408],{"type":32,"value":28409},"Then, I added a few tweaks into this.",{"type":27,"tag":851,"props":28411,"children":28412},{},[28413,28424],{"type":27,"tag":109,"props":28414,"children":28415},{},[28416,28418],{"type":32,"value":28417},"I rewrote the command to add triple dash in front and at the end of the message, to make my logs pop out. Basically to guide my eyes better (yes, I know ",{"type":27,"tag":172,"props":28419,"children":28421},{"href":28420},"/what-psychology-taught-me-about-qa",[28422],{"type":32,"value":28423},"some psychology",{"type":27,"tag":109,"props":28425,"children":28426},{},[28427],{"type":32,"value":28428},"I added a counter, so that every log has a number and I can get a grasp of where the test failed",{"type":27,"tag":28,"props":28430,"children":28431},{},[28432],{"type":32,"value":28433},"The result looks like this:",{"type":27,"tag":28,"props":28435,"children":28436},{},[28437,28442],{"type":27,"tag":959,"props":28438,"children":28441},{"alt":28439,"src":28440},"Upper screenshots is without logs, lower screenshot shows log on each step.","log-comparison.png",[],{"type":27,"tag":302,"props":28443,"children":28444},{},[28445],{"type":32,"value":28439},{"type":27,"tag":28,"props":28447,"children":28448},{},[28449],{"type":32,"value":28450},"Adding this was in fact quite easy. I added this piece of code to my support/index.js file:",{"type":27,"tag":793,"props":28452,"children":28455},{"className":28453,"code":28454,"language":1513,"meta":5},[1510],"beforeEach( function() {\n  window.logCalls = 1;\n});\n\nCypress.Commands.overwrite('log', (...args) => {\n\n  const msg: string = args[1];\n\n  Cypress.log({\n    displayName: `--- ${window.logCalls}. ${msg.toUpperCase()} ---`,\n    message: '\\n'\n  });\n\n  window.logCalls++;\n\n});\n",[28456],{"type":27,"tag":653,"props":28457,"children":28458},{"__ignoreMap":5},[28459],{"type":32,"value":28454},{"type":27,"tag":28,"props":28461,"children":28462},{},[28463],{"type":32,"value":28464},"While writing this blog, I realized, that these logs can be further used for creating better error logs. Each of these steps can be added to error message, which would look something like this:",{"type":27,"tag":28,"props":28466,"children":28467},{},[28468],{"type":27,"tag":959,"props":28469,"children":28472},{"alt":28470,"src":28471},"Error report in Cypress log","2.png",[],{"type":27,"tag":28,"props":28474,"children":28475},{},[28476],{"type":32,"value":28477},"Basically we are just creating an empty array that we are going to output as a part of error message when a test fails.",{"type":27,"tag":793,"props":28479,"children":28482},{"className":28480,"code":28481,"language":1513,"meta":5},[1510],"beforeEach( function() {\n  window.logCalls = 1;\n  window.testFlow = [];\n});\n\nCypress.Commands.overwrite('log', (...args) => {\n\n  const msg: string = args[1];\n\n  Cypress.log({\n    displayName: `--- ${window.logCalls}. ${msg.toUpperCase()} ---`,\n    message: '\\n'\n  });\n\n  window.testFlow.push(`${window.logCalls}. ${msg}`);\n  window.logCalls++;\n\n});\n\nCypress.on('fail', (err) => {\n  err.message += `${'\\n\\n' + 'Test flow was:\\n\\n'}${window.testFlow.join('\\n')}`;\n  throw err;\n});\n",[28483],{"type":27,"tag":653,"props":28484,"children":28485},{"__ignoreMap":5},[28486],{"type":32,"value":28481},{"type":27,"tag":28,"props":28488,"children":28489},{},[28490],{"type":32,"value":28491},"These error messages are written out to Cypress dashboard, so if you caption them right, boom! Instant error scenario! There goes a JIRA ticket 😆",{"title":5,"searchDepth":320,"depth":320,"links":28493},[],"content:improve-your-error-screenshots-in-cypress:index.md","improve-your-error-screenshots-in-cypress/index.md","improve-your-error-screenshots-in-cypress/index",{"_path":28498,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":28499,"description":28500,"date":28501,"published":10,"slug":28502,"tags":28503,"readingTime":28507,"body":28511,"_type":329,"_id":28706,"_source":331,"_file":28707,"_stem":28708,"_extension":334},"/i-did-a-cypress-workshop-and-this-is-what-i-learned","I did a Cypress workshop and this is what I learned","Describing how I conducted my first live workshop on Cypress.io on a conference in Brno and sharing my personal recommendations.","2019-10-29","i-did-a-cypress-workshop-and-this-is-what-i-learned",[28504,28505,28506],"learnings","personal","workshop",{"text":585,"minutes":28508,"time":28509,"words":28510},3.58,214800,716,{"type":24,"children":28512,"toc":28699},[28513,28527,28532,28538,28543,28555,28561,28575,28587,28593,28598,28603,28619,28625,28630,28656,28662,28667,28680],{"type":27,"tag":28,"props":28514,"children":28515},{},[28516,28518,28525],{"type":32,"value":28517},"I had a chance to do a Cypress workshop at this year’s ",{"type":27,"tag":172,"props":28519,"children":28522},{"href":28520,"rel":28521},"https://www.testcrunch.cz/2019/prednaska/5",[696],[28523],{"type":32,"value":28524},"TestCrunch in Brno",{"type":32,"value":28526},". It was an amazing experience. I knew I loved teaching, but I never thought I’d enjoy it this much. The format of a hands-on workshop is truly an amazing experience, and the most cool thing about this — it’s a different story every time.",{"type":27,"tag":28,"props":28528,"children":28529},{},[28530],{"type":32,"value":28531},"That said, there we some rough edges I hope to smooth in the future. Here I my learnings, hope this helps you in your future workshop.",{"type":27,"tag":45,"props":28533,"children":28535},{"id":28534},"preparation-preparation-preparation",[28536],{"type":32,"value":28537},"Preparation, preparation, preparation",{"type":27,"tag":28,"props":28539,"children":28540},{},[28541],{"type":32,"value":28542},"No matter how big of an expert you (think) are on presented topic, you need to prepare. Good preparation gives you a good structure, that provides you some anchoring points. If participants ask questions (and it’s a sign you are doing a great job when they do), you often get sidetracked. Good structure helps you decide on answering a question immediately, or leaving it for later, so you can discuss it in more depth afterwards.",{"type":27,"tag":28,"props":28544,"children":28545},{},[28546,28553],{"type":27,"tag":302,"props":28547,"children":28548},{},[28549],{"type":27,"tag":79,"props":28550,"children":28551},{},[28552],{"type":32,"value":2161},{"type":32,"value":28554}," Don’t hide your table of contents. It helps participants time their questions well or give them a frame on when their question is going to be answered if postponed.",{"type":27,"tag":45,"props":28556,"children":28558},{"id":28557},"learn-from-the-best",[28559],{"type":32,"value":28560},"Learn from the best",{"type":27,"tag":28,"props":28562,"children":28563},{},[28564,28566,28573],{"type":32,"value":28565},"I often thought of doing a workshop on Cypress and I failed at creating a good structure. Luckily, folks at Cypress have prepared a workshop that I was able to ",{"type":27,"tag":172,"props":28567,"children":28570},{"href":28568,"rel":28569},"https://github.com/cypress-io/testing-workshop-cypress",[696],[28571],{"type":32,"value":28572},"download right from GitHub",{"type":32,"value":28574},". It helped me create my own version more easily. But more importantly I learned how to pace workshop in a way that makes the learning experience smooth.",{"type":27,"tag":28,"props":28576,"children":28577},{},[28578,28585],{"type":27,"tag":302,"props":28579,"children":28580},{},[28581],{"type":27,"tag":79,"props":28582,"children":28583},{},[28584],{"type":32,"value":2161},{"type":32,"value":28586}," Divide every part of your workshop into theoretical, practical and discussion part. This helps you provide information, encourage learning by doing, and then share those learnings or answer questions at the end.",{"type":27,"tag":45,"props":28588,"children":28590},{"id":28589},"prepare-for-troubleshooting",[28591],{"type":32,"value":28592},"Prepare for troubleshooting",{"type":27,"tag":28,"props":28594,"children":28595},{},[28596],{"type":32,"value":28597},"During my workshop on Cypress, we have spent quite a long time installing node, git, figuring out command line and debugging problems. This was not really a comfortable part. I was a little stressed, participants were frustrated (but determined nevertheless, which was nice) or bored. Next time I’d definitely reach out to participants beforehand and ask them to try to do the installations on their own. If that would fail, I’d give the option to come in early and provide help, so that I save some time and move more quickly to the fun stuff.",{"type":27,"tag":28,"props":28599,"children":28600},{},[28601],{"type":32,"value":28602},"As a Mac user, I found myself in a room full of Windows users. I realized, I have never used command line on a windows computer and at one point I have advised to use ls command — which of course did nothing. In hindsight, I think I should have done a workshop dry run at home, or on someone else’s computer.",{"type":27,"tag":28,"props":28604,"children":28605},{},[28606,28610,28612,28617],{"type":27,"tag":79,"props":28607,"children":28608},{},[28609],{"type":32,"value":2161},{"type":32,"value":28611}," Use knowledge of others. Participants can help each other, so the pressure is not all on you. Encourage participants to discuss problems, especially in practical parts. I’ve seen this being done amazingly by ",{"type":27,"tag":172,"props":28613,"children":28615},{"href":22381,"rel":28614},[696],[28616],{"type":32,"value":22385},{"type":32,"value":28618}," -himself at a workshop he held in Bratislava.",{"type":27,"tag":45,"props":28620,"children":28622},{"id":28621},"ask-for-feedback-regularly",[28623],{"type":32,"value":28624},"Ask for feedback, regularly",{"type":27,"tag":28,"props":28626,"children":28627},{},[28628],{"type":32,"value":28629},"There is always a potential threat of missed opportunity when information flows only in one direction. Engage in conversation with your participants. They can give you a great feedback on what they understand and what needs to be explained better. The fear of asking stupid question is especially present during workshops. I like to use Slido and create some polls, especially for the purpose of asking about which part was hardest to understand. Participants can vote anonymously. After votes come in, I point out that I understand why this topic in particular can be hard to understand. And 100% of the times, I really do. This usually gives participants the confidence to speak up when I ask them to elaborate more on what’s unclear, creating a safe environment for any type of question.",{"type":27,"tag":28,"props":28631,"children":28632},{},[28633,28637,28639,28646,28648,28655],{"type":27,"tag":79,"props":28634,"children":28635},{},[28636],{"type":32,"value":2161},{"type":32,"value":28638}," I gained a lot of great learnings from ",{"type":27,"tag":172,"props":28640,"children":28643},{"href":28641,"rel":28642},"https://blog.sli.do/9-tips-engaging-webinars/",[696],[28644],{"type":32,"value":28645},"blog that my colleague wrote on webinars",{"type":32,"value":28647},". I realized running a webinar isn’t all that different from running a workshop and a lot of the same principles apply. If you’d like to see me doing a webinar, ",{"type":27,"tag":172,"props":28649,"children":28652},{"href":28650,"rel":28651},"https://www.cypress.io/blog/2019/08/16/webcast-recording-from-zero-to-hero-with-cypress/",[696],[28653],{"type":32,"value":28654},"you can find a recording here",{"type":32,"value":256},{"type":27,"tag":45,"props":28657,"children":28659},{"id":28658},"summary",[28660],{"type":32,"value":28661},"Summary",{"type":27,"tag":28,"props":28663,"children":28664},{},[28665],{"type":32,"value":28666},"As I said — this was an amazing experience to me and participants left me some very nice feedbacks:",{"type":27,"tag":28,"props":28668,"children":28669},{},[28670,28675],{"type":27,"tag":959,"props":28671,"children":28674},{"alt":28672,"src":28673},"Some feedbacks from participants: useful, amazing, beneficial","1.png",[],{"type":27,"tag":302,"props":28676,"children":28677},{},[28678],{"type":32,"value":28679},"Some feedbacks from participants: useful, amazing, beneficial,...",{"type":27,"tag":28,"props":28681,"children":28682},{},[28683,28685,28689,28691,28697],{"type":32,"value":28684},"I hope my blog will help you in doing ",{"type":27,"tag":302,"props":28686,"children":28687},{},[28688],{"type":32,"value":5987},{"type":32,"value":28690}," workshops. Any other tips? Feedbacks? ",{"type":27,"tag":172,"props":28692,"children":28694},{"href":1893,"rel":28693},[696],[28695],{"type":32,"value":28696},"Tweet at me",{"type":32,"value":28698},"!",{"title":5,"searchDepth":320,"depth":320,"links":28700},[28701,28702,28703,28704,28705],{"id":28534,"depth":320,"text":28537},{"id":28557,"depth":320,"text":28560},{"id":28589,"depth":320,"text":28592},{"id":28621,"depth":320,"text":28624},{"id":28658,"depth":320,"text":28661},"content:i-did-a-cypress-workshop-and-this-is-what-i-learned:index.md","i-did-a-cypress-workshop-and-this-is-what-i-learned/index.md","i-did-a-cypress-workshop-and-this-is-what-i-learned/index",{"_path":28710,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":28711,"description":28712,"date":28713,"published":10,"slug":28714,"tags":28715,"readingTime":28717,"body":28721,"_type":329,"_id":28921,"_source":331,"_file":28922,"_stem":28923,"_extension":334},"/testing-email-flows-with-mailosaur","Testing email flows with Mailosaur","A short giude on how to test e2e flows that require an email to be opened. Code demonstration using Mailosaur.","2019-09-13","testing-email-flows-with-mailosaur",[5279,28716],"email testing",{"text":3229,"minutes":28718,"time":28719,"words":28720},2.755,165300,551,{"type":24,"children":28722,"toc":28916},[28723],{"type":27,"tag":5872,"props":28724,"children":28725},{"id":8356},[28726,28737,28743,28753,28758,28786,28791,28796,28802,28812,28826,28831,28840,28846,28860,28869,28874,28883,28888,28897,28902,28911],{"type":27,"tag":28,"props":28727,"children":28728},{},[28729,28731,28736],{"type":32,"value":28730},"I would like to give you a glimpse of how we use Cypress with a tool called ",{"type":27,"tag":172,"props":28732,"children":28734},{"href":2464,"rel":28733},[696],[28735],{"type":32,"value":2468},{"type":32,"value":256},{"type":27,"tag":45,"props":28738,"children":28740},{"id":28739},"telling-the-whole-story",[28741],{"type":32,"value":28742},"Telling the whole story",{"type":27,"tag":28,"props":28744,"children":28745},{},[28746,28748],{"type":32,"value":28747},"When writing tests, I like to resemble user behavior as much as possible. In other words, ",{"type":27,"tag":79,"props":28749,"children":28750},{},[28751],{"type":32,"value":28752},"I want to tell the whole story.",{"type":27,"tag":28,"props":28754,"children":28755},{},[28756],{"type":32,"value":28757},"Let me give you an example. Let’s say you forgot your password to your account. Here is what you would do as a user. You would:",{"type":27,"tag":851,"props":28759,"children":28760},{},[28761,28766,28771,28776,28781],{"type":27,"tag":109,"props":28762,"children":28763},{},[28764],{"type":32,"value":28765},"go to reset login page and request password reset",{"type":27,"tag":109,"props":28767,"children":28768},{},[28769],{"type":32,"value":28770},"receive an email with a reset link",{"type":27,"tag":109,"props":28772,"children":28773},{},[28774],{"type":32,"value":28775},"visit that link",{"type":27,"tag":109,"props":28777,"children":28778},{},[28779],{"type":32,"value":28780},"type in your new password",{"type":27,"tag":109,"props":28782,"children":28783},{},[28784],{"type":32,"value":28785},"log in with new password",{"type":27,"tag":28,"props":28787,"children":28788},{},[28789],{"type":32,"value":28790},"With Cypress, you can easily take care of step 1. But getting to step 3 requires opening an email inbox, which is not something you can easily do (or want to do) in a test.",{"type":27,"tag":28,"props":28792,"children":28793},{},[28794],{"type":32,"value":28795},"Normally, this is something you would solve on staging environment, by either creating a special setup, where you can access the information you need. But that is something that user does not do, so it does not resemble the nehavior of a real user. You can of course make sure that every part of the functionality works (which is good), but if you are like me, you’ll feel like that’s not enough.",{"type":27,"tag":45,"props":28797,"children":28799},{"id":28798},"enter-mailosaur",[28800],{"type":32,"value":28801},"Enter Mailosaur",{"type":27,"tag":28,"props":28803,"children":28804},{},[28805,28810],{"type":27,"tag":172,"props":28806,"children":28808},{"href":2464,"rel":28807},[696],[28809],{"type":32,"value":2468},{"type":32,"value":28811}," a service that creates an email inbox for you. What’s special about it is, that you can access it not only via their interface, but also via their plugin. All of email content and metadata can be viewed in plain text, HTML, or parsed to JSON, where you can view all links, attachments or images.",{"type":27,"tag":28,"props":28813,"children":28814},{},[28815,28817,28824],{"type":32,"value":28816},"My favourite part is the fact that you can ",{"type":27,"tag":172,"props":28818,"children":28821},{"href":28819,"rel":28820},"https://link.filiphric.com/mailosaur-cypress-plugin",[696],[28822],{"type":32,"value":28823},"wait for a specific message to arrive",{"type":32,"value":28825},". This means you can access all of that information within few milliseconds and then use them further in your test.",{"type":27,"tag":28,"props":28827,"children":28828},{},[28829],{"type":32,"value":28830},"With Mailosaur, you create an email server, where all emails with specific username land. You’ll end up with something like:",{"type":27,"tag":793,"props":28832,"children":28835},{"className":28833,"code":28834,"language":2250,"meta":5},[2248],"{{any string here}}.abcdefg@mailosaur.io\n",[28836],{"type":27,"tag":653,"props":28837,"children":28838},{"__ignoreMap":5},[28839],{"type":32,"value":28834},{"type":27,"tag":45,"props":28841,"children":28843},{"id":28842},"integrating-mailosaur-with-cypress",[28844],{"type":32,"value":28845},"Integrating Mailosaur with Cypress",{"type":27,"tag":28,"props":28847,"children":28848},{},[28849,28851,28858],{"type":32,"value":28850},"Mailosaur has integrations for all the main testing frameworks such as Cypress, Playwright, WebdriverIO, and ",{"type":27,"tag":172,"props":28852,"children":28855},{"href":28853,"rel":28854},"https://link.filiphric.com/mailosaur-tools",[696],[28856],{"type":32,"value":28857},"much more",{"type":32,"value":28859},". With Cypress, you can simply install the plugin, set it up and start testing your emails.",{"type":27,"tag":793,"props":28861,"children":28864},{"className":28862,"code":28863,"language":1084,"meta":5},[1082],"npm install cypress-mailosaur\n",[28865],{"type":27,"tag":653,"props":28866,"children":28867},{"__ignoreMap":5},[28868],{"type":32,"value":28863},{"type":27,"tag":28,"props":28870,"children":28871},{},[28872],{"type":32,"value":28873},"After installation, you need to import the plugin in your support file.",{"type":27,"tag":793,"props":28875,"children":28878},{"className":28876,"code":28877,"filename":5661,"language":1513,"meta":5},[1510],"import 'cypress-mailosaur'\n",[28879],{"type":27,"tag":653,"props":28880,"children":28881},{"__ignoreMap":5},[28882],{"type":32,"value":28877},{"type":27,"tag":28,"props":28884,"children":28885},{},[28886],{"type":32,"value":28887},"You also need to set up your API key in your Cypress config file, or make sure to have it in your environment variables so that it can be passed to the plugin. You can get your API key from the Mailosaur dashboard.",{"type":27,"tag":793,"props":28889,"children":28892},{"className":28890,"code":28891,"filename":5669,"language":3520,"meta":5},[3517],"import { defineConfig } from 'cypress'\n\nexport default defineConfig({\n  env: {\n    MAILOSAUR_API_KEY: 'your-mailosaur-api-key',\n  },\n})\n",[28893],{"type":27,"tag":653,"props":28894,"children":28895},{"__ignoreMap":5},[28896],{"type":32,"value":28891},{"type":27,"tag":28,"props":28898,"children":28899},{},[28900],{"type":32,"value":28901},"You should now have everything set up and ready to go. So let’s write a test.",{"type":27,"tag":793,"props":28903,"children":28906},{"className":28904,"code":28905,"language":3520,"meta":5},[3517],"const userEmail = 'email.abcdefg@mailosaur.io'\n\ndescribe('Reset password flow', () => {\n\n  it('Should receive an email and use reset password link', () => {\n\n    // trigger reset password for user\n    cy.visit('/reset-password')\n    cy.get('input[name=\"email\"]').type(userEmail)\n    cy.get('button[type=\"submit\"]').click()\n\n    cy.mailosaurGetMessage(\"abcdefg\", { // abcdefg is server name\n      sentTo:userEmail, \n      subject: 'Password reset'\n    }).then( response => {\n\n      // continue with test, open link\n      cy.visit(response.body.text.links[1])\n\n      // fill in and confirm new password\n      cy.get('input[name=\"password\"]').type(newPassword);\n\n      cy.get('button[type=\"submit\"]').click();\n\n      // land on desired page after confirming password reset\n      cy\n        .location('href')\n        .should('contains', '/login');\n\n      // login with my new password\n      // ...\n\n    })\n\n  });\n\n});\n",[28907],{"type":27,"tag":653,"props":28908,"children":28909},{"__ignoreMap":5},[28910],{"type":32,"value":28905},{"type":27,"tag":28,"props":28912,"children":28913},{},[28914],{"type":32,"value":28915},"After obtaining the reset password link, you can continue with your test journey. Opening link, setting up new password, and then finally logging in to application. This resembles real life behavior more closely, connecting and integrating each step.",{"title":5,"searchDepth":320,"depth":320,"links":28917},[28918,28919,28920],{"id":28739,"depth":320,"text":28742},{"id":28798,"depth":320,"text":28801},{"id":28842,"depth":320,"text":28845},"content:testing-email-flows-with-mailosaur:index.md","testing-email-flows-with-mailosaur/index.md","testing-email-flows-with-mailosaur/index",{"_path":28420,"_dir":5,"_draft":6,"_partial":6,"_locale":5,"title":28925,"description":28926,"date":28927,"published":10,"slug":28928,"tags":28929,"readingTime":28931,"body":28935,"_type":329,"_id":29402,"_source":331,"_file":29403,"_stem":29404,"_extension":334},"What psychology taught me about QA","Some of my psychology background has translated to my tech career. I hope to provide some principles which I hope to be valuable to you as well.","2019-03-26","what-psychology-taught-me-about-qa",[28505,28930],"psychology",{"text":12205,"minutes":28932,"time":28933,"words":28934},9.025,541500,1805,{"type":24,"children":28936,"toc":29395},[28937,28942,28947,28959,28965,28998,29010,29015],{"type":27,"tag":28,"props":28938,"children":28939},{},[28940],{"type":32,"value":28941},"I am a psychology graduate and I practiced psychology for a few years after my studies. After some time, I realized that being a therapist is not my vocation. There was an urge to help people inside me, but I realized that this was just not my path.",{"type":27,"tag":28,"props":28943,"children":28944},{},[28945],{"type":32,"value":28946},"I was always an IT enthusiast and I decided to steer my life path in this direction. After some time I landed a job at Slido, where I learned a great deal and where I am now team lead of our QA department.",{"type":27,"tag":28,"props":28948,"children":28949},{},[28950,28952,28957],{"type":32,"value":28951},"When people find out about my background in psychology, they are often surprised. But then they quickly add that it can actually help me a lot in my job. I agree with that. ",{"type":27,"tag":79,"props":28953,"children":28954},{},[28955],{"type":32,"value":28956},"I like to say that once you study psychology, you can never look at the world the same way. It builds up thought patterns that influence how you think.",{"type":32,"value":28958}," I thought I’d share a few of these patterns in this blog, hoping they will influence you in a positive way. They certainly did that for me in my role as a tester.",{"type":27,"tag":45,"props":28960,"children":28962},{"id":28961},"focus-on-peoples-motivation",[28963],{"type":32,"value":28964},"Focus on people’s motivation",{"type":27,"tag":28,"props":28966,"children":28967},{},[28968,28970,28975,28977,28982,28984,28989,28991,28996],{"type":32,"value":28969},"What drives people’s behavior? Freud said it is unresolved ",{"type":27,"tag":302,"props":28971,"children":28972},{},[28973],{"type":32,"value":28974},"conflicts from childhood",{"type":32,"value":28976},", Maslow talked about ",{"type":27,"tag":302,"props":28978,"children":28979},{},[28980],{"type":32,"value":28981},"hierarchy of needs",{"type":32,"value":28983},", Frankl said that it’s the ",{"type":27,"tag":302,"props":28985,"children":28986},{},[28987],{"type":32,"value":28988},"meaning",{"type":32,"value":28990}," that drives us and James said it’s learned ",{"type":27,"tag":302,"props":28992,"children":28993},{},[28994],{"type":32,"value":28995},"behavioral patterns",{"type":32,"value":28997},". Whichever it may be, the question of what drives us was burning enough to create a myriad schools of thought. It goes without saying that it is an important question (maybe even the most important in psychology). And it stuck with me when I started testing software.",{"type":27,"tag":28,"props":28999,"children":29000},{},[29001,29003,29008],{"type":32,"value":29002},"Looking at a new feature, ",{"type":27,"tag":79,"props":29004,"children":29005},{},[29006],{"type":32,"value":29007},"I listen carefully to what the product intention is, but more importantly, I look at motivations of the end user",{"type":32,"value":29009},". It is often really hard, but also really valuable to put yourself in someone else’s shoes and see the world from their perspective. In my view, it is vital that testers do that. Just for a minute, imagine users’ problems as your own. It helps you to think as someone who needs that problem solved. It unfolds the motivation that manifests in user behavior. It helps you to understand that behavior.",{"type":27,"tag":28,"props":29011,"children":29012},{},[29013],{"type":32,"value":29014},"I love to demonstrate this via this joke:",{"type":27,"tag":5872,"props":29016,"children":29018},{"id":29017},"1068615953989087232",[29019,29024,29030,29035,29047,29059,29086,29095,29105,29110,29122,29128,29140,29155,29160,29165,29175,29185,29195,29207,29217,29227,29232,29238,29248,29253,29258,29263,29316,29321,29327,29332,29337,29342,29354,29363,29368,29373],{"type":27,"tag":28,"props":29020,"children":29021},{},[29022],{"type":32,"value":29023},"Some of us know the feeling of when this happens. Users’ needs are often different from our intentions. Those needs, however, need to be taken into account when testing. As testers we should make our best effort not to be blind to those needs. If we try to focus solely on making a product that is “perfect” in our own view, we risk that it may not be so great in the eyes of our users.",{"type":27,"tag":45,"props":29025,"children":29027},{"id":29026},"consider-carefully-what-people-say-and-feel",[29028],{"type":32,"value":29029},"Consider carefully what people say and feel",{"type":27,"tag":28,"props":29031,"children":29032},{},[29033],{"type":32,"value":29034},"I remember back in university when we did a therapy role play and I was in the role of therapist for the first time. Sitting across was my colleague questioning my competence to counsel as a family therapist while being childless. This was a tough setting. While doubts and accusations kept on coming, I tried to defend myself. Of course, it was a big failure, but it was also a good impulse for a discussion on what went wrong.",{"type":27,"tag":28,"props":29036,"children":29037},{},[29038,29040,29045],{"type":32,"value":29039},"Our Professor told me: „You were trying to defend your ego, being totally oblivious to what the person sitting in front of you was *really *trying to say. Step outside of yourself and try to ",{"type":27,"tag":79,"props":29041,"children":29042},{},[29043],{"type":32,"value":29044},"shift your focus to the other person",{"type":32,"value":29046},".“",{"type":27,"tag":28,"props":29048,"children":29049},{},[29050,29052,29057],{"type":32,"value":29051},"This made me realize that instead of trying to prove myself, I should have listened to the doubts and anger of the person sitting in front of me I could have found out — that ",{"type":27,"tag":79,"props":29053,"children":29054},{},[29055],{"type":32,"value":29056},"she was trying to tell me about her fear from not being understood",{"type":32,"value":29058},". That was a good lesson to learn.",{"type":27,"tag":28,"props":29060,"children":29061},{},[29062,29064,29069,29071,29076,29078,29085],{"type":32,"value":29063},"We testers often try to prove our value to the world (and maybe to ourselves, too). Understandably so, since it is not always that obvious where the value of our work comes from. As a result, we create some cool names for ourselves, like QA. Although I use the ",{"type":27,"tag":79,"props":29065,"children":29066},{},[29067],{"type":32,"value":29068},"QA abbreviation too, I don’t really like the meaning — quality assurance. To me, it completely removes some essential elements of the context — mostly people and their emotions.",{"type":32,"value":29070}," I often find myself going back to that classroom and think about what is *really *being said. When we work on a product, how does it ",{"type":27,"tag":302,"props":29072,"children":29073},{},[29074],{"type":32,"value":29075},"really",{"type":32,"value":29077}," impact a user’s life? I think ",{"type":27,"tag":172,"props":29079,"children":29082},{"href":29080,"rel":29081},"https://www.youtube.com/watch?v=Uz6uZ5xvEVw",[696],[29083],{"type":32,"value":29084},"Isabel Evans said it best",{"type":32,"value":1474},{"type":27,"tag":1029,"props":29087,"children":29088},{},[29089],{"type":27,"tag":45,"props":29090,"children":29092},{"id":29091},"we-dont-experience-software-we-experience-emotions",[29093],{"type":32,"value":29094},"„We don’t experience software. We experience emotions.“",{"type":27,"tag":28,"props":29096,"children":29097},{},[29098,29103],{"type":27,"tag":79,"props":29099,"children":29100},{},[29101],{"type":32,"value":29102},"Shift the focus from software to people.",{"type":32,"value":29104}," It is vital to think about those emotions and be able to name them. Don’t focus solely on assuring quality, but also on evoking the right emotions when using your product.",{"type":27,"tag":28,"props":29106,"children":29107},{},[29108],{"type":32,"value":29109},"What do I mean by that? I can give you an example from Slido. It is being used at live events, presentations and in company meetings. There is already a variety of emotions present in that scenario. Stress, time pressure, battle with unexpected situations, urge to fulfill various expectations etc. When entering this environment, we can either be helpful with our product, or cause a big frustration. Even a small bug can result in losing a customer’s trust.",{"type":27,"tag":28,"props":29111,"children":29112},{},[29113,29115,29120],{"type":32,"value":29114},"This is an angle I really need to work with when testing out new features, writing automation etc. ",{"type":27,"tag":79,"props":29116,"children":29117},{},[29118],{"type":32,"value":29119},"Looking at user emotions helps me look beyond what’s functioning and what’s not.",{"type":32,"value":29121}," It helps me understand what happens in a user’s head when they are using a certain feature.",{"type":27,"tag":45,"props":29123,"children":29125},{"id":29124},"acknowledge-and-question-your-assumptions",[29126],{"type":32,"value":29127},"Acknowledge and question your assumptions",{"type":27,"tag":28,"props":29129,"children":29130},{},[29131,29133,29138],{"type":32,"value":29132},"When talking to someone about their personal problem, it is often tempting to share your own experience. It actually makes a lot of sense. If you went through a similar situation, you want to offer a solution that worked for you — maybe it will help. ",{"type":27,"tag":79,"props":29134,"children":29135},{},[29136],{"type":32,"value":29137},"The big problem with that is that your experience is totally different from the experiences of others.",{"type":32,"value":29139}," There is no better way to help someone with a problem than to guide them and to help them find a solution on their own*. Most of the time, advice does not work. There’s actually a nice little joke about this in the psychologists’ world:",{"type":27,"tag":1029,"props":29141,"children":29142},{},[29143,29149],{"type":27,"tag":45,"props":29144,"children":29146},{"id":29145},"q-what-is-the-most-common-answer-to-an-advice",[29147],{"type":32,"value":29148},"Q: What is the most common answer to an advice?",{"type":27,"tag":45,"props":29150,"children":29152},{"id":29151},"a-yes-but",[29153],{"type":32,"value":29154},"A: „Yes, but…“",{"type":27,"tag":28,"props":29156,"children":29157},{},[29158],{"type":32,"value":29159},"Psychologists barely give you any advice in therapy. That is because they are very aware of the fact that their own experience is in no way applicable to your own situation.",{"type":27,"tag":28,"props":29161,"children":29162},{},[29163],{"type":32,"value":29164},"We all think we can be objective (to a certain extent), but we often fall into a trap with our own biases. We are all using assumptions, and we are doing that daily:",{"type":27,"tag":28,"props":29166,"children":29167},{},[29168,29173],{"type":27,"tag":302,"props":29169,"children":29170},{},[29171],{"type":32,"value":29172},"„Oh I know what this is about.“",{"type":32,"value":29174}," — Seeing a bug report for the first time",{"type":27,"tag":28,"props":29176,"children":29177},{},[29178,29183],{"type":27,"tag":302,"props":29179,"children":29180},{},[29181],{"type":32,"value":29182},"„Everyone’s going to love this!“",{"type":32,"value":29184}," — Finishing testing of a feature",{"type":27,"tag":28,"props":29186,"children":29187},{},[29188,29193],{"type":27,"tag":302,"props":29189,"children":29190},{},[29191],{"type":32,"value":29192},"„No one is going to do that.“",{"type":32,"value":29194}," — Waving hand at a benign issue we just don’t have enough time to fix before release",{"type":27,"tag":28,"props":29196,"children":29197},{},[29198,29200,29205],{"type":32,"value":29199},"Sound familiar? I remember many situations where I would slap my forehead after I realized I was wrong with my assumptions. The biggest problem was that ",{"type":27,"tag":79,"props":29201,"children":29202},{},[29203],{"type":32,"value":29204},"I didn’t realize they were assumptions",{"type":32,"value":29206}," and thought of them as facts.",{"type":27,"tag":28,"props":29208,"children":29209},{},[29210,29215],{"type":27,"tag":79,"props":29211,"children":29212},{},[29213],{"type":32,"value":29214},"We all have assumptions and it is impossible to not have them.",{"type":32,"value":29216}," You may attempt to get rid of them, but you might end up just fooling yourself into thinking that you did.",{"type":27,"tag":28,"props":29218,"children":29219},{},[29220,29222],{"type":32,"value":29221},"That does not mean, though, that you cannot do anything about them. ",{"type":27,"tag":79,"props":29223,"children":29224},{},[29225],{"type":32,"value":29226},"The best thing you can do is to acknowledge you have them.",{"type":27,"tag":28,"props":29228,"children":29229},{},[29230],{"type":32,"value":29231},"The upside of knowing the difference between fact and opinion is that it enables you to make good decisions. I bet that in the life of a tester there are many crucial decisions that have a deep impact on the product.",{"type":27,"tag":45,"props":29233,"children":29235},{"id":29234},"more-communication-better-communication",[29236],{"type":32,"value":29237},"More communication != better communication",{"type":27,"tag":28,"props":29239,"children":29240},{},[29241,29243],{"type":32,"value":29242},"Testers work closely with developers. While a new feature is being developed, they spend most of their day giving feedback on those features. ",{"type":27,"tag":79,"props":29244,"children":29245},{},[29246],{"type":32,"value":29247},"The way you give that feedback can make a big difference.",{"type":27,"tag":28,"props":29249,"children":29250},{},[29251],{"type":32,"value":29252},"When dealing with married couples’ problems, there is a false assumption that *communication solves every problem. *Actually, if your communication is aggressive, not only will it fail to solve any problems, but also it could actually make things much worse.",{"type":27,"tag":28,"props":29254,"children":29255},{},[29256],{"type":32,"value":29257},"And yes, I just compared developers and testers to married couples.",{"type":27,"tag":28,"props":29259,"children":29260},{},[29261],{"type":32,"value":29262},"Good communication has a big impact on the face of your relationship though. Whether talking marriage or product team relationships, you can either build them or destroy them. Here are some suggestions on how to make sure you won’t do the latter:",{"type":27,"tag":851,"props":29264,"children":29265},{},[29266,29271,29276,29281,29286,29291,29296,29301,29306,29311],{"type":27,"tag":109,"props":29267,"children":29268},{},[29269],{"type":32,"value":29270},"Words matter. Be careful on how you use them.",{"type":27,"tag":109,"props":29272,"children":29273},{},[29274],{"type":32,"value":29275},"Remove any hint of judgement (in other words, don’t tell developers their baby is ugly)",{"type":27,"tag":109,"props":29277,"children":29278},{},[29279],{"type":32,"value":29280},"Be clear.",{"type":27,"tag":109,"props":29282,"children":29283},{},[29284],{"type":32,"value":29285},"Make sure you and your colleague are on the same page.",{"type":27,"tag":109,"props":29287,"children":29288},{},[29289],{"type":32,"value":29290},"Actively work on improving your communication skills.",{"type":27,"tag":109,"props":29292,"children":29293},{},[29294],{"type":32,"value":29295},"Don’t assume that everyone approaches the problem in the same way you do. Be aware of different contexts of the same problem.",{"type":27,"tag":109,"props":29297,"children":29298},{},[29299],{"type":32,"value":29300},"Try to be nice.",{"type":27,"tag":109,"props":29302,"children":29303},{},[29304],{"type":32,"value":29305},"You both care. If you don’t see the good intention, at least try to assume it until you find it.",{"type":27,"tag":109,"props":29307,"children":29308},{},[29309],{"type":32,"value":29310},"Don’t try to win. Swallow your ego and be humble. We are people.",{"type":27,"tag":109,"props":29312,"children":29313},{},[29314],{"type":32,"value":29315},"Choose your timing carefully.",{"type":27,"tag":28,"props":29317,"children":29318},{},[29319],{"type":32,"value":29320},"Applies for marriage too.",{"type":27,"tag":45,"props":29322,"children":29324},{"id":29323},"there-is-no-stupid-feedback-only-some-is-poorly-constructed",[29325],{"type":32,"value":29326},"There is no stupid feedback, only some is poorly constructed",{"type":27,"tag":28,"props":29328,"children":29329},{},[29330],{"type":32,"value":29331},"I recently got into a fight with my colleague. It was a very unfortunate course of events that resulted in waving arms and shouting. I forgot many of my principles that day. All because of a wrong choice of words, which hurt me to my very core. Although meant as a feedback, my first perception was how incredibly stupid and insulting those words were.",{"type":27,"tag":28,"props":29333,"children":29334},{},[29335],{"type":32,"value":29336},"Whose problem that was?",{"type":27,"tag":28,"props":29338,"children":29339},{},[29340],{"type":32,"value":29341},"Mine.",{"type":27,"tag":28,"props":29343,"children":29344},{},[29345,29347,29352],{"type":32,"value":29346},"Underneath a harsh message, there was a valuable feedback which I didn’t listen to. Instead of focusing on *how *the message was delivered, I should have focused on ",{"type":27,"tag":302,"props":29348,"children":29349},{},[29350],{"type":32,"value":29351},"what",{"type":32,"value":29353}," is being said. It’s easier said than done, but there is actually a good principle that helps unravel even a very poorly constructed feedback:",{"type":27,"tag":1029,"props":29355,"children":29356},{},[29357],{"type":27,"tag":45,"props":29358,"children":29360},{"id":29359},"problems-are-negative-images-of-our-values",[29361],{"type":32,"value":29362},"Problems are negative images of our values.",{"type":27,"tag":28,"props":29364,"children":29365},{},[29366],{"type":32,"value":29367},"What that means is that when someone talks about a problem, you can identify a value that is actually held high by that person. E.g. if you hear someone complaining about everyone being late to meetings, you can actually tell that the person values efficiency. If you hear someone complain about number of bugs on product, you can actually tell that the person cares deeply about quality. The list goes on. Next time, if you are annoyed by someone complaining, try to listen carefully and see if you can identify the value they hold precious.",{"type":27,"tag":28,"props":29369,"children":29370},{},[29371],{"type":32,"value":29372},"With this in mind, you will be able to see an opportunity to gain even from clumsiest feedback and learn more. I’m not saying it’s easy, you often get overwhelmed by emotions if it’s personal (as did I). But if you take both good and bad feedback, you’ll get twice the opportunity to grow.",{"type":27,"tag":1029,"props":29374,"children":29375},{},[29376],{"type":27,"tag":28,"props":29377,"children":29378},{},[29379,29381,29386,29388,29393],{"type":32,"value":29380},"*notice that I separate guidance from advice. I view ",{"type":27,"tag":302,"props":29382,"children":29383},{},[29384],{"type":32,"value":29385},"guidance",{"type":32,"value":29387}," as cooperative and ",{"type":27,"tag":302,"props":29389,"children":29390},{},[29391],{"type":32,"value":29392},"advice",{"type":32,"value":29394}," as authoritative, although I don’t mean to say that you cannot advice cooperatively and guide authoritatively",{"title":5,"searchDepth":320,"depth":320,"links":29396},[29397,29398,29399,29400,29401],{"id":28961,"depth":320,"text":28964},{"id":29026,"depth":320,"text":29029},{"id":29124,"depth":320,"text":29127},{"id":29234,"depth":320,"text":29237},{"id":29323,"depth":320,"text":29326},"content:what-psychology-taught-me-about-qa:index.md","what-psychology-taught-me-about-qa/index.md","what-psychology-taught-me-about-qa/index",[],1775327317408]