<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marcelo C.</title>
    <description>The latest articles on DEV Community by Marcelo C. (@marcelo_sqe).</description>
    <link>https://dev.to/marcelo_sqe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1805681%2Fe450e775-428b-46c4-9bb6-869551761185.jpg</url>
      <title>DEV Community: Marcelo C.</title>
      <link>https://dev.to/marcelo_sqe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcelo_sqe"/>
    <language>en</language>
    <item>
      <title>How I leveraged Claude Code skills to help me migrate Cypress 16 deprecations in time</title>
      <dc:creator>Marcelo C.</dc:creator>
      <pubDate>Fri, 03 Jul 2026 15:05:58 +0000</pubDate>
      <link>https://dev.to/cypress/how-i-leveraged-claude-code-skills-to-help-me-migrate-cypress-16-deprecations-in-time-37ff</link>
      <guid>https://dev.to/cypress/how-i-leveraged-claude-code-skills-to-help-me-migrate-cypress-16-deprecations-in-time-37ff</guid>
      <description>&lt;p&gt;Have you ever stared at a deprecation warning scrolling by in your terminal and thought &lt;em&gt;"eh, future me will deal with that"&lt;/em&gt;? We all have! But here's the thing about future you: he eventually shows up. And in my case, he showed up wearing a shirt that said &lt;strong&gt;Cypress v16&lt;/strong&gt;, holding a changelog, and pointing at the line that says &lt;code&gt;Cypress.env()&lt;/code&gt; will be &lt;strong&gt;fully removed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxw1gx0ztg6645q543ozf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxw1gx0ztg6645q543ozf.png" alt="Cypress terminal warning before migration" width="799" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Warning in 15.10. Gone in 16. And sprinkled across every config, page object, and spec file in the massive E2E repository I maintain at my work monorepo. Configs, support files, fluent page-object chains — everywhere.&lt;/p&gt;

&lt;p&gt;So let's get to the grain here: even though Cypress has a &lt;a href="https://docs.cypress.io/app/references/migration-guide" rel="noopener noreferrer"&gt;great documentation about it&lt;/a&gt;, I didn't want to execute a find-and-replace job manually, and I also wanted to move fast. So instead of pasting a prompt into a chat window forty times, I did something better: I turned the migration playbook itself into a &lt;strong&gt;Claude Code Skill&lt;/strong&gt; — a reusable, self-triggering unit of expertise that lives in my machine and outlives this one migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cypress.env() had to die
&lt;/h2&gt;

&lt;p&gt;If you've written E2E tests, you know &lt;code&gt;Cypress.env()&lt;/code&gt;. Synchronous, available everywhere, dead simple.&lt;/p&gt;

&lt;p&gt;And that simplicity was exactly the problem: &lt;strong&gt;it hydrated the entire env object straight into the browser runtime&lt;/strong&gt;. Every value. Including the ones a given test never reads. Your admin password, your API tokens — all of it, sitting in browser-accessible memory, one &lt;code&gt;window&lt;/code&gt; inspection away, in every single spec.&lt;/p&gt;

&lt;p&gt;Easy to use? Absolutely. A credential-leaking surface baked into your test runner? Also absolutely.&lt;/p&gt;

&lt;p&gt;The v16 API splits the concern into two deliberately asymmetric halves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cy.env(['key']).then(...)&lt;/code&gt;&lt;/strong&gt; — asynchronous, resolved &lt;strong&gt;Node-side&lt;/strong&gt;, strictly read-only. Values never touch the browser context until you explicitly consume them inside the callback. This is where credentials and tokens live now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Cypress.expose('key')&lt;/code&gt;&lt;/strong&gt; — synchronous, a drop-in replacement, but only for values you've &lt;em&gt;explicitly opted in&lt;/em&gt; to exposing via a new top-level &lt;code&gt;expose:&lt;/code&gt; block in your config (a sibling to &lt;code&gt;env:&lt;/code&gt;, not nested inside it).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the design: the safe path is ergonomic, the sensitive path is deliberately awkward. That asymmetry is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The strategy: teaching the AI a classification heuristic
&lt;/h2&gt;

&lt;p&gt;Doing a mindless regex sweep across thousands of files would have been fast — and catastrophically wrong, because every single call site needed a &lt;strong&gt;classification decision&lt;/strong&gt; first. So instead of doing the work 3,000 times, I did it once: I encoded a strict heuristic and let Claude Code apply it mechanically.&lt;/p&gt;

