Site builder: security & data-loss hardening + unified asset picker + audit backlog #3
@@ -91,3 +91,22 @@ describe('ContentSlider.toHtml autoplay silences aria-live and is pausable (F-ex
|
|||||||
expect(html).not.toMatch(/visibilitychange/);
|
expect(html).not.toMatch(/visibilitychange/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ContentSlider.toHtml renders slide.imageSrc as a background-image (INT)', () => {
|
||||||
|
test('a slide with imageSrc set exports a background-image referencing it', () => {
|
||||||
|
const slidesWithImage = [
|
||||||
|
{ type: 'image' as const, imageSrc: 'https://example.com/photo.jpg', heading: 'One' },
|
||||||
|
];
|
||||||
|
const { html } = toHtml({ slides: slidesWithImage }, '');
|
||||||
|
expect(html).toContain("background-image:url('https://example.com/photo.jpg')");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a slide with no imageSrc falls back to bgColor (no broken/empty background-image url)', () => {
|
||||||
|
const slidesNoImage = [
|
||||||
|
{ type: 'image' as const, imageSrc: '', heading: 'One', bgColor: '#123456' },
|
||||||
|
];
|
||||||
|
const { html } = toHtml({ slides: slidesNoImage }, '');
|
||||||
|
expect(html).not.toContain('background-image:url(');
|
||||||
|
expect(html).toContain('background-color:#123456');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import React from 'react';
|
||||||
|
import { createRoot, Root } from 'react-dom/client';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
/* MediaStylePanel (via useNodeProp/ArrayPropEditor in ./shared) only needs
|
||||||
|
useEditor from @craftjs/core. Mock it following the DOM-harness pattern
|
||||||
|
used in Footer.editguard.test.tsx / AssetPicker.test.tsx (no
|
||||||
|
@testing-library/react in this repo) so setProp calls can be observed
|
||||||
|
without mounting a real <Editor> tree. */
|
||||||
|
const setPropSpy = vi.fn((_id: string, updater: (p: any) => void) => {
|
||||||
|
updater(lastProps);
|
||||||
|
});
|
||||||
|
let lastProps: any;
|
||||||
|
|
||||||
|
vi.mock('@craftjs/core', () => ({
|
||||||
|
useEditor: () => ({ actions: { setProp: setPropSpy } }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../utils/assets', () => ({
|
||||||
|
uploadAsset: vi.fn(),
|
||||||
|
listAssets: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { MediaStylePanel } from './MediaStylePanel';
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let root: Root;
|
||||||
|
|
||||||
|
function render(ui: React.ReactElement) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
act(() => {
|
||||||
|
root = createRoot(container);
|
||||||
|
root.render(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmount() {
|
||||||
|
act(() => { root.unmount(); });
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function q<T extends Element = Element>(testId: string): T | null {
|
||||||
|
return container.querySelector(`[data-testid="${testId}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValue(input: HTMLInputElement, value: string) {
|
||||||
|
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')!.set!;
|
||||||
|
setter.call(input, value);
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setPropSpy.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (container) unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* INT: ContentSlider (src/components/sections/ContentSlider.tsx) reads
|
||||||
|
* `slide.imageSrc` in both render and toHtml, but the Slides array editor
|
||||||
|
* here guarded on `item.image !== undefined` and wrote `image` via
|
||||||
|
* AssetPicker's onChange -- a key that ContentSlider never reads. Default
|
||||||
|
* slides (`imageSrc:''`, no `image` key) therefore showed NO image picker at
|
||||||
|
* all (the guard was never true), and even a manually-added `image` value
|
||||||
|
* was a silent no-op on export/render.
|
||||||
|
*/
|
||||||
|
describe('MediaStylePanel Slides editor uses imageSrc (INT)', () => {
|
||||||
|
test('a slide shaped like defaultSlides (imageSrc present, no `image` key) renders the image AssetPicker', () => {
|
||||||
|
lastProps = { slides: [{ type: 'image', imageSrc: '', heading: 'First Slide', text: '', bgColor: '' }] };
|
||||||
|
render(<MediaStylePanel selectedId="cs-1" nodeProps={lastProps} />);
|
||||||
|
|
||||||
|
// Before the fix, the guard `item.image !== undefined` was false for
|
||||||
|
// this default-shaped slide, so no AssetPicker rendered at all.
|
||||||
|
expect(q('asset-picker-compact')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('editing the slide image writes imageSrc (not image) via setProp', () => {
|
||||||
|
lastProps = { slides: [{ type: 'image', imageSrc: '', heading: 'First Slide', text: '', bgColor: '' }] };
|
||||||
|
render(<MediaStylePanel selectedId="cs-1" nodeProps={lastProps} />);
|
||||||
|
|
||||||
|
const urlInput = q<HTMLInputElement>('asset-picker-url-input')!;
|
||||||
|
expect(urlInput).not.toBeNull();
|
||||||
|
setValue(urlInput, 'https://example.com/slide.jpg');
|
||||||
|
act(() => {
|
||||||
|
urlInput.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setPropSpy).toHaveBeenCalled();
|
||||||
|
const updatedSlide = lastProps.slides[0];
|
||||||
|
expect(updatedSlide.imageSrc).toBe('https://example.com/slide.jpg');
|
||||||
|
expect(updatedSlide.image).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adding a new slide (emptyItem) includes imageSrc, matching defaultSlides shape', () => {
|
||||||
|
lastProps = { slides: [] };
|
||||||
|
render(<MediaStylePanel selectedId="cs-1" nodeProps={lastProps} />);
|
||||||
|
|
||||||
|
// The "+ Add Item" button for the Slides ArrayPropEditor.
|
||||||
|
const addButtons = Array.from(container.querySelectorAll('button')).filter(
|
||||||
|
(b) => b.textContent?.includes('Add Item'),
|
||||||
|
);
|
||||||
|
expect(addButtons.length).toBeGreaterThan(0);
|
||||||
|
act(() => {
|
||||||
|
addButtons[0].dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setPropSpy).toHaveBeenCalled();
|
||||||
|
expect(lastProps.slides).toHaveLength(1);
|
||||||
|
expect(lastProps.slides[0]).toHaveProperty('imageSrc');
|
||||||
|
expect(lastProps.slides[0].image).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -145,14 +145,14 @@ export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
|
|||||||
});
|
});
|
||||||
}} placeholder="Text" rows={2} style={{ ...smallInputStyle, resize: 'vertical' }} />
|
}} placeholder="Text" rows={2} style={{ ...smallInputStyle, resize: 'vertical' }} />
|
||||||
)}
|
)}
|
||||||
{item.image !== undefined && (
|
{item.imageSrc !== undefined && (
|
||||||
<AssetPicker
|
<AssetPicker
|
||||||
variant="compact"
|
variant="compact"
|
||||||
value={item.image || ''}
|
value={item.imageSrc || ''}
|
||||||
onChange={(url) => {
|
onChange={(url) => {
|
||||||
actions.setProp(selectedId, (props: any) => {
|
actions.setProp(selectedId, (props: any) => {
|
||||||
const updated = [...(props.slides || [])];
|
const updated = [...(props.slides || [])];
|
||||||
updated[index] = { ...updated[index], image: url };
|
updated[index] = { ...updated[index], imageSrc: url };
|
||||||
props.slides = updated;
|
props.slides = updated;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -160,7 +160,7 @@ export const MediaStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodePro
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
emptyItem={{ heading: 'New Slide', text: '', image: '' }}
|
emptyItem={{ type: 'image', imageSrc: '', heading: 'New Slide', text: '', bgColor: '' }}
|
||||||
/>
|
/>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user