<?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: CloudQA</title>
    <description>The latest articles on DEV Community by CloudQA (@cloudqa).</description>
    <link>https://dev.to/cloudqa</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%2F4147624%2F3703850f-4537-45d8-be70-3615f0be1c48.png</url>
      <title>DEV Community: CloudQA</title>
      <link>https://dev.to/cloudqa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cloudqa"/>
    <language>en</language>
    <item>
      <title>How to Prompt an LLM for Test Cases You Can Actually Use</title>
      <dc:creator>CloudQA</dc:creator>
      <pubDate>Wed, 14 Oct 2026 04:32:19 +0000</pubDate>
      <link>https://dev.to/cloudqa/how-to-prompt-an-llm-for-test-cases-you-can-actually-use-1pdj</link>
      <guid>https://dev.to/cloudqa/how-to-prompt-an-llm-for-test-cases-you-can-actually-use-1pdj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cloudqa.io/gpt-prompts-test-case-generation-qa-automation-2025/" rel="noopener noreferrer"&gt;CloudQA blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ask an LLM to "write test cases for a login page" and you'll get something that looks impressive and tests almost nothing. The output is fluent, well formatted, and generic: valid login, invalid password, empty fields. Any junior tester could have listed those in two minutes.&lt;/p&gt;

&lt;p&gt;The model isn't the problem. The prompt is. An LLM can only work with what you give it, and "write test cases for login" gives it nothing about your product, your rules, or what counts as a failure.&lt;/p&gt;

&lt;p&gt;This guide covers a simple prompt structure, five prompts you can adapt today, and the review habits that keep AI-generated tests from becoming a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four parts of a useful test-generation prompt
&lt;/h2&gt;

&lt;p&gt;Whichever model you use, prompts that produce usable test cases tend to contain the same four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A role.&lt;/strong&gt; "Act as a senior QA engineer" sets the level and vocabulary of the response. It's a small lever, but it works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real context.&lt;/strong&gt; Paste the actual user story, acceptance criteria, API spec, or a code snippet. This matters more than anything else on the list. A model reading your real requirements can find real gaps. A model reading "a login form" can only guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A specific task.&lt;/strong&gt; Use direct verbs: "Generate," "List," "Identify." Avoid "think about" or "consider."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An output format.&lt;/strong&gt; Ask for a table with named columns, Gherkin, or JSON. If you want to paste the result into a test management tool, say so.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you only fix one thing, fix the context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 1: Functional test cases from a user story
&lt;/h2&gt;

&lt;p&gt;The most common use case. You have a story and acceptance criteria and need a first pass at coverage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Act as a senior QA engineer. Based on the user story and acceptance
criteria below, generate functional test cases as a table with the
columns: Test Case ID, Scenario, Preconditions, Steps, Expected Result.

Include positive, negative, and boundary cases. If any acceptance
criterion is ambiguous, list your assumption in a separate "Assumptions"
section instead of silently choosing an interpretation.

User story:
As a registered user, I want to add a product to my cart from the
product details page so that I can purchase it later.

Acceptance criteria:
1. The product details page displays an "Add to Cart" button.
2. Clicking it adds the selected item to the cart.
3. A confirmation message appears: "Product added to cart successfully."
4. The cart icon in the header updates to show the new item count.
5. If the item is already in the cart, the button stays clickable and
   the cart quantity increases.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "Assumptions" instruction is the most valuable line in that prompt. It makes the model show its reasoning about gaps in your requirements instead of papering over them, and those gaps are often the real bugs.&lt;/p&gt;

&lt;p&gt;Here's what good output looks like (an illustrative example, written by hand):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;Expected Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TC-01&lt;/td&gt;
&lt;td&gt;Add a new item&lt;/td&gt;
&lt;td&gt;Open product page, click Add to Cart&lt;/td&gt;
&lt;td&gt;Item appears in cart, confirmation shown, header count +1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-02&lt;/td&gt;
&lt;td&gt;Add an item already in cart&lt;/td&gt;
&lt;td&gt;Add the same product twice&lt;/td&gt;
&lt;td&gt;Quantity becomes 2, header count updates correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-03&lt;/td&gt;
&lt;td&gt;Add while out of stock&lt;/td&gt;
&lt;td&gt;Open an out-of-stock product&lt;/td&gt;
&lt;td&gt;Button disabled or clear error shown, cart unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TC-04&lt;/td&gt;
&lt;td&gt;Rapid repeated clicks&lt;/td&gt;
&lt;td&gt;Click Add to Cart five times quickly&lt;/td&gt;
&lt;td&gt;Quantity is consistent, no duplicate line items&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;TC-03 and TC-04 aren't in the acceptance criteria. Cases like these are where the model earns its keep, and where you should look first when reviewing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 2: Edge cases and negative scenarios
&lt;/h2&gt;

