mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(frames): resolve relative screen= against embedder URL (#2316)
Shared device frames serve at /frames/<name>.html and previously assigned the raw ?screen= value to the inner iframe.src. A project-relative value like screen=screens/foo.html resolved against /frames/, producing /frames/screens/foo.html (404), instead of the embedding project's /api/projects/:id/raw/screens/foo.html. The five frame HTML files now resolve relative ?screen= values against document.referrer when present (the embedding project preview), falling back to location.href so standalone /frames/* loads keep working. Absolute and root-relative paths are passed through unchanged. Adds an e2e Vitest spec that evaluates each frame's inline <script> in a Node vm and asserts iframe.src under five scenarios per file (25 cases total): project-relative against referrer, root-relative pass-through, absolute pass-through, empty referrer fallback, and missing ?screen= no-op. Fixes #2234
This commit is contained in:
parent
80d305858b
commit
8bcd96f5e5
6 changed files with 159 additions and 10 deletions
|
|
@ -149,9 +149,13 @@
|
|||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var raw = qs.get('screen');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
if (!raw) return;
|
||||
var isAbsolute = /^[a-zA-Z][a-zA-Z\d+.\-]*:/.test(raw) || raw.charAt(0) === '/';
|
||||
iframe.src = isAbsolute
|
||||
? raw
|
||||
: new URL(raw, document.referrer || location.href).toString();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -119,10 +119,15 @@
|
|||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var raw = qs.get('screen');
|
||||
var url = qs.get('url');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
if (raw) {
|
||||
var isAbsolute = /^[a-zA-Z][a-zA-Z\d+.\-]*:/.test(raw) || raw.charAt(0) === '/';
|
||||
iframe.src = isAbsolute
|
||||
? raw
|
||||
: new URL(raw, document.referrer || location.href).toString();
|
||||
}
|
||||
if (url) document.getElementById('url-text').textContent = url;
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -87,9 +87,13 @@
|
|||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var raw = qs.get('screen');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
if (!raw) return;
|
||||
var isAbsolute = /^[a-zA-Z][a-zA-Z\d+.\-]*:/.test(raw) || raw.charAt(0) === '/';
|
||||
iframe.src = isAbsolute
|
||||
? raw
|
||||
: new URL(raw, document.referrer || location.href).toString();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -166,9 +166,13 @@
|
|||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var raw = qs.get('screen');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
if (!raw) return;
|
||||
var isAbsolute = /^[a-zA-Z][a-zA-Z\d+.\-]*:/.test(raw) || raw.charAt(0) === '/';
|
||||
iframe.src = isAbsolute
|
||||
? raw
|
||||
: new URL(raw, document.referrer || location.href).toString();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -126,9 +126,13 @@
|
|||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var raw = qs.get('screen');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
if (!raw) return;
|
||||
var isAbsolute = /^[a-zA-Z][a-zA-Z\d+.\-]*:/.test(raw) || raw.charAt(0) === '/';
|
||||
iframe.src = isAbsolute
|
||||
? raw
|
||||
: new URL(raw, document.referrer || location.href).toString();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
128
e2e/tests/frames/screen-resolution.test.ts
Normal file
128
e2e/tests/frames/screen-resolution.test.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url));
|
||||
const framesDir = path.join(repoRoot, 'assets', 'frames');
|
||||
|
||||
const FRAME_FILES = [
|
||||
'iphone-15-pro.html',
|
||||
'android-pixel.html',
|
||||
'ipad-pro.html',
|
||||
'macbook.html',
|
||||
'browser-chrome.html',
|
||||
] as const;
|
||||
|
||||
function extractFrameScript(htmlPath: string): string {
|
||||
const html = readFileSync(htmlPath, 'utf8');
|
||||
const matches = html.matchAll(/<script>([\s\S]*?)<\/script>/g);
|
||||
for (const m of matches) {
|
||||
const body = m[1];
|
||||
if (body != null && (body.includes("qs.get('screen')") || body.includes('qs.get("screen")'))) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
throw new Error(`No screen= script block found in ${htmlPath}`);
|
||||
}
|
||||
|
||||
interface MockElement {
|
||||
id: string;
|
||||
src: string;
|
||||
textContent: string;
|
||||
}
|
||||
|
||||
function makeElement(id: string): MockElement {
|
||||
return { id, src: 'about:blank', textContent: '' };
|
||||
}
|
||||
|
||||
interface RunOptions {
|
||||
search: string;
|
||||
href: string;
|
||||
referrer?: string;
|
||||
}
|
||||
|
||||
interface RunResult {
|
||||
iframeSrc: string;
|
||||
}
|
||||
|
||||
function runFrame(htmlPath: string, opts: RunOptions): RunResult {
|
||||
const script = extractFrameScript(htmlPath);
|
||||
const screenEl = makeElement('screen');
|
||||
const urlEl = makeElement('url-text');
|
||||
const elements: Record<string, MockElement> = {
|
||||
screen: screenEl,
|
||||
'url-text': urlEl,
|
||||
};
|
||||
const documentMock = {
|
||||
getElementById: (id: string): MockElement | null => elements[id] ?? null,
|
||||
referrer: opts.referrer ?? '',
|
||||
};
|
||||
const locationMock = { search: opts.search, href: opts.href };
|
||||
const sandbox: Record<string, unknown> = {
|
||||
document: documentMock,
|
||||
location: locationMock,
|
||||
window: { location: locationMock, document: documentMock },
|
||||
URLSearchParams,
|
||||
URL,
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(script, sandbox);
|
||||
return { iframeSrc: screenEl.src };
|
||||
}
|
||||
|
||||
describe('assets/frames/<device>.html ?screen= resolution (#2234)', () => {
|
||||
describe.each(FRAME_FILES)('%s', (file) => {
|
||||
const htmlPath = path.join(framesDir, file);
|
||||
|
||||
it('resolves a project-relative screen path against document.referrer', () => {
|
||||
const result = runFrame(htmlPath, {
|
||||
search: '?screen=screens/foo.html',
|
||||
href: `http://localhost/frames/${file}?screen=screens/foo.html`,
|
||||
referrer: 'http://localhost/api/projects/abc/raw/index.html',
|
||||
});
|
||||
expect(result.iframeSrc).toBe('http://localhost/api/projects/abc/raw/screens/foo.html');
|
||||
});
|
||||
|
||||
it('keeps a root-relative screen path resolving to the same path', () => {
|
||||
const result = runFrame(htmlPath, {
|
||||
search: '?screen=/api/projects/abc/raw/screens/foo.html',
|
||||
href: `http://localhost/frames/${file}?screen=/api/projects/abc/raw/screens/foo.html`,
|
||||
referrer: 'http://localhost/api/projects/abc/raw/index.html',
|
||||
});
|
||||
const pathname = result.iframeSrc.startsWith('http')
|
||||
? new URL(result.iframeSrc).pathname
|
||||
: result.iframeSrc;
|
||||
expect(pathname).toBe('/api/projects/abc/raw/screens/foo.html');
|
||||
});
|
||||
|
||||
it('keeps an absolute screen URL unchanged', () => {
|
||||
const result = runFrame(htmlPath, {
|
||||
search: '?screen=https://example.com/inner.html',
|
||||
href: `http://localhost/frames/${file}?screen=https://example.com/inner.html`,
|
||||
referrer: 'http://localhost/api/projects/abc/raw/index.html',
|
||||
});
|
||||
expect(result.iframeSrc).toBe('https://example.com/inner.html');
|
||||
});
|
||||
|
||||
it('falls back to the frame URL when document.referrer is empty', () => {
|
||||
const result = runFrame(htmlPath, {
|
||||
search: '?screen=screens/foo.html',
|
||||
href: `http://localhost/frames/${file}?screen=screens/foo.html`,
|
||||
referrer: '',
|
||||
});
|
||||
expect(result.iframeSrc).toBe('http://localhost/frames/screens/foo.html');
|
||||
});
|
||||
|
||||
it('leaves iframe.src untouched when no screen param is provided', () => {
|
||||
const result = runFrame(htmlPath, {
|
||||
search: '',
|
||||
href: `http://localhost/frames/${file}`,
|
||||
referrer: 'http://localhost/api/projects/abc/raw/index.html',
|
||||
});
|
||||
expect(result.iframeSrc).toBe('about:blank');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue