SOFA SHOP Sticky header, z-index 9999 — the modal must cover it.

Norah Velvet Single-Seater Sofa

£849.00

SKU 100996 · In stock

The two widget buttons should appear directly under Add to cart, in the widget's own styling — not pink, not Comic Sans.

Test scenarios

1 · Auto-detected target (running above)

Initialised from the script tag's data-* attributes with no data-target-selector. Open both modals: the 3D one should load the GLB lazily, and the AR one should show a QR code on desktop or a Place in Your Room button on a phone.

2 · Explicit selector, second product

A different product id, inserted with targetSelector: '#explicit-slot' and insertPosition: 'append'. Proves the widget is not hardcoded to one product and that two instances coexist.

3 · Missing model

A product id that is in no manifest and has no glbUrl. Expected: no buttons at all — not a bar that opens an empty modal. The manifest is read first, and a product with nothing in it is injected nowhere. The console says which of the two happened; the page is otherwise untouched.

4 · Invalid target selector

targetSelector: '.does-not-exist' and then a selector that isn't valid CSS at all. Expected: nothing injected, nothing thrown, Add to cart still works.

5 · Per-site theme

The same product initialised with site: 'atozfurniture.ae'. On the real domain this happens automatically from the hostname; here the key is forced, because localhost matches nothing. The buttons should come out navy with 4px corners instead of the neutral default, and the QR link should gain &site=atozfurniture.ae so the AR page lands branded too.

Home & Soul is the second registered shop and a different Shopify palette: near-black, 3px corners, and upper-case labels — the one entry so far that needs the css hatch, because no token can say text-transform. It also loads a different model, the Sophia outdoor seater, which is pale teak with cream cushions and so the better test of whether the viewer's white background is too white.

The third shop is not in SITE_THEMES at all — it exists only as a _sites entry in products.local.json. Nothing about it was compiled into the loader you are running, so purple buttons with 12px corners are proof the manifest path is being read. That is the whole point: onboarding a shop is an edit to a JSON file on the CDN, not a rebuild and a redeploy to every shop already installed.

Two things to watch. The buttons appear with their colours rather than recolouring, because the widget already withholds them until the manifest lands. And the QR link still gains &site=newshop.example.com — the AR page resolves that key against the same manifest, so it lands branded too, one fetch after it paints.

6 · The viewer: intro swing, hint, footer

Open the 3D modal. The product holds still, then turns once with a tap icon riding it, and under the canvas: three background swatches (light selected), Share, and View in Your Space.

  • The order is: 1.5s of nothing, the icon fades in, the product turns 40° with it, the icon lets go about a third of the way through while the product keeps turning, then it comes back and stops exactly where it started — not near it. Then nothing more: it plays once.
  • Zoom all the way in from several angles — front, along the length, from above. The camera must stop outside the model every time, not push through the arm of the sofa. It should still get about 3× closer than the view it opened at. All the way out, the product should still fill roughly half the frame — never shrink to a speck in an empty stage.
  • Loading counts up — Loading the 3D model… 42% over a bar — rather than just spinning. Throttle to Slow 3G and reopen to watch it; a second open is instant, because the parsed model is cached and there is nothing left to count. A server that sends no Content-Length shows megabytes instead.
  • The icon is stuck to a point on the model, so it travels with the swing rather than hovering in the middle. It stays legible over both pale and dark parts — that's the white halo, not luck.
  • Drag at any point — during the hold, mid-swing — and the rest is cancelled: the model stays where your drag put it and the icon goes. Neither returns.
  • With Reduce motion on — macOS System Settings → Accessibility → Display, or DevTools → Rendering → Emulate CSS prefers-reduced-motion, which survives a reload and is easy to leave on — the icon stops pulsing but the swing still plays. Suppressing both is indistinguishable from a broken viewer, and model-viewer doesn't honour the preference for auto-rotate either. With data-debug the console says the preference was seen.
  • On a 120 Hz display the swing must take the same 8.8 seconds as on a 60 Hz one, and still rest on its opening framing. It is scaled by frame time precisely because three-stdlib's auto-rotate is per-frame, not per-second.
  • Each swatch repaints the scene and the area behind it, with no reload and without losing the angle you orbited to. On dark, the loading and error text stays readable — throttle the network to Slow 3G and reopen to see the spinner on it.
  • Share copies this page's URL with ?p3dw-view=100996 (plus &p3dw-bg= when the background isn't light). On a phone it opens the OS share sheet instead. Over plain http:// both are refused and the button says so — that is correct, not a bug.
  • View in Your Space closes the 3D modal and opens the AR one; closing that returns focus to the button on this page, not to a second dialog.
  • Keyboard: Tab reaches all five controls, the pressed swatch is visible without colour vision, and the focus ring is never removed.

7 · Cleanup

destroyAll() removes every injected button and drops every listener. Check the console for leaks afterwards.

console output appears here