MarketCart CI › Guides

Shopify Horizon theme: a Playwright cart test that reads the real drawer

Horizon (Shopify's newer theme, version 4.4.0 at the time of writing) uses different cart markup from Dawn, so Dawn selectors find nothing and a naive test passes or fails for the wrong reason. Below: the Horizon 4.4.0 selectors, a minimal Playwright script, and the adapter MarketCart CI ships.

Horizon 4.4.0 cart markup (from the official source)

WhatSelector
Add to cartadd-to-cart-component button[name="add"] inside product-form-component
Selected variantproduct-form-component input[name="id"]
Cart drawertheme-drawer#cart-drawer > dialog (open = dialog.open); trigger [data-testid="cart-drawer-trigger"]
Linestr.cart-items__table-row:not(.cart-items__nested-line); title a.cart-items__title; options dl.cart-items__variants dd (values end with “, ”)
Quantity / priceinput[name="updates[]"] / td.cart-items__price bdi
Badge / total.cart-bubble__text-count / .cart-totals__total-value

Minimal Playwright script

// Horizon 4.4.0 selectors (checked against Shopify/horizon source)
import { chromium } from 'playwright-core';

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(process.argv[2]);
const added = page.waitForResponse((r) => /\/cart\/add(\.js)?$/.test(new URL(r.url()).pathname));
await page.click('add-to-cart-component button[name="add"], product-form-component button[name="add"]');
await added;
await page.waitForTimeout(800);

const stored = await page.evaluate(async () => (await fetch(window.Shopify.routes.root + 'cart.js')).json());
const dialog = page.locator('theme-drawer#cart-drawer dialog');
if (await dialog.count()) {
  // cart_type = drawer. If "auto open" is off, open it like a shopper would; never fall back to /cart.
  if (!(await dialog.evaluate((d) => d.open))) await page.click('[data-testid="cart-drawer-trigger"]');
} else {
  await page.goto(new URL('cart', page.url()).href); // cart_type = page: the cart page is the real surface
}
const visible = await page.evaluate(() => {
  const root = document.querySelector('theme-drawer#cart-drawer dialog[open]') ?? document.querySelector('main');
  return [...root.querySelectorAll('tr.cart-items__table-row:not(.cart-items__nested-line)')].map((row) => ({
    title: row.querySelector('a.cart-items__title')?.textContent?.trim(),
    options: [...row.querySelectorAll('dl.cart-items__variants dd')].map((d) => d.textContent.replace(/[,\u00a0\s]+$/u, '').trim()),
    qty: Number(row.querySelector('input[name="updates[]"]')?.value),
    price: row.querySelector('td.cart-items__price bdi')?.textContent?.trim(),
  }));
});
console.log({ stored: stored.items.map((i) => [i.variant_title, i.quantity, i.final_line_price]), visible,
  badge: await page.locator('.cart-bubble__text-count').textContent(),
  total: await page.locator('.cart-totals__total-value').first().textContent() });
await browser.close();

Expected and actual output

On our synthetic Horizon skins (drawer, cart page) the MarketCart Horizon adapter classified 21/21 cases as expected: healthy → PASS; wrong variant, clamped quantity, wrong amount, wrong currency sign, stored currency mismatch, stale drawer, stale badge, stale quantity → REGRESSION; required total missing and unserved market → UNSUPPORTED. Example REGRESSION reason: stored 1 line(s), visible 0 line(s) in drawer.

Tested versions and what is not verified

Pitfalls

Try it: run the free demo (synthetic store, no account), check one case on your own store for free, or use the open-source CLI and GitHub Action locally. Hosted runs: EUR 19 for 100 completed cases (30 days, no subscription).

Updated 2026-11-07. Evidence: repository evidence folder.