&lt;p&gt;The heuristic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does the value contain a password, a username paired with a password, or any secret?&lt;/strong&gt; → It's sensitive. It &lt;strong&gt;stays&lt;/strong&gt; in the &lt;code&gt;env:&lt;/code&gt; block and every read site gets rewritten to the async &lt;code&gt;cy.env([...]).then(...)&lt;/code&gt; form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it a feature flag (our &lt;code&gt;idp_active&lt;/code&gt;, &lt;code&gt;xbrl_active&lt;/code&gt;), a locale, a timeout, a base URL fragment?&lt;/strong&gt; → It's public. It &lt;strong&gt;moves&lt;/strong&gt; to the &lt;code&gt;expose:&lt;/code&gt; block and the read becomes a one-line swap to &lt;code&gt;Cypress.expose()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rule 2 is boring. Rule 1 is where the migration earns its war stories.&lt;/p&gt;

&lt;h2&gt;
  
  
  From prompt to Skill: making the knowledge permanent
&lt;/h2&gt;

&lt;p&gt;The Cypress team actually ships an official starting point for this — the &lt;a href="https://github.com/cypress-io/ai-toolkit/blob/main/prompt-library/cypress-v16-migration/remove-cypress-env-usage.md" rel="noopener noreferrer"&gt;remove-cypress-env-usage prompt from the Cypress AI Toolkit&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagmdeoz0yidhduh5xcrv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fagmdeoz0yidhduh5xcrv.png" alt="Real prompt from Cypress team" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's good. But a prompt in a library has two problems: &lt;strong&gt;you have to remember it exists&lt;/strong&gt;, and &lt;strong&gt;you have to re-paste it every session&lt;/strong&gt;. A Claude Code Skill solves both. A Skill is just a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; — a markdown file with a small YAML frontmatter (name + description) followed by the instructions. Claude Code scans the descriptions on every task and &lt;strong&gt;auto-loads the skill when the context matches&lt;/strong&gt;, so the moment I say "migrate this spec off Cypress.env", the whole playbook is already in its head. No copy-paste, no drift between sessions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9pa2uml0cwyjoch7ber0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9pa2uml0cwyjoch7ber0.png" alt="Prompt set as Claude skill" width="800" height="194"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/
└── .claude/
    └── skills/
        └── cypress-v16-migration/
            └── SKILL.md    # frontmatter + the full migration playbook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But here's the important part — I didn't just copy the official prompt into a file. I &lt;strong&gt;transformed&lt;/strong&gt; it, layering in everything the generic version can't know about &lt;em&gt;my&lt;/em&gt; repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;classification heuristic&lt;/strong&gt; above, with our real flag names (&lt;code&gt;idp_active&lt;/code&gt;, &lt;code&gt;xbrl_active&lt;/code&gt;) as worked examples, so it never has to guess what "sensitive" means in our codebase.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;command-queue traps&lt;/strong&gt; you're about to read below, written as explicit "never do this / always do this" rules — because I hit them once and never wanted the AI to reproduce them.&lt;/li&gt;
&lt;li&gt;Our repo topology: &lt;strong&gt;all four active config files&lt;/strong&gt; that need the &lt;code&gt;expose:&lt;/code&gt; block and the final flag flip, so nothing gets a partial migration.&lt;/li&gt;
&lt;li&gt;The verification sequence (grep → flag → CI) as a mandatory checklist the skill must run before declaring victory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generic prompt is the textbook; the Skill is the textbook annotated by someone who took the exam. And since it's a personal skill (kept local, not committed to the repo), I can keep sharpening it without a PR review debate about my prompt-engineering style. 😄&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the plot thickens 🕵️
&lt;/h2&gt;

&lt;p&gt;You might be thinking: &lt;em&gt;"Ok, this is still just a glorified find-and-replace with two output templates. What's the big deal?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The big deal is &lt;strong&gt;the Cypress command queue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Swapping a synchronous read for an asynchronous one &lt;em&gt;inside a fluent page-object chain&lt;/em&gt; changes the execution semantics of everything around it. Take our global &lt;code&gt;cy.login()&lt;/code&gt; command — the one every spec in the codebase depends on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BEFORE — synchronous, credentials resolved at enqueue time&lt;/span&gt;
&lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;loginPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;typeUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;typePass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goToHome&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Naive migration attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BROKEN — do you see it?&lt;/span&gt;
&lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;loginPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;typeUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;typePass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goToHome&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 💥&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks reasonable. It's a time bomb. Here's why:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap #1 — silent reordering.&lt;/strong&gt; Cypress enqueues commands synchronously as JavaScript executes, but runs them later. That trailing &lt;code&gt;cy.goToHome()&lt;/code&gt; gets &lt;strong&gt;enqueued before the &lt;code&gt;.then()&lt;/code&gt; callback ever fires&lt;/strong&gt; — and any &lt;em&gt;non-Cypress&lt;/em&gt; code after the block (variable assignments, page-object state mutations) executes immediately, before the credentials even exist. No error. No warning. Just your test quietly doing things in the wrong order. This is, by far, the easiest mistake to make in the entire migration, and grep will never find it for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap #2 — &lt;code&gt;cy.session()&lt;/code&gt; needs a resolved ID.&lt;/strong&gt; The session ID argument is evaluated &lt;em&gt;at call time&lt;/em&gt;, not at run time. You can't hand it a promise or a pending value. So the entire &lt;code&gt;cy.session(...)&lt;/code&gt; invocation — not just its setup callback — has to move &lt;strong&gt;inside&lt;/strong&gt; the &lt;code&gt;cy.env().then()&lt;/code&gt; callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CORRECT — everything downstream of the credentials lives inside the callback&lt;/span&gt;
&lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;loginPage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;typeUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;typePass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goToHome&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the payoff of years of discipline: because every login flow was registered through &lt;code&gt;Cypress.Commands.add&lt;/code&gt;, this entire architectural shift happened &lt;strong&gt;behind the command boundary&lt;/strong&gt;. Thousands of &lt;code&gt;cy.login()&lt;/code&gt; call sites across the specs didn't change by a single character. The abstraction held. This, kids, is why you wrap your login logic in a custom command instead of copy-pasting it into 5k tests (ask me how I know 😉).&lt;/p&gt;

