diff --git a/craft/scripts/e2e-canvas-parity.mjs b/craft/scripts/e2e-canvas-parity.mjs
index b67ab4e..276d45c 100644
--- a/craft/scripts/e2e-canvas-parity.mjs
+++ b/craft/scripts/e2e-canvas-parity.mjs
@@ -8,6 +8,10 @@
* same page's published HTML rendered in a plain page whose viewport equals
* the canvas content width.
*
+ * Stored state is loaded VERBATIM (blank style values included). Allowed editor-only
+ * differences (empty-container drop zone, empty-image placeholder) are neutralised in the
+ * canvas before measuring; see the ALLOWED block in run().
+ *
* Compared per matched element: bounding box (x/y/w/h, within 1px) and the
* computed font-size / line-height / margin / padding / colour / font-family
* / font-weight / display. Editor-only chrome (the dashed header/footer zone
@@ -240,6 +244,15 @@ async function run(name, launcher, exe) {
await ed.waitForSelector('.canvas-device-frame [data-craft-container]', { timeout: 20000 });
await ed.waitForTimeout(1500); // header/footer zone previews + layout settle
const canvasW = await ed.evaluate(() => document.querySelector('.canvas-device-frame').clientWidth);
+ // ALLOWED editor-only differences (deliberate UX with no published counterpart), neutralised
+ // before measuring so they don't mask real diffs:
+ // - the 40px drop zone on EMPTY containers (Container sets data-wsb-dropzone only then)
+ // - the placeholder
shown for an image node with no src (toHtml emits nothing)
+ await ed.evaluate(() => {
+ const f = document.querySelector('.canvas-device-frame');
+ f.querySelectorAll('[data-wsb-dropzone]').forEach((el) => el.style.setProperty('min-height', '0', 'important'));
+ f.querySelectorAll('img[src^="data:image/svg"]').forEach((el) => el.style.setProperty('display', 'none', 'important'));
+ });
const canvas = await ed.evaluate(measure, { rootSel: '.canvas-device-frame', isCanvas: true, skipNav: idx > 0 });
if (shotDir) await ed.screenshot({ path: path.join(shotDir, `${label.replace('/', '_')}.${name}.canvas.png`) });
if (process.env.CANVAS_CHROME_DUMP && idx === 0 && spec === specs[0]) {
diff --git a/craft/src/components/layout/Container.dropZone.test.tsx b/craft/src/components/layout/Container.dropZone.test.tsx
index 3621b4e..8d75e75 100644
--- a/craft/src/components/layout/Container.dropZone.test.tsx
+++ b/craft/src/components/layout/Container.dropZone.test.tsx
@@ -31,3 +31,11 @@ describe('Container empty-only drop zone', () => {
expect(mount({ style: { minHeight: '100px' } }, false).style.minHeight).toBe('100px');
});
});
+
+describe('data-wsb-dropzone marker (harness hook)', () => {
+ test('set only while the drop-zone min-height is the one applied', () => {
+ expect(mount({ style: {} }, false).hasAttribute('data-wsb-dropzone')).toBe(true);
+ expect(mount({ style: {} }, true).hasAttribute('data-wsb-dropzone')).toBe(false);
+ expect(mount({ style: { minHeight: '100px' } }, false).hasAttribute('data-wsb-dropzone')).toBe(false);
+ });
+});
diff --git a/craft/src/components/layout/Container.tsx b/craft/src/components/layout/Container.tsx
index 4797f83..3c59b1d 100644
--- a/craft/src/components/layout/Container.tsx
+++ b/craft/src/components/layout/Container.tsx
@@ -117,6 +117,7 @@ export const Container: UserComponent = ({
style: outerStyle,
'data-craft-container': 'true',
'data-fullwidth': fullWidth ? '' : undefined,
+ 'data-wsb-dropzone': isEmpty && !style.minHeight ? '' : undefined,
id: idValue,
className: cssClass || undefined,
},