&lt;p&gt;Models are good at brainstorming the unusual inputs people forget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Act as a meticulous QA tester. For the login form described below,
list negative scenarios and edge cases. Focus on invalid input,
boundary conditions, and unusual user behavior. Group them by
category and, for each one, state what you expect the system to do.

Functionality:
A login form with "Email" (must be a valid email format) and
"Password" (minimum 8 characters).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask for categories (input validation, session behavior, rate limiting, accessibility). Unstructured lists drift toward the obvious cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 3: Security-focused test ideas
&lt;/h2&gt;

&lt;p&gt;For testing your own applications, a model can quickly generate a checklist of common vulnerability checks. Only run these against systems you own or are explicitly authorized to test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Act as an application security tester. For a registration form that
accepts username, email, and password, generate test cases to check
for common input-handling vulnerabilities such as SQL injection and
cross-site scripting. For each case, give the input, the field it
targets, and the secure behavior you'd expect.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat the output as a starting checklist, not a security assessment. It doesn't replace a proper review or dedicated security tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt 4: BDD scenarios in Gherkin
&lt;/h2&gt;

&lt;p&gt;If your team writes behavior-driven specs, you can generate a first draft of the feature file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="err"&gt;Act&lt;/span&gt; &lt;span class="err"&gt;as&lt;/span&gt; &lt;span class="nf"&gt;a &lt;/span&gt;BDD specialist. Write a Gherkin feature file for the feature
&lt;span class="err"&gt;below,&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;multiple&lt;/span&gt; &lt;span class="err"&gt;Scenario&lt;/span&gt; &lt;span class="err"&gt;blocks&lt;/span&gt; &lt;span class="err"&gt;covering&lt;/span&gt; &lt;span class="err"&gt;positive&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;negative&lt;/span&gt;
&lt;span class="err"&gt;paths.&lt;/span&gt; &lt;span class="err"&gt;Use&lt;/span&gt; &lt;span class="err"&gt;Scenario&lt;/span&gt; &lt;span class="err"&gt;Outline&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;Examples&lt;/span&gt; &lt;span class="err"&gt;where&lt;/span&gt; &lt;span class="err"&gt;inputs&lt;/span&gt; &lt;span class="err"&gt;vary.&lt;/span&gt;

&lt;span class="kd"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Product search
As a user, I want to search for products by name so that I can find
what I'm looking for.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A trimmed example of the kind of output to expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Search with a matching product name
  &lt;span class="nf"&gt;Given &lt;/span&gt;I am on the home page
  &lt;span class="nf"&gt;When &lt;/span&gt;I search for &lt;span class="s"&gt;"wireless mouse"&lt;/span&gt;
  &lt;span class="nf"&gt;Then &lt;/span&gt;I should see products whose names contain &lt;span class="s"&gt;"wireless mouse"&lt;/span&gt;

&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Search with no matches
  &lt;span class="nf"&gt;Given &lt;/span&gt;I am on the home page
  &lt;span class="nf"&gt;When &lt;/span&gt;I search for &lt;span class="s"&gt;"zzzzqqq"&lt;/span&gt;
  &lt;span class="nf"&gt;Then &lt;/span&gt;I should see a &lt;span class="s"&gt;"no results found"&lt;/span&gt; message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prompt 5: Ask the model to critique its own work
&lt;/h2&gt;

&lt;p&gt;This one is underused. After generating a suite, run a second prompt against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review the test cases below against the requirements I provided.
1. Which requirements have no test coverage?
2. Which test cases are duplicates or near-duplicates?
3. Which test cases assume behavior that isn't stated in the
   requirements?
4. What are the five highest-risk scenarios that are missing?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Question 3 is the important one. It surfaces cases where the model invented behavior, which is the most common failure in AI-generated tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI-generated tests go wrong
&lt;/h2&gt;

