DEV Community

CloudQA
CloudQA

Posted on Originally published at cloudqa.io AI-assisted

Why Automating Shopify Admin is a Nightmare (And How We Solved It)

Shopify is one of the harder platforms to automate reliably, and not for the reasons most teams expect.

Creating a product or syncing a feed isn't complex logic. The hard part is everything around it. This post breaks down how the QA team at CloudQA built stable, unattended end-to-end automation for AdNabu, a Shopify app for product feed and catalog automation, and the design decisions that made it work.

The four problems

Before a single meaningful test could run consistently, four separate problems had to be solved.

1. Session timeouts. 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.

2. MFA prompts. 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.

3. Shadow DOM. 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.

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

Any one of these is manageable. Together, they make stable, repeatable, unattended automation genuinely difficult.

The approach

1. Isolate the environment first

Instead of running Shopify–AdNabu tests on shared infrastructure, the team set up a dedicated virtual machine for this workflow.

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.

2. Solve login and MFA once, not every run

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 dedicated Chrome Debugger Profile that retains an already-authenticated Shopify session.

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.

Here's a generic sketch of the pattern (illustrative, not CloudQA's code):

# Launch Chrome once with a dedicated profile and remote debugging enabled.
# Log in manually (including MFA) in this window one time.
google-chrome \
  --remote-debugging-port=9222 \
  --user-data-dir="$HOME/qa-profiles/shopify-adnabu"
Enter fullscreen mode Exit fullscreen mode
// Playwright: attach to the already-authenticated browser instead of logging in
import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP('http://localhost:9222');
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();

await page.goto('https://admin.shopify.com/');
// No login step. The session is already valid.
Enter fullscreen mode Exit fullscreen mode

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.

3. Handle Shadow DOM deliberately

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

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

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):

// Walk into open shadow roots to find an element
function deepQuery(root, selector) {
  const direct = root.querySelector(selector);
  if (direct) return direct;

  for (const el of root.querySelectorAll('*')) {
    if (el.shadowRoot) {
      const found = deepQuery(el.shadowRoot, selector);
      if (found) return found;
    }
  }
  return null;
}

const saveButton = await page.evaluateHandle(
  () => deepQuery(document, 'button[type="submit"]')
);
Enter fullscreen mode Exit fullscreen mode

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

4. Self-healing selectors

To avoid rewriting tests every time Shopify ships a UI change, the team implemented fallback and self-healing selector logic 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.

A minimal version of the idea (illustrative):

async function resilientLocate(page, strategies) {
  for (const { name, selector } of strategies) {
    const locator = page.locator(selector);
    if (await locator.count() === 1) {
      if (name !== 'primary') {
        console.warn(`Primary selector failed; healed via "${name}"`);
      }
      return locator;
    }
  }
  throw new Error('Element not found with any strategy');
}

const addVariant = await resilientLocate(page, [
  { name: 'primary',   selector: '[data-test-id="add-variant"]' },
  { name: 'role+text', selector: 'role=button[name="Add variant"]' },
  { name: 'aria',      selector: '[aria-label="Add variant"]' },
]);
Enter fullscreen mode Exit fullscreen mode

Self-healing shouldn't hide real problems. The point is to stop cosmetic 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.

What got automated

With a stable environment, persistent auth, and resilient selectors in place, the team covered the workflows that matter most to AdNabu:

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

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

// Instead of: await page.waitForTimeout(10000);
await expect(page.locator('[data-status="synced"]'))
  .toBeVisible({ timeout: 60_000 });
Enter fullscreen mode Exit fullscreen mode

The outcome

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.

Key takeaways

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

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.

This post is based on a CloudQA case study of their work with AdNabu.

Top comments (0)