Visual regression snapshots for Shopify themes using Playwright — no running dev server required.
This enables drastic refactoring without the fear of breaking existing themes, and makes it easy to add visual tests for new Liquid templates as you build them.
- ✨ Render OS 2.0 JSON or Liquid templates entirely in-memory with LiquidJS and custom Shopify helpers.
- 🧱 Resolves sections, snippets, and local-path includes just like a deployed theme.
- 🎨 Inlines CSS/JS assets (including
{{ 'theme.css' | asset_url | stylesheet_tag }}) so snapshots reflect final storefront styling. - 🖼️ Replaces
shopify://shop_images/...references with deterministic SVG placeholders (respecting requested width/height) so tests never need the real CDN assets. - 🌐 Respects Shopify locale strings: load
locales/en.default.json(or passlocale/SNAPIFY_LOCALE) and{{ 'sections.*' | t }}renders with the same copy as production. - 📸 Uses Playwright to capture screenshots and
pixelmatchto diff against baselines. - 🧪 Ships both a programmatic API (
render) and a CLI (snapify render).
npm save --dev @bosun-ai/snapify playwright
npx playwright install --with-deps chromiumYou can then write your tests, or run the CLI against the current repository root (which already contains a full theme).
snapify render <template> [options]Common flags:
--theme-root– root of the Shopify theme (defaults toprocess.cwd()).--layout– override layout file (without.liquid).--data– inline JSON or a path to a JSON file providing Liquid data.--styles/--styles-file– inject additional CSS.--viewport 1440x900– customize Playwright viewport.--baseline-dirand--output-dir– control artifact locations (.snapify/baselineand.snapify/artifactsby default).--update– rebuild the baseline snapshot.
Example:
snapify render index --theme-root .. --viewport 1440x900 --data ./fixtures/home.jsonimport { render } from 'snapify';
await render({
themeRoot: '/path/to/theme',
template: 'product',
locale: 'en.default',
layout: 'checkout',
data: { product: { title: 'Sample' } },
styles: '.debug-outline { outline: 1px solid red; }',
viewport: { width: 1440, height: 900 },
snapshot: {
name: 'product-page',
baselineDir: './.snapify/baseline',
outputDir: './.snapify/artifacts',
update: process.env.CI ? false : true
}
});The resolved object includes:
htmlPath– rendered document saved to disk for inspection. Recommended to add these to your .gitignore.screenshotPath– Playwright capture for the latest run.diffPath– optional PNG diff if the baseline mismatches.updatedBaseline–trueif the baseline image was re-written this run.
- Liquid + sections.
TemplateAssemblerconfigures LiquidJS with Shopify-like defaults, resolves JSON templates (sections, block order,custom_css) and plain.liquidtemplates. - Inline assets. Filters such as
asset_url,stylesheet_tag, andscript_tagare re-implemented to read fromassets/and inline their contents directly into the<head>. - Head injection. Anything coming from filters or user-provided
stylesis piped throughcontent_for_header(or injected at the top of<head>if a layout omits it) so the snapshot matches storefront styling. - Playwright capture. HTML is handed to a headless Chromium page via
page.setContent, and the resulting screenshot is compared with the baseline usingpixelmatch.
Snapify keeps Liquid's relativeReference behavior enabled, so you can co-locate fixtures next to the template you are testing:
{%- comment -%}sections/__snapify__/hero.liquid{%- endcomment -%}
<section class="hero">
{% render './partials/cta', label: 'Book a demo' %}
</section>Place sections/__snapify__/partials/cta.liquid next to it and the renderer will resolve the relative include without needing to copy files into snippets/.
Repository-level tests live in tests/ (outside this package) and import the compiled Snapify build. From the repo root:
# build snapify first
npm run snapify:build
# refresh baselines across index/product/cart
SNAPIFY_UPDATE_BASELINES=1 npm run snapify:test:templates
# validate against existing baselines
npm run snapify:test:templatesArtifacts land under .snapify/templates/{baseline,artifacts} at the repo root so they can be reviewed or committed.