&lt;p&gt;LLMs are good first-draft engines. They are not oracles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They don't know your business.&lt;/strong&gt; A model doesn't know that refunds over a certain amount need manager approval, or that one customer tier gets special pricing. It only knows what's in your prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They invent plausible behavior.&lt;/strong&gt; If the requirements are silent, the model may fill in an answer that sounds right and is wrong for your product. Tests built on invented behavior fail for the wrong reasons, or worse, pass for the wrong reasons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They produce volume, not priority.&lt;/strong&gt; Thirty test cases isn't better than ten if you can't tell which five matter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated automation code is a draft.&lt;/strong&gt; If you ask for Playwright or WebdriverIO scripts, check every selector and wait strategy against your real application. Generated selectors are often guesses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data privacy is your responsibility.&lt;/strong&gt; Don't paste proprietary code, customer data, or confidential requirements into a public model interface. Use an enterprise offering with the data guarantees your organization requires.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A short review checklist
&lt;/h2&gt;

&lt;p&gt;Before any AI-generated test case enters your suite:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does it trace back to a real requirement?&lt;/li&gt;
&lt;li&gt;Are the expected results stated explicitly, or just implied?&lt;/li&gt;
&lt;li&gt;Is any behavior assumed that you haven't confirmed?&lt;/li&gt;
&lt;li&gt;Is it a duplicate of something you already have?&lt;/li&gt;
&lt;li&gt;Would you know why it failed if it failed?&lt;/li&gt;
&lt;li&gt;Is it worth automating, or is it better as an exploratory check?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a test case can't pass that list, rewrite it or drop it.&lt;/p&gt;

&lt;h2&gt;
  
  
  From prompt to automation
&lt;/h2&gt;

&lt;p&gt;The usual workflow is: generate cases, review them, pick the ones worth automating, then turn those into scripts. You can ask a model for framework-specific code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Convert test case TC-02 into a Playwright test in TypeScript. Use
role-based locators where possible, avoid fixed timeouts, and add a
comment above any selector you're unsure about.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last instruction, flagging uncertain selectors, gives you a built-in list of things to verify by hand.&lt;/p&gt;

&lt;p&gt;If you'd rather not hand-write prompts at all, CloudQA's &lt;a href="https://cloudqa.io/ai-test-case-generator/" rel="noopener noreferrer"&gt;AI test case generator&lt;/a&gt; produces test cases from a feature description in the browser.&lt;/p&gt;

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

&lt;p&gt;Better prompts come from better inputs: real requirements, an explicit format, and a request for the model to state its assumptions. The model drafts quickly. Your job is to judge what's worth keeping.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://cloudqa.io/" rel="noopener noreferrer"&gt;CloudQA&lt;/a&gt; builds codeless test automation for web applications. If you're working on test case generation or reducing test maintenance, we'd be glad to hear what you're working on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>End-to-End Salesforce Automation: Lightning Components, a Dynamic DOM, and OTP MFA</title>
      <dc:creator>CloudQA</dc:creator>
      <pubDate>Tue, 06 Oct 2026 17:24:29 +0000</pubDate>
      <link>https://dev.to/cloudqa/end-to-end-salesforce-automation-lightning-components-a-dynamic-dom-and-otp-mfa-4ng7</link>
      <guid>https://dev.to/cloudqa/end-to-end-salesforce-automation-lightning-components-a-dynamic-dom-and-otp-mfa-4ng7</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://cloudqa.io/case-study-salesforce-lightning-test-automation/" rel="noopener noreferrer"&gt;CloudQA blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Salesforce is a genuinely difficult platform to automate well, and the difficulty is structural rather than incidental. Three characteristics of the platform combine to make naive automation approaches fail quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A dynamic DOM.&lt;/strong&gt; Salesforce's Lightning interface generates much of its markup dynamically, and element attributes — including IDs and classes automation would normally rely on — can change between sessions or even between renders of the same page. Selectors that work today can silently stop working tomorrow, with no code change on the testing side to explain why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lightning components.&lt;/strong&gt; Salesforce's component framework renders complex UI elements — dynamic forms, dropdowns, data tables, pop-ups — through its own internal structure rather than plain HTML controls. These components often don't expose the kind of simple, stable attributes that standard selector strategies expect, which makes reliably identifying and interacting with them harder than automating a conventional web form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OTP-based MFA.&lt;/strong&gt; Salesforce's one-time-password multi-factor authentication is, like most MFA, deliberately resistant to automated bypass. A test suite that has to clear an OTP challenge on every run either can't run unattended at all, or has to solve session persistence some other way.&lt;/p&gt;

