Files
site-builder/craft/src/components/layout/ColumnLayout.stack-parity.test.tsx
T

113 lines
5.7 KiB
TypeScript

import { describe, test, expect, afterEach } from 'vitest';
import React from 'react';
import { exportBodyHtml, RESPONSIVE_CSS_MINIFIED } from '../../utils/html-export';
import { renderEditorHarness, EditorHarness } from '../../test-utils/editorHarness';
import { DeviceProvider } from '../../state/DeviceContext';
import { RenderNode } from '../../editor/RenderNode';
import type { DeviceMode } from '../../types';
// Vitest stubs CSS imports (`?raw` reads back empty); read the real file (same as DropIndicator.test).
const nodeFs = (globalThis as any).process.getBuiltinModule('node:fs');
const EDITOR_CSS: string = nodeFs.readFileSync(`${(globalThis as any).process.cwd()}/src/styles/editor.css`, 'utf8');
/**
* Minor finding: un-stacking Columns on mobile (styleMobile.flexDirection 'row'
* under a stacked tablet) rendered STACKED in the editor but UN-stacked when
* published. Published mobile stacking is the global RESPONSIVE_CSS rule, which
* matches the DESKTOP inline style; the editor's device-emulation copy of that
* rule matched the device-RESOLVED inline style instead.
*/
/** Selector list of every editor.css rule that stacks (flex-direction:column) on the mobile frame. */
function editorMobileStackSelectors(): string {
const out: string[] = [];
const re = /([^{}]+)\{([^{}]*)\}/g;
let m: RegExpExecArray | null;
while ((m = re.exec(EDITOR_CSS))) {
const sel = m[1].replace(/\/\*[\s\S]*?\*\//g, '').trim();
if (sel.includes('[data-device="mobile"]') && /flex-direction:\s*column\s*!important/.test(m[2])) out.push(sel);
}
return out.join(',');
}
/** The published global mobile stacking selector (from RESPONSIVE_CSS). */
const PUBLISHED_STACK_SELECTOR = /\{([^{}]+)\{flex-direction:column!important\}/.exec(RESPONSIVE_CSS_MINIFIED)![1];
const node = (type: string, props: Record<string, unknown>, nodes: string[] = [], extra: Record<string, unknown> = {}) => ({
type: { resolvedName: type }, isCanvas: type === 'Container', props, displayName: type,
custom: {}, hidden: false, nodes, linkedNodes: {}, ...extra,
});
const colsState = (props: Record<string, unknown>) =>
JSON.stringify({
ROOT: node('Container', { tag: 'div', style: {} }, ['cl']),
cl: node('ColumnLayout', { columns: 2, split: '50-50', gap: '16px', anchorId: 'cl', ...props }, [], {
parent: 'ROOT', isCanvas: false, linkedNodes: { 'col-0': 'c0', 'col-1': 'c1' },
}),
c0: node('Container', { tag: 'div', style: {} }, [], { parent: 'cl' }),
c1: node('Container', { tag: 'div', style: {} }, [], { parent: 'cl' }),
});
let h: EditorHarness | null = null;
afterEach(() => { h?.unmount(); h = null; });
function editorStacksOnMobile(state: string, device: DeviceMode = 'mobile'): boolean {
h = renderEditorHarness({
initialState: state,
onRender: RenderNode,
wrap: (tree) => <DeviceProvider initialDevice={device}><div className="canvas-device-frame" data-device={device}>{tree}</div></DeviceProvider>,
});
const el = h.container.querySelector('#cl') as HTMLElement;
return el.matches(editorMobileStackSelectors());
}
function publishedBaseStacks(state: string): boolean {
const doc = document.createElement('div');
doc.innerHTML = exportBodyHtml(state).html;
return (doc.querySelector('#cl') as HTMLElement).matches(PUBLISHED_STACK_SELECTOR);
}
describe('Columns mobile stacking: editor matches published', () => {
test('un-stack on mobile under a stacked tablet -> NOT stacked in the editor (published is not)', () => {
const state = colsState({ style: {}, styleTablet: { flexDirection: 'column' }, styleMobile: { flexDirection: 'row' } });
expect(publishedBaseStacks(state)).toBe(false);
expect(exportBodyHtml(state).html).toMatch(/@media \(max-width:767px\)\{\[data-wsb-r="b-cl"\]\{flex-direction:row!important/);
expect(editorStacksOnMobile(state)).toBe(false);
});
test('desktop row, no override -> stacked in both', () => {
const state = colsState({ style: { flexDirection: 'row' } });
expect(publishedBaseStacks(state)).toBe(true);
expect(editorStacksOnMobile(state)).toBe(true);
});
test('desktop row + mobile row override -> stacked in both (global rule wins published)', () => {
const state = colsState({ style: { flexDirection: 'row' }, styleMobile: { flexDirection: 'row' } });
expect(publishedBaseStacks(state)).toBe(true);
expect(editorStacksOnMobile(state)).toBe(true);
});
});
describe('Columns stack on phones BY DEFAULT (no flex-direction in the props)', () => {
test('published: wrapper carries the cols_ class the global head rule targets; mobile row override is the opt-out', () => {
const html = exportBodyHtml(colsState({ style: {} })).html;
expect(html).toMatch(/<div class="cols_[a-z0-9]+" id="cl"/);
expect(RESPONSIVE_CSS_MINIFIED).toContain('[class^="cols_"]{flex-direction:column!important;align-items:stretch!important}');
const optOut = exportBodyHtml(colsState({ styleMobile: { flexDirection: 'row' } })).html;
expect(optOut).toMatch(/@media \(max-width:767px\)\{\[data-wsb-r="b-cl"\]\{flex-direction:row!important/);
});
test('editor mobile frame stacks default Columns, and a mobile row override un-stacks them', () => {
const stacksDefault = () => {
h = renderEditorHarness({
initialState: colsState({ style: {} }),
onRender: RenderNode,
wrap: (tree) => <DeviceProvider initialDevice="mobile"><div className="canvas-device-frame" data-device="mobile">{tree}</div></DeviceProvider>,
});
const el = h.container.querySelector('#cl') as HTMLElement;
return el.matches(editorMobileStackSelectors());
};
expect(stacksDefault()).toBe(true);
h?.unmount(); h = null;
expect(editorStacksOnMobile(colsState({ styleMobile: { flexDirection: 'row' } }))).toBe(false);
});
});