Files
site-builder/craft/src/state/DeviceContext.test.tsx
T
shadowdaoandClaude Opus 5.5 5f99df23a7 feat(site-builder): per-breakpoint style model, device context and responsive export
- utils/breakpoints.ts: single source for 767/768/991/992; head
  VISIBILITY/RESPONSIVE CSS built from it (flex-stacking rule moved from
  768 to the 767 mobile boundary).
- utils/responsive-style.ts: styleTablet/styleMobile partial overrides,
  cascade resolution, pure setters, scoped @media CSS per node.
- state/DeviceContext.tsx: real DeviceProvider (owns the one-time mobile
  forcing from EditorShell); TopBar, overflow menu, ReportIssueModal and
  Canvas read useDevice() instead of props.
- editor/RenderNode.tsx: resolves the effective style for the current
  device in one place and marks overridden nodes with data-wsb-overridden.
  Craft passes an opaque <DefaultRender/> to onRender, so the component
  element is rebuilt with the resolved style (cloneElement cannot work).
- utils/html-export.ts: hidden nodes skipped; overrides emitted as one
  body-level <style data-wsb-responsive> block + data-wsb-r scope attr;
  injectAttrs skips leading <style> elements.
- test-utils/editorHarness.tsx: optional onRender/wrap options.

Refs #45
Refs #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:28:44 -07:00

78 lines
2.4 KiB
TypeScript

import { describe, test, expect, afterEach, vi } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';
import { DeviceProvider, useDevice } from './DeviceContext';
import type { DeviceMode } from '../types';
let container: HTMLDivElement;
let root: Root;
let api: ReturnType<typeof useDevice>;
const Probe: React.FC = () => {
api = useDevice();
return <span data-testid="device">{api.device}</span>;
};
function mount(ui: React.ReactElement) {
container = document.createElement('div');
document.body.appendChild(container);
act(() => {
root = createRoot(container);
root.render(ui);
});
}
const shown = () => container.querySelector('[data-testid="device"]')!.textContent;
function stubMatchMedia(matches: boolean) {
vi.stubGlobal('matchMedia', (q: string) => ({
matches, media: q, onchange: null,
addListener() {}, removeListener() {}, addEventListener() {}, removeEventListener() {}, dispatchEvent: () => false,
}));
}
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.unstubAllGlobals();
});
describe('DeviceContext', () => {
test('without a provider: desktop + a no-op setter', () => {
mount(<Probe />);
expect(shown()).toBe('desktop');
act(() => api.setDevice('mobile'));
expect(shown()).toBe('desktop');
});
test('provider defaults to desktop on a desktop viewport', () => {
stubMatchMedia(false);
mount(<DeviceProvider><Probe /></DeviceProvider>);
expect(shown()).toBe('desktop');
});
test('setDevice propagates to every consumer', () => {
stubMatchMedia(false);
const Other: React.FC = () => <i data-testid="other">{useDevice().device}</i>;
mount(<DeviceProvider><Probe /><Other /></DeviceProvider>);
act(() => api.setDevice('tablet'));
expect(shown()).toBe('tablet');
expect(container.querySelector('[data-testid="other"]')!.textContent).toBe('tablet');
});
test('a mobile viewport forces the mobile canvas once, then respects the user choice', () => {
stubMatchMedia(true);
mount(<DeviceProvider><Probe /></DeviceProvider>);
expect(shown()).toBe('mobile');
act(() => api.setDevice('desktop'));
expect(shown()).toBe('desktop');
});
test('initialDevice seeds the state', () => {
stubMatchMedia(false);
mount(<DeviceProvider initialDevice={'tablet' as DeviceMode}><Probe /></DeviceProvider>);
expect(shown()).toBe('tablet');
});
});