&lt;p&gt;Compounding all three: the goal wasn't to verify isolated pages in a vacuum, but to automate &lt;em&gt;complete business workflows&lt;/em&gt; — multi-step processes spanning several Salesforce screens, involving navigation, data entry, validation, and record updates in sequence. Any one unstable step in a long workflow can break the entire run, which raises the stakes on getting each of these problems solved properly rather than patched around.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Stable selector strategy for a dynamic DOM
&lt;/h3&gt;

&lt;p&gt;Rather than relying on the kind of single-attribute selectors that break the moment Salesforce regenerates its markup, we identified genuinely stable selectors where they existed and built custom selector strategies for the elements where they didn't. That meant looking past whatever attribute seemed most convenient at first glance, and instead identifying the properties of each element that actually stayed consistent across sessions and re-renders — the difference between automation that survives Salesforce's normal behavior and automation that has to be rewritten every time the DOM regenerates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solving OTP/MFA with a persistent authenticated session
&lt;/h3&gt;

&lt;p&gt;As with session-based authentication generally, trying to script around an OTP challenge on every execution is both fragile and, for most implementations, not a reliable long-term approach. Instead, we used a dedicated Chrome Debugger Profile to maintain an already-authenticated Salesforce session. Because the profile retains valid login state between runs, automated executions connect to a session that has already cleared OTP validation, rather than triggering a fresh MFA challenge every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  A dedicated, isolated execution environment
&lt;/h3&gt;

&lt;p&gt;We configured a dedicated VM specifically for Salesforce test execution, separate from other automation infrastructure. This gives Salesforce automation a consistent environment run after run — no state bleed from unrelated projects, no resource contention, and a much shorter path to diagnosing a failure when one does occur, since the environment itself is a known, controlled variable rather than a moving target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automating Salesforce's complex UI components
&lt;/h3&gt;

&lt;p&gt;Salesforce-specific components — dynamic forms, dropdowns, data tables, pop-ups, and Lightning components generally — needed automation logic built for how they actually behave, not treated like standard HTML controls. That meant building interaction strategies specific to each component type: handling dropdowns that render their options dynamically, tables that load and paginate data asynchronously, and pop-ups that need to be detected and handled without stalling the workflow behind them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Selector maintenance and healing
&lt;/h3&gt;

&lt;p&gt;To keep the suite stable as Salesforce elements and attributes change over time, we used fallback selector strategies so that when a primary selector no longer matches, the automation can still locate the correct element through an alternate identifying property. Just as important: when a selector does need to be updated, it can be updated directly — without re-recording the entire test from scratch. That distinction matters over the life of a test suite; the cost of Salesforce's inevitable UI drift becomes a small, targeted fix instead of a rebuild.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full end-to-end workflow automation
&lt;/h3&gt;

&lt;p&gt;With the underlying stability problems solved, we automated complete business workflows across multiple Salesforce screens — not individual pages checked in isolation, but full processes: navigating between screens, entering data, validating it, and updating records, all within a single automated run that mirrors how a real user actually moves through the system. This is a meaningfully higher bar than page-level testing, since a failure anywhere in the chain has to be caught, and every step depends on the ones before it having completed correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Outcome
&lt;/h2&gt;

