Files
site-builder/craft/scripts/e2e-sitesmith-editor-assets-entry.ts
T
shadowdaoandClaude Sonnet 5.5 c6809293c4 sitesmith: show uploaded images in the editor canvas and Preview
Sitesmith writes uploads as assets/<file>, which the editor (panel origin) cannot load.
After the repair + allow-list cleaner, apply now converts every real-upload reference to the
same panel proxy URL the AssetPicker stores (publish rewrites it back to assets/<file>);
canvas summaries sent to the model show assets/<file> again. Covers apply, self-review
patches and regenerate-section. Adds unit, Craft integration and a Chromium+Firefox
browser suite (test:browser:sitesmith-editor-assets) incl. the real PHP publish rewrite.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185ukXgk6UunFCKyXqLe2Cq
2026-11-02 17:43:06 -08:00

49 lines
3.6 KiB
TypeScript

// Bundled (esbuild) by scripts/e2e-sitesmith-editor-assets.mjs: one Sitesmith response that places uploads as
// `assets/<file>`, run through the apply-time order (cleaner, then the editor-URL conversion) and exported the way the
// editor canvas / Preview and a publish do. Three variants: as the model wrote it (the bug), converted (the fix), and
// the same page as a manual AssetPicker insertion (the reference the fix must equal).
import { cleanAiResponse } from '../src/sitesmith/clean-ai-response';
import { toEditorAssetUrls, editorProxyUrl } from '../src/sitesmith/editor-asset-urls';
import { flattenTreeForCraft } from '../src/utils/craft-tree';
import { exportBodyHtml, exportComposedPreviewHtml } from '../src/utils/html-export';
import { DEFAULT_SITE_DESIGN } from '../src/state/SiteDesignContext';
import type { SitesmithResponse } from '../src/types/sitesmith';
export const SITE_ID = 7;
/** Real upload names. Upload() sanitises to [A-Za-z0-9_.-]; the other two arrive via FTP / stock import. */
export const FILES = ['1700000000_aa11bb22_hero.png', '1700000001_bb22cc33_logo+v2.png', 'my photo.png', 'café.png'];
export const MISSING = 'assets/does-not-exist.png';
export const ASSETS = FILES.map((f) => ({ filename: f, relUrl: `assets/${f}`, url: editorProxyUrl(SITE_ID, f), width: 800, height: 600 }));
// what the model writes: relUrl verbatim, but the cleaner only accepts [A-Za-z0-9._%~+-], so a space / unicode name arrives %-encoded
const rel = (i: number) => 'assets/' + FILES[i].split('').map((c) => (/[A-Za-z0-9._~+-]/.test(c) ? c : encodeURIComponent(c))).join('');
const n = (type: string, props: Record<string, unknown>, nodes?: any[]) => ({ type: { resolvedName: type }, props, ...(nodes ? { nodes } : {}) });
function page(src: (i: number) => string): SitesmithResponse {
return { type: 'replace', scope: 'page', message: 'm', pages: [{ name: 'Home', tree: n('Container', { node_id: 'ROOT', tag: 'div' }, [
n('Navbar', { node_id: 'nav', logoType: 'image', logoImage: src(1), logoUrl: '/', links: [{ text: 'Home', href: '/' }] }),
n('BackgroundSection', { node_id: 'bg', bgImage: src(0), overlayOpacity: 0.2 }, [n('Heading', { node_id: 'h', text: 'Hello', level: 'h1' })]),
n('ImageBlock', { node_id: 'i1', src: src(0), alt: 'plain' }),
n('ImageBlock', { node_id: 'i2', src: src(1), alt: 'plus' }),
n('ImageBlock', { node_id: 'i3', src: src(2), alt: 'space' }),
n('ImageBlock', { node_id: 'i4', src: src(3), alt: 'unicode' }),
n('Container', { node_id: 'c', tag: 'div', style: { minHeight: '60px', backgroundImage: `url(${src(0)})` } }),
n('HtmlBlock', { node_id: 'html', code: `<img id="rich" src="${src(0)}" alt="rich">` }),
n('ImageBlock', { node_id: 'i5', src: MISSING, alt: 'missing' }),
]) }] };
}
type Variant = 'bug' | 'fix' | 'manual';
function tree(v: Variant) {
// manual: the proxy URL is what the picker stores (no cleaner in that path); bug / fix: the model's `assets/<file>`, cleaned
let resp: SitesmithResponse = v === 'manual' ? page((i) => ASSETS[i].url) : cleanAiResponse(page(rel));
if (v === 'fix') resp = toEditorAssetUrls(resp, ASSETS, { siteId: SITE_ID }).resp;
if (resp.type !== 'replace') throw new Error('unexpected');
return JSON.stringify(flattenTreeForCraft(resp.pages[0].tree).nodes);
}
export function body(v: Variant): string { return exportBodyHtml(tree(v)).html; }
/** The editor Preview / canvas document. */
export function editorDoc(v: Variant): string {
const state = tree(v);
return exportComposedPreviewHtml(state, exportBodyHtml(state).html, { title: 'Home', includeFonts: false, design: DEFAULT_SITE_DESIGN });
}