&lt;h2&gt;
  
  
  The final lockdown 🔒
&lt;/h2&gt;

&lt;p&gt;A migration you can't &lt;em&gt;verify&lt;/em&gt; is just a migration you &lt;em&gt;hope&lt;/em&gt; worked. Cypress v16 gives you a kill switch for exactly this: &lt;code&gt;allowCypressEnv: false&lt;/code&gt;. With it set, any surviving &lt;code&gt;Cypress.env()&lt;/code&gt; call becomes a &lt;strong&gt;hard runtime failure&lt;/strong&gt; — not a warning, a crash.&lt;/p&gt;

&lt;p&gt;My verification sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;grep&lt;/code&gt; the branch for live &lt;code&gt;Cypress.env(&lt;/code&gt; calls → zero hits (comments and the migration doc don't count).&lt;/li&gt;
&lt;li&gt;Flip &lt;code&gt;allowCypressEnv: false&lt;/code&gt; across &lt;strong&gt;all four&lt;/strong&gt; active config files — no partial lockdowns, no "we'll enable it on that config later."&lt;/li&gt;
&lt;li&gt;Ship it to CI and let the full E2E stages be the judge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Green across the board. And here's the subtle part that made it safe: we only changed the &lt;strong&gt;read API&lt;/strong&gt;, never the &lt;strong&gt;injection mechanism&lt;/strong&gt;. Every CI secret, every &lt;code&gt;--env&lt;/code&gt; CLI override, every pipeline variable kept flowing exactly as before — they just land in a place the browser can no longer see by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The commit log tells the real story 📜
&lt;/h2&gt;

&lt;p&gt;Want proof that the Skill approach actually moved fast? Read the git history like a detective:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First PR:
Tue 11:27  Migration to v16
Tue 12:11  [E2E] Migration to Cypress v16 architecture
Tue 14:44  Updating Cypress env flag
Wed 16:04  Updating Typescript types

Fixing login issue:
Thu 10:23  [E2E] Fixing idp override with v16 Cy migration 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at Tuesday morning: the &lt;strong&gt;bulk rewrite of the entire architecture landed in about 45 minutes&lt;/strong&gt;, between the first pass and the structured &lt;code&gt;[E2E]&lt;/code&gt; commit. That's thousands of call sites, classified and rewritten, in less time than a sprint planning meeting. Two hours later, &lt;code&gt;allowCypressEnv: false&lt;/code&gt; was already flipped — the kill switch armed on day one.&lt;/p&gt;

&lt;p&gt;Then comes the honest part, the part most migration posts conveniently forget: &lt;strong&gt;the long tail&lt;/strong&gt;. Wednesday was TypeScript's turn — &lt;code&gt;cy.env()&lt;/code&gt; returning a promise-like of a keyed object meant the old string-based typings had to be reshaped, so the compiler could catch a mistyped key at build time instead of CI catching it at run time.&lt;/p&gt;

&lt;p&gt;And Thursday? Thursday delivered the one trap the Skill didn't know about yet — &lt;strong&gt;Trap #3, the runtime override&lt;/strong&gt;. One special spec family runs against a different server with IDP/SSO enabled, and it used to flip that on with a runtime &lt;code&gt;Cypress.env('idp_active', true)&lt;/code&gt; in a &lt;code&gt;beforeEach&lt;/code&gt;. In v16, &lt;code&gt;idp_active&lt;/code&gt; now lives in the &lt;code&gt;expose:&lt;/code&gt; block — and exposed values are &lt;strong&gt;resolved from config, read-only at runtime&lt;/strong&gt;. The old setter didn't crash; it just silently did nothing, and the spec calmly logged into the wrong server. The fix was moving the override where v16 wants it: into the spec-level config override, resolved &lt;em&gt;before&lt;/em&gt; the test runs, not mutated &lt;em&gt;during&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;Guess what happened next? That trap went straight into the &lt;code&gt;SKILL.md&lt;/code&gt;. The next migration Claude Code runs already knows about runtime overrides of exposed values. &lt;strong&gt;That's the whole point of a Skill over a prompt: it compounds.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A breaking-change migration at this scale isn't a refactor problem, it's a &lt;strong&gt;classification + execution-semantics&lt;/strong&gt; problem. The AI didn't make the hard decisions — the heuristic did. The AI just made applying that heuristic across thousands of files not cost me a month of my life. And by encoding it as a Skill instead of a throwaway prompt, every trap I hit made the &lt;em&gt;next&lt;/em&gt; migration cheaper.&lt;/p&gt;

&lt;p&gt;What about you? Have you ever had to architect a massive, breaking-change migration across thousands of tests? What was the trap that almost got you — and did your abstractions hold when it mattered? Leave me a comment, I'd love to hear!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cypress</category>
      <category>e2etesting</category>
    </item>
    <item>
      <title>How I stopped declaring login in each of my 5k tests</title>
      <dc:creator>Marcelo C.</dc:creator>
      <pubDate>Fri, 27 Feb 2026 19:26:44 +0000</pubDate>
      <link>https://dev.to/cypress/how-i-stopped-declaring-login-in-each-of-my-5k-tests-37km</link>
      <guid>https://dev.to/cypress/how-i-stopped-declaring-login-in-each-of-my-5k-tests-37km</guid>
      <description>&lt;p&gt;Have you ever encountered a testing codebase that many portions are repeated over and over? We all have! Of course, I could be talking about DRY princples (Don't Repeat Yourself), but lets keep that aside for now and focus on a Cypress trick up it's sleeve that can go unnoticed for many senior devs: the global hooks.&lt;/p&gt;

&lt;p&gt;And what do I mean by "global" hooks? They're called like that because you only declare them once and they're applied to all your tests instantly. &lt;/p&gt;

&lt;p&gt;So let's get to the grain here: when installing Cypress it already comes with a file at created at &lt;code&gt;cypress/support/e2e.&amp;lt;ts|js&amp;gt;&lt;/code&gt; level. This is usually where you declare some important commands &lt;em&gt;imports&lt;/em&gt; that your E2E testing will need to access and run properly.&lt;/p&gt;

&lt;p&gt;But it's also responsible for adding let's say &lt;em&gt;before&lt;/em&gt;, &lt;em&gt;beforeEach&lt;/em&gt;, or &lt;em&gt;after&lt;/em&gt;, &lt;em&gt;afterEach&lt;/em&gt; hooks that will be applied by all your tests. This can be responsible for login hooks, clean-ups to the database after the tests run, screenshots resolutions configuration -- a million of possibilities here.&lt;/p&gt;

&lt;p&gt;I guess that the "global hooks" makes more sense now, right? &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The challenge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At my workplace, I encountered the command to login, called &lt;code&gt;cy.login&lt;/code&gt;, declared in each of the 5.634 tests that we have. For a while it bothered me a lot, because I always wanted to remove this codelines, and make my life easier with a simple &lt;code&gt;e2e.ts&lt;/code&gt; file that handled all existing and new login logic for future tests.&lt;/p&gt;

&lt;p&gt;But if I knew how it worked, why didn't I just do it already? &lt;/p&gt;

&lt;p&gt;Becuase, for the first time (for me), I was dealing with a really complex system: a big chunk of the legacy tests ran with &lt;code&gt;testIsolation: false&lt;/code&gt;. That meant that they only login once, and the &lt;code&gt;it&lt;/code&gt; blocks don't load a new baseURL after each one is done. They do it all in one session.&lt;/p&gt;

&lt;p&gt;Because? Well, that would be another story, so let's just accept their done like that for "system requirements" at the time.&lt;/p&gt;

&lt;p&gt;Ok, so I basically needed:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;before() → cy.login()&lt;br&gt;
beforeEach() → cy.login() only if testIsolation is true&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Easy right? Not yet. The complexity resides because different spec families have different needs: different credentials, different environments, different session strategies, and different testIsolation settings. &lt;/p&gt;

&lt;p&gt;There are two needs here to login:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which login method?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cy.login(): Default specs with standard app with default credential.&lt;/li&gt;
&lt;li&gt;cy.loginDemo(): Presentation specs with different environment, likely SSO or different credentials.&lt;/li&gt;
&lt;li&gt;Custom (own login): Need specific credentials or totally different E2E tests outside enviroment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When to login? (before vs beforeEach)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is driven by Cypress's testIsolation setting:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;testIsolation: true&lt;/code&gt; — Cypress clears browser state (cookies, storage) between each test. Session is lost → must re-login before each test.&lt;br&gt;
&lt;code&gt;testIsolation: false&lt;/code&gt; — Cookies persist across tests in the same spec → login once is enough.&lt;/p&gt;

&lt;p&gt;Now, here is where the plot thickens. I had to declare folders, tests, paths that needed to be skipped (or not), because on the same folder I had &lt;code&gt;testIsolation: true/false&lt;/code&gt;. So, follow me along:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Legacy specs (/legacy/ folder)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;before()&lt;/code&gt; → skip entirely&lt;br&gt;
&lt;code&gt;beforeEach()&lt;/code&gt; → only call cy.login() if testIsolation is true&lt;/p&gt;

&lt;p&gt;Legacy specs use specific credentials. If the global before() called cy.login() first, cy.session() would cache the wrong credentials, causing 500 errors when the spec then tries to login with different ones. So global before() is completely skipped. In beforeEach(), it only re-validates when state is actually cleared (testIsolation: true).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Training portal specs (/training/ folder) + Custom login specs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;before()&lt;/code&gt; → skip entirely&lt;br&gt;
&lt;code&gt;beforeEach()&lt;/code&gt; → skip entirely&lt;/p&gt;

&lt;p&gt;These specs manage their own login end-to-end. The global hooks stay completely out of the way. Some tests likely test login flows themselves or use role-specific credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. BEFORE_LOGIN_DEMO_SPECS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;before()&lt;/code&gt; → &lt;code&gt;cy.loginDemo() + cy.goToHome()&lt;/code&gt;&lt;br&gt;
&lt;code&gt;beforeEach()&lt;/code&gt; → skip (already logged in)&lt;/p&gt;

&lt;p&gt;Login once per spec, reused across all tests. These are demo/presentation specs where re-logging in per test would be slow and unnecessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. BEFORE_EACH_LOGIN_DEMO_SPECS&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;&lt;code&gt;before()&lt;/code&gt; → nothing&lt;br&gt;
&lt;code&gt;beforeEach() → cy.loginDemo() + cy.goToHome()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Similar to above but login is repeated per test because &lt;code&gt;testIsolation: true&lt;/code&gt;, the state is cleared between tests, so they must re-login each time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. LOGIN_DEMO_SPECIAL_SPECS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;before() → nothing&lt;br&gt;
beforeEach() → override baseUrl + timeout + idp_active env, then cy.loginDemo()&lt;/p&gt;

&lt;p&gt;This spec runs against a completely different server, with an IDP/SSO active flag and a much longer timeout. The config must be set before each test because test isolation may reset Cypress config state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Default specs (everything else, A.K.A the MOST important logic!)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;before() → cy.login()&lt;br&gt;
beforeEach() → cy.login() only if testIsolation is true&lt;/p&gt;

&lt;p&gt;Standard app, default credentials. Login once in before(), then beforeEach() only re-validates the session if Cypress actually cleared it (testIsolation: true). If testIsolation is false, cookies persist and calling cy.login() again would navigate back to home, breaking any test that expects to be on a specific page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key insight: why before AND beforeEach?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You're asking "are you mental? why on earth would you declare login TWICE?", basically because:&lt;/p&gt;

&lt;p&gt;before()     → establishes the initial session (runs once)&lt;br&gt;
beforeEach() → re-validates/restores the session if testIsolation cleared it&lt;/p&gt;

&lt;p&gt;For specs with testIsolation: false, beforeEach() is a no-op (or returns early) because the session is still alive. For specs with testIsolation: true, beforeEach() must re-run the login to restore the cleared session — but it uses cy.session() internally which caches credentials.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4p9u788bh4lr91levn43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4p9u788bh4lr91levn43.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a sneak peek of how my &lt;code&gt;e2e.ts&lt;/code&gt; file looks in it's (hopefully) final form:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdf703lpxrsubqex85z3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbdf703lpxrsubqex85z3.png" alt=" " width="800" height="783"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And I basically removed the cy.login function from more than 1k files in my codebase, leaving me or any other engineer to not worry anymore about the login being declared at test level, it handles everything for me now, as it should.&lt;/p&gt;

&lt;p&gt;What about you? Have you ever encountered a challenging and complex test codebase to deal with? What was the most difficult change you had to make? Leave me a comment, I would love to hear!&lt;/p&gt;

</description>
      <category>cypress</category>
      <category>e2etesting</category>
      <category>typescript</category>
      <category>login</category>
    </item>
    <item>
      <title>How to validate tables, rows or any content of an Excel file using Cypress</title>
      <dc:creator>Marcelo C.</dc:creator>
      <pubDate>Fri, 31 Oct 2025 11:43:24 +0000</pubDate>
      <link>https://dev.to/cypress/how-to-validate-a-content-of-an-xlsx-file-using-cypress-45da</link>
      <guid>https://dev.to/cypress/how-to-validate-a-content-of-an-xlsx-file-using-cypress-45da</guid>
      <description>&lt;p&gt;At the company I work for, we already have many test cases to validate a key behavior of our SaaS, which through the user downloads a table as an Excel file of the information needed. But there was a need to validate some edge cases, in which we also needed to validate that the content corresponds to what the table showed.&lt;/p&gt;

&lt;p&gt;This would mean that Cypress needs to deterministically validate rows, numbers, names and even colors inside the Excel file set by our user flows. After some research, we basically came upon two &lt;em&gt;Node.js&lt;/em&gt; libs: &lt;code&gt;@e965/xlsx&lt;/code&gt; and &lt;code&gt;exceljs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While &lt;code&gt;@e965/xlsx&lt;/code&gt; is mostly used for data content validation, as in validating a JSON rows straight from the sheet - &lt;code&gt;exceljs&lt;/code&gt; is more focused for style assertion, meaning assertions like “is A1 light-green?”. All right, so now we could split keeps tests readable and fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuring &lt;a class="mentioned-user" href="https://dev.to/e965"&gt;@e965&lt;/a&gt;/xlsx library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, wire up the configuration in Cypress. Head off to Node with cy.task(). It’s the official way to run filesystem code from Cypress tests: register tasks in setupNodeEvents and they’ll return values back to your spec.&lt;/p&gt;

&lt;p&gt;Remember to also import the package on the config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//cypress.config.ts

const xlsx = require('@e965/xlsx');
...
...
...
      on('task', {
        async readExcelByPattern(pattern: string, timeoutMs = 15000) {
          const re = new RegExp(pattern);
          const end = Date.now() + timeoutMs;

          while (Date.now() &amp;lt; end) {
            const files = fs.readdirSync(downloadsDir).filter(f =&amp;gt; re.test(f) &amp;amp;&amp;amp; !f.endsWith('.crdownload') &amp;amp;&amp;amp; !f.endsWith('.tmp'));

            if (files.length) {
              const { fullPath } = files
                .map(f =&amp;gt; {
                  const fullPath = path.join(downloadsDir, f);
                  return { fullPath, mtime: fs.statSync(fullPath).mtimeMs };
                })
                .sort((a, b) =&amp;gt; b.mtime - a.mtime)[0];

              await sleep(200);

              const wb = xlsx.readFile(fullPath);
              const sheet = wb.Sheets[wb.SheetNames[0]];
              const data = xlsx.utils.sheet_to_json(sheet);
              return { fileName: path.basename(fullPath), data };
            }

            await sleep(300);
          }

          throw new Error(`File .xlsx taht matches /${pattern}/ not found on "${downloadsDir}" inside ${timeoutMs}ms`);
        },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see that &lt;code&gt;readExcelByPattern&lt;/code&gt; is the task we should call to validate the content like rows, tables and any information inside the Excel file. You can then define it inside your test context and methods (or define it globally over &lt;code&gt;commands.ts&lt;/code&gt; if you plan to use it in many tests), but for a single test it should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//my-testing-context.ts

  @step('Read downloaded excel values')
  readExcelDownloadedFile(
    pathToFile: string = 'excel_export/bugs/',
    fixture: string,
    fileName: string
  ): ExportPrintableReportContext&amp;lt;TParent&amp;gt; {
    cy.fixture(pathToFile + fixture).then((expected: any[]) =&amp;gt; {
      cy.task('readExcelByPattern', fileName).then(({ data }: { data: any[] }) =&amp;gt; {
        expect(data.length, 'Table length').to.equal(expected.length);
        expected.forEach((expectedRow, i) =&amp;gt; {
          const actualRow = data[i];
          Object.entries(expectedRow).forEach(([key, expectedValue]) =&amp;gt; {
            const actualValue = actualRow[key] === undefined ? null : actualRow[key];
            expect(actualValue, `Row ${i} - Column "${key}"`).to.equal(expectedValue);
          });
        });
      });
    });
    return this;
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see it's pretty straight forward, it calls for a JSON file inside 'fixtures/excel_export/bugs/' that already has the values you want to validate and should be equal to the Excel file and executes a forEach of the Table length, and each row, which already awaits for a value.&lt;/p&gt;

&lt;p&gt;And this is how it would look inside a test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { testContext } from '@/my-testing-context'

const testingContext = new testContext();

describe('example of reading excel files', =&amp;gt; ()
  it('case 1', () =&amp;gt; {
    testingContext.readExcelDownloadedFile('excel_export/bugs/', tk, 'Excel.xlsx');
  });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Basically it checked 171 rows of the file content and succeeded in 40 seconds.&lt;/p&gt;

&lt;p&gt;For the second part of this tutorial, I'll expand on how to validate Excel colors as well. Happy testing!&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to get most out of cy.prompt() - 6 tips and tricks for your new AI tool!</title>
      <dc:creator>Marcelo C.</dc:creator>
      <pubDate>Thu, 09 Oct 2025 13:54:06 +0000</pubDate>
      <link>https://dev.to/cypress/how-to-get-most-out-of-cyprompt-6-tips-and-tricks-for-your-new-ai-tool-425l</link>
      <guid>https://dev.to/cypress/how-to-get-most-out-of-cyprompt-6-tips-and-tricks-for-your-new-ai-tool-425l</guid>
      <description>&lt;p&gt;I know, I know, Cypress has just announced a game changing feature with &lt;a href="https://www.cypress.io/blog/cy-prompt-frequently-asked-questions" rel="noopener noreferrer"&gt;cy.prompt()&lt;/a&gt; that is going to change the way we test - or at least approach how we think of it. You're going to use Natural Language all the way to test your new app? Read through my recommendations then!&lt;/p&gt;

&lt;p&gt;As a Cypress Ambassador I was lucky enough to be using &lt;a href="https://dev.to/marcelo_sqe/how-cypress-will-revolutionize-the-use-of-ai-in-testing-with-cyprompt-5gm2-temp-slug-2006966?preview=9c4ef5e51e58155f38ad1dcbf9f431475aa6874a9ce433001e022fdfd3b97ca85fd89420f5701d27bf06a28628c31b13e4802cb49d6966bf50a7aaee"&gt;cy.prompt&lt;/a&gt; for the past weeks and here are a few tips to make your testing and usage go a bit smoothly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) Start your phrase with the action or assertion you want&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of giving it an instruction like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;When the page loads, check that the header is seen and then click on Create button&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Would be better to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Wait 8 seconds for the page to load&lt;/code&gt;&lt;br&gt;
&lt;code&gt;Assert that the header is visible&lt;/code&gt;&lt;br&gt;
&lt;code&gt;Click on create button&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Now Cypress will translate your instructions more easily - a hardcoded &lt;em&gt;wait&lt;/em&gt;, followed by an &lt;em&gt;assertion&lt;/em&gt;, followed by a &lt;em&gt;click&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Try to separate instructions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The previous step gave it away already! Cypress prompt works as any other LLM - give clear instructions of what you want to do and it'll have a better chance to execute it.&lt;/p&gt;

&lt;p&gt;Do not mix assertions, with force clicks, with reloads in the same line of action! The prompt needs to go through, so in a way try to act as a &lt;em&gt;prompt engineer&lt;/em&gt; and step by step you'll get there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3) You can have up to 20 steps for each prompt you execute&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;20 is the limit, ok. But that doesn't mean that you need to have 20 steps each prompt. Also, the more steps you add, the prone it is to ask for clarifications or make mistakes.&lt;/p&gt;

&lt;p&gt;Think of it as this: each plain English text line you introduce is an abstraction layer, right? Do want an over-complicated test, or a easy to read through, understandable (for non-developers specially) test?&lt;/p&gt;

&lt;p&gt;Lesser is better in some cases!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxcdw6hj3hgfmfmt31sfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxcdw6hj3hgfmfmt31sfs.png" alt=" " width="800" height="687"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) Leave some tests in prompt in order to validate flaky behavior&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Got a new feature? Want to avoid brittle in your E2E test? Want to check for any weird behavior here and there? Then cy.prompt is your way to go! You can always leave your tests in plain English to see if the BDD/TDD behavior stays the same. &lt;/p&gt;

&lt;p&gt;Remember: it works in both local and CI - but it only supports Chrome or Chromium browsers (Edge/Electron). Any others are out (sorry Firefox!). Leave it a few days or weeks in your CI in prompt scenario and see what happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5) Portability first?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For me any test that is written in plain English has a natural advantage over each other. It doesn't need to be refactored into any other programming language. So if you already know that the application you're working now is starting to be ported into another modern framework, leave your tests in prompt format. Your devs will appreaciate!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6) It's always cached&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Another advantage with cy.prompt is that once it runs, it will cache the steps in order to avoid LLM interaction. But if you change one line in your prompt - wait 15 seconds instead of 8, for example - it will execute all over again.&lt;/p&gt;

&lt;p&gt;Remember this to focus on speed and reliability in your tests!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How Cypress will revolutionize the use of AI in testing with cy.prompt()</title>
      <dc:creator>Marcelo C.</dc:creator>
      <pubDate>Thu, 09 Oct 2025 10:28:22 +0000</pubDate>
      <link>https://dev.to/cypress/how-cypress-will-revolutionize-the-use-of-ai-in-testing-with-cyprompt-fe9</link>
      <guid>https://dev.to/cypress/how-cypress-will-revolutionize-the-use-of-ai-in-testing-with-cyprompt-fe9</guid>
      <description>&lt;p&gt;Cypress has become the go-to testing framework for SDETs and QA engineers to validate modern web apps. It’s fast, reliable, and backed by a mature ecosystem—both in &lt;a href="https://docs.cypress.io/app/references/changelog" rel="noopener noreferrer"&gt;software updates&lt;/a&gt; and excellent &lt;a href="https://docs.cypress.io/app/get-started/why-cypress" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. Add to that the vibrant community building powerful &lt;a href="https://dev.to/sebastianclavijo/my-top-191-favorite-cypress-plugins-for-testing-with-wick-like-precision-3fhh"&gt;plugins and extensions&lt;/a&gt;, and it’s clear why Cypress dominates the testing landscape.&lt;/p&gt;

&lt;p&gt;Cypress is taking a bold step into AI-powered testing with the upcoming &lt;a href="https://www.cypress.io/blog/cy-prompt-frequently-asked-questions" rel="noopener noreferrer"&gt;cy.prompt()&lt;/a&gt;. Unlike typical AI integrations that act as external copilots or rely on general-purpose &lt;em&gt;MCP-style&lt;/em&gt; assistants, &lt;code&gt;cy.prompt()&lt;/code&gt; adds the intent (what we want) built directly into the testing workflow.&lt;/p&gt;

&lt;p&gt;This means no context switching, no juggling between an IDE plugin and your test runner. Instead, Cypress allows you to describe your intent in plain English, and the AI automatically generates selectors, actions, and assertions right inside your test.&lt;/p&gt;

&lt;p&gt;It’s a shift from writing tests line by line to guiding your tests conversationally. Think less about &lt;code&gt;cy.get()&lt;/code&gt; or &lt;code&gt;cy.click()&lt;/code&gt; and more about telling Cypress what you want verified, letting the framework translate that into executable code.&lt;/p&gt;

&lt;p&gt;Here’s a video demonstration of &lt;code&gt;cy.prompt()&lt;/code&gt; in action:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Z_u8R3Z5spw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;This is the code that I used in the validation:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz55buxl9xe4od78p2oox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz55buxl9xe4od78p2oox.png" alt=" raw `cy.prompt()` endraw  in action" width="690" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here is what the prompt suggests of code locators right after is executed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fag177k1he4h8hj438fik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fag177k1he4h8hj438fik.png" alt="After usage,  raw `cy.prompt()` endraw  can insert the needed code into your IDE" width="767" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, you can leave the prompt as it is and push to your CI/CD pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxcvxemz08kdy0nwyw91.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqxcvxemz08kdy0nwyw91.png" alt="Github Actions running directly with the prompt and passing" width="800" height="1096"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;cy.prompt()&lt;/code&gt;, Cypress is no longer “just” a testing framework—it’s stepping into the AI-assisted development era. For SDETs and QA engineers, this means faster authoring, smarter locator handling, and easier onboarding for teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The possibilities of cy.prompt()&lt;/strong&gt;&lt;br&gt;
&lt;br&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cy.prompt focus on the intent:&lt;/strong&gt; what we want, not how do do it. It's a great tool for non-developers, or anyone who doesn’t want to dive deep into app implementation.

&lt;/li&gt;
&lt;li&gt;Imagine writing the &lt;strong&gt;BDD&lt;/strong&gt; (Behavior Driven Development) acceptance criteria directly into the test. You'll have the best of both worlds here, BDD criteria that is understood by all stakeholders (Project Managers, Product Owners), and the code being executed in the background.

&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TDD&lt;/strong&gt; (Test-Driven-Development) is also covered for the developers. Imagine developing a feature until is ready and, step by step (word by word, line by line) it start to pass. Until is ready for deployment.

&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portability is here:&lt;/strong&gt; Need to refactor your project? Move from one programming language to another? Don't need to change a thing in your tests written in plain English, they can be easily shared, exported, or integrated across different systems.

&lt;/li&gt;
&lt;li&gt;Also, another great benefit here are the &lt;strong&gt;self-healing&lt;/strong&gt; tests, they’re more resilient to changes in the DOM or selectors. This feature could fundamentally change how we approach automation. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The future of QA is not just code—it’s collaboration between AI and testers.&lt;/p&gt;

&lt;p&gt;What are your thoughts? &lt;/p&gt;

&lt;p&gt;Share, comment or connect with me directly in &lt;a href="https://www.linkedin.com/in/marceloc/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>cypress</category>
      <category>ai</category>
      <category>promptengineering</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