&lt;p&gt;The result is a Salesforce automation suite that runs complete, multi-screen business workflows unattended — without manual OTP entry, without breaking every time Salesforce regenerates an attribute, and without needing a full test rewrite every time a Lightning component's markup shifts. By solving authentication, environment stability, and selector resilience as foundational problems first, the workflows built on top of them — the actual business processes the project needed covered — could be automated end-to-end rather than only in fragile pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treat Salesforce's dynamic DOM as the default, not the exception.&lt;/strong&gt; Selector strategy has to account for change from day one, not react to it after tests start failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent authenticated sessions solve MFA more reliably than trying to automate around it.&lt;/strong&gt; This applies as much to OTP-based MFA on Salesforce as it does to any other MFA implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightning components need component-aware automation logic&lt;/strong&gt;, not generic selectors borrowed from standard HTML testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selector healing should let you patch, not rebuild.&lt;/strong&gt; The ability to update a single broken selector — rather than re-record an entire test — is what keeps a long-lived suite maintainable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate the workflow, not just the page.&lt;/strong&gt; Multi-screen, end-to-end coverage catches issues that isolated page-level tests would miss entirely.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;We do this kind of testing work for teams dealing with Salesforce's dynamic DOM, Lightning components, or MFA that keeps breaking their automation. If you're wrestling with something similar, &lt;a href="https://cloudqa.io/" rel="noopener noreferrer"&gt;CloudQA&lt;/a&gt; is worth a look.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>automation</category>
    </item>
    <item>
      <title>Why Automating Shopify Admin is a Nightmare (And How We Solved It)</title>
      <dc:creator>CloudQA</dc:creator>
      <pubDate>Mon, 05 Oct 2026 03:57:50 +0000</pubDate>
      <link>https://dev.to/cloudqa/why-automating-shopify-admin-is-a-nightmare-and-how-we-solved-it-1hmo</link>
      <guid>https://dev.to/cloudqa/why-automating-shopify-admin-is-a-nightmare-and-how-we-solved-it-1hmo</guid>
      <description>&lt;p&gt;Shopify is one of the harder platforms to automate reliably, and not for the reasons most teams expect.&lt;/p&gt;

&lt;p&gt;Creating a product or syncing a feed isn't complex logic. The hard part is everything &lt;em&gt;around&lt;/em&gt; it. This post breaks down how the QA team at &lt;a href="https://cloudqa.io" rel="noopener noreferrer"&gt;CloudQA&lt;/a&gt; built stable, unattended end-to-end automation for &lt;strong&gt;AdNabu&lt;/strong&gt;, a Shopify app for product feed and catalog automation, and the design decisions that made it work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four problems
&lt;/h2&gt;

&lt;p&gt;Before a single meaningful test could run consistently, four separate problems had to be solved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Session timeouts.&lt;/strong&gt; Scheduled or long-running suites need a session that's still valid when they execute. Logging in fresh on every run is slower, and every login is another point of failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. MFA prompts.&lt;/strong&gt; MFA exists specifically to interrupt automated, unattended access. Hitting a challenge mid-run doesn't just fail one test, it stalls the whole pipeline behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Shadow DOM.&lt;/strong&gt; Shopify's admin encapsulates component internals in shadow roots. Standard DOM queries can't see inside them, so conventional selector strategies can't reach a meaningful portion of the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Frequent UI changes.&lt;/strong&gt; Shopify ships updates on its own schedule. Single-attribute selectors break whenever markup shifts, even when user-facing behavior hasn't changed.&lt;/p&gt;

&lt;p&gt;Any one of these is manageable. Together, they make stable, repeatable, unattended automation genuinely difficult.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Isolate the environment first
&lt;/h3&gt;

&lt;p&gt;Instead of running Shopify–AdNabu tests on shared infrastructure, the team set up a &lt;strong&gt;dedicated virtual machine&lt;/strong&gt; for this workflow.&lt;/p&gt;

&lt;p&gt;That removed a whole class of flaky-test causes: resource contention, state bleed from unrelated runs, and configuration drift from other projects. The VM's config, browser state, and session data belong exclusively to this suite, so when something fails, it's a Shopify–AdNabu problem and not an artifact of something else running alongside it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Solve login and MFA once, not every run
&lt;/h3&gt;

&lt;p&gt;Scripting around MFA on every run is fragile, and for many MFA implementations it isn't reliably automatable at all. So instead of fighting it, the team used a &lt;strong&gt;dedicated Chrome Debugger Profile&lt;/strong&gt; that retains an already-authenticated Shopify session.&lt;/p&gt;

&lt;p&gt;The profile persists login state between runs, so the automation attaches to a session that's already past the MFA challenge. One mechanism fixes both the session-timeout and MFA problems: as long as the profile's session is valid, tests run against a live, authenticated Shopify instance with no login step at all.&lt;/p&gt;

&lt;p&gt;Here's a generic sketch of the pattern (illustrative, not CloudQA's code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Launch Chrome once with a dedicated profile and remote debugging enabled.&lt;/span&gt;
&lt;span class="c"&gt;# Log in manually (including MFA) in this window one time.&lt;/span&gt;
google-chrome &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--remote-debugging-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9222 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-data-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/qa-profiles/shopify-adnabu"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Playwright: attach to the already-authenticated browser instead of logging in&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&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;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connectOverCDP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:9222&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://admin.shopify.com/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// No login step. The session is already valid.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off: you now own the lifecycle of that session. Plan for what happens when it eventually expires, such as an alert for a manual re-auth, rather than discovering it through a wall of red tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Handle Shadow DOM deliberately
&lt;/h3&gt;

&lt;p&gt;Standard selectors stop at the shadow host. The team built custom logic for &lt;strong&gt;piercing shadow roots&lt;/strong&gt;: first identifying which parts of the Shopify admin use Shadow DOM, then writing selector and interaction code that traverses &lt;em&gt;into&lt;/em&gt; those shadow trees instead of stopping at the host element.&lt;/p&gt;

&lt;p&gt;The result is automation that can click, type into, and read values from elements that would otherwise be invisible to the test framework.&lt;/p&gt;

&lt;p&gt;If you're using Playwright, its CSS and text locators pierce open shadow roots by default. Here's a generic example of doing it by hand, useful when your tooling doesn't (illustrative):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Walk into open shadow roots to find an element&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deepQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&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;direct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;direct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;direct&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &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;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&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;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deepQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;found&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&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;saveButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluateHandle&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="nf"&gt;deepQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button[type="submit"]&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that closed shadow roots can't be traversed this way, which is another reason to map out where Shadow DOM is used &lt;em&gt;before&lt;/em&gt; you build your suite.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Self-healing selectors
&lt;/h3&gt;

&lt;p&gt;To avoid rewriting tests every time Shopify ships a UI change, the team implemented &lt;strong&gt;fallback and self-healing selector logic&lt;/strong&gt; that identifies elements using multiple properties instead of a single brittle reference. When the primary selector stops matching (an attribute changed, an ID shifted, markup was restructured), the automation falls back to alternate identifying properties to find the same functional element.&lt;/p&gt;

&lt;p&gt;A minimal version of the idea (illustrative):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resilientLocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;strategies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;strategies&lt;/span&gt;&lt;span class="p"&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;locator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Primary selector failed; healed via "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;locator&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Element not found with any strategy&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addVariant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resilientLocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test-id="add-variant"]&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role+text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role=button[name="Add variant"]&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aria&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[aria-label="Add variant"]&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-healing shouldn't hide real problems. The point is to stop &lt;em&gt;cosmetic&lt;/em&gt; changes from being mistaken for failures, so the team's attention goes to real behavioral changes. Logging every heal (as above) keeps the healing visible instead of silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What got automated
&lt;/h2&gt;

&lt;p&gt;With a stable environment, persistent auth, and resilient selectors in place, the team covered the workflows that matter most to AdNabu:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product creation:&lt;/strong&gt; building listings end to end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variants:&lt;/strong&gt; size, color, and other option combinations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images:&lt;/strong&gt; uploading and associating product imagery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; setting and updating price data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Feed sync:&lt;/strong&gt; verifying product data syncs to the Google Shopping feed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-generated descriptions:&lt;/strong&gt; validating AI-produced description content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Image uploads, feed syncs, and AI generation all complete on their own timelines, so the suite uses &lt;strong&gt;dynamic waits instead of fixed sleeps&lt;/strong&gt;. Tests proceed as soon as an operation genuinely finishes, not on a guessed timer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Instead of: await page.waitForTimeout(10000);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-status="synced"]&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="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The outcome
&lt;/h2&gt;

&lt;p&gt;The combination of an isolated environment, persistent authentication, Shadow DOM handling, and self-healing selectors produced a suite that runs unattended and stays stable through Shopify's routine UI updates. It doesn't need manual re-login, doesn't stall on MFA, and doesn't need constant selector rewrites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolate before you optimize.&lt;/strong&gt; A dedicated environment eliminates a whole class of flakiness before you write any selector logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solve authentication once, not every run.&lt;/strong&gt; Persisting a session removes login and MFA as a per-execution failure point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow DOM needs a strategy, not a workaround.&lt;/strong&gt; Plan for it up front instead of discovering it mid-project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-healing buys time, not immunity.&lt;/strong&gt; It absorbs UI churn so you can focus on genuine regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These patterns aren't Shopify-specific. Any modern SaaS admin with MFA, web components, and a fast release cadence presents the same problems, and the same playbook applies.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is based on a &lt;a href="https://cloudqa.io/case-study-shopify-adnabu-test-automation/" rel="noopener noreferrer"&gt;CloudQA case study&lt;/a&gt; of their work with AdNabu.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>devops</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
