469 lines
21 KiB
JavaScript
469 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ObsiGate — Inline edition tests (ROADMAP #93).
|
|
*
|
|
* « Editer » and « Forge » must replace the document read view instead of
|
|
* opening an overlay on top of it (`#editor-container` is moved into the
|
|
* content area, so the AI assistant sidebar stays usable while editing).
|
|
*
|
|
* Covers:
|
|
* - frontend/js/editor-inline.js — DOM behaviour (mount/unmount/classes)
|
|
* - the wiring of that module in viewer.js / utils.js / sync.js / bookslm.js
|
|
* (static checks, like tests/frontend/toolbar-order.test.mjs)
|
|
* - the Forge iframe reload hook (frontend/editor-poc.html)
|
|
* - the CSS that makes the editor fill the content area (frontend/style.css)
|
|
*
|
|
* Usage: node tests/frontend/editor-inline.test.mjs
|
|
*/
|
|
|
|
import { strict as assert } from "node:assert";
|
|
import { readFileSync } from "node:fs";
|
|
import { JSDOM } from "jsdom";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import path from "node:path";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
|
|
const dom = new JSDOM(
|
|
`<!DOCTYPE html><html><body>
|
|
<div class="app-container">
|
|
<div class="content-wrapper">
|
|
<div class="content-area" id="content-area">
|
|
<div class="file-header"><div class="file-title">Accueil</div></div>
|
|
<div class="md-content" id="file-rendered-content"><p>Lecture</p></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="editor-modal" id="editor-modal">
|
|
<div class="editor-container" id="editor-container">
|
|
<div class="editor-header"><span class="editor-brand">ObsiGate</span></div>
|
|
<div class="editor-body editor-body-cm" id="editor-body"></div>
|
|
<div class="editor-footer"></div>
|
|
</div>
|
|
</div>
|
|
</body></html>`,
|
|
{ url: "https://example.com/", pretendToBeVisual: true },
|
|
);
|
|
|
|
const w = dom.window;
|
|
globalThis.window = w;
|
|
globalThis.document = w.document;
|
|
globalThis.HTMLElement = w.HTMLElement;
|
|
globalThis.Element = w.Element;
|
|
globalThis.Node = w.Node;
|
|
|
|
const mod = await import(
|
|
pathToFileURL(path.join(REPO_ROOT, "frontend", "js", "editor-inline.js")).href
|
|
);
|
|
const {
|
|
INLINE_CONTAINER_CLASS,
|
|
INLINE_HOST_CLASS,
|
|
INLINE_MODAL_CLASS,
|
|
getEditorModal,
|
|
getEditorContainer,
|
|
getInlineHostArea,
|
|
isInlineEditorActive,
|
|
mountEditorInline,
|
|
unmountEditorInline,
|
|
queryEditor,
|
|
} = mod;
|
|
|
|
let testCount = 0;
|
|
let failCount = 0;
|
|
|
|
function test(name, fn) {
|
|
testCount++;
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
} catch (err) {
|
|
failCount++;
|
|
console.error(` ✗ ${name}\n ${err.message}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
const modal = () => document.getElementById("editor-modal");
|
|
const container = () => document.getElementById("editor-container");
|
|
const area = () => document.getElementById("content-area");
|
|
|
|
function reset() {
|
|
const c = container();
|
|
const m = modal();
|
|
if (c.parentElement !== m) m.appendChild(c);
|
|
c.classList.remove(INLINE_CONTAINER_CLASS);
|
|
m.classList.remove(INLINE_MODAL_CLASS);
|
|
area().classList.remove(INLINE_HOST_CLASS);
|
|
area().innerHTML = '<div class="md-content" id="file-rendered-content"><p>Lecture</p></div>';
|
|
delete w._activePaneContentArea;
|
|
delete w.PaneManager;
|
|
delete w.getActiveContentArea;
|
|
}
|
|
|
|
console.log("Inline edition (#93) — editor-inline.js");
|
|
|
|
test("lookup helpers resolve the container and the host area", () => {
|
|
reset();
|
|
assert.equal(getEditorModal(), modal());
|
|
assert.equal(getEditorContainer(), container());
|
|
assert.equal(getInlineHostArea(), area());
|
|
assert.equal(isInlineEditorActive(), false);
|
|
});
|
|
|
|
test("mount moves the container into the content area and drops the read view", () => {
|
|
reset();
|
|
assert.equal(mountEditorInline(area()), true);
|
|
assert.equal(container().parentElement, area());
|
|
assert.equal(area().querySelector(".md-content"), null, "read view replaced");
|
|
assert.ok(container().classList.contains(INLINE_CONTAINER_CLASS));
|
|
assert.ok(area().classList.contains(INLINE_HOST_CLASS));
|
|
assert.ok(modal().classList.contains(INLINE_MODAL_CLASS));
|
|
assert.equal(isInlineEditorActive(), true);
|
|
});
|
|
|
|
test("mount keeps the overlay mounted so the mobile ribbon keeps working", () => {
|
|
reset();
|
|
mountEditorInline(area());
|
|
assert.ok(modal(), "overlay still in the document");
|
|
assert.equal(container().closest("#editor-modal"), null, "container left the overlay");
|
|
});
|
|
|
|
test("mount is idempotent on the same host area", () => {
|
|
reset();
|
|
mountEditorInline(area());
|
|
assert.equal(mountEditorInline(area()), true);
|
|
assert.equal(area().querySelectorAll(".editor-container").length, 1);
|
|
});
|
|
|
|
test("mount refuses a missing area", () => {
|
|
reset();
|
|
assert.equal(mountEditorInline(null), false);
|
|
assert.equal(isInlineEditorActive(), false);
|
|
});
|
|
|
|
test("unmount returns the host area, restores the overlay and clears classes", () => {
|
|
reset();
|
|
mountEditorInline(area());
|
|
const host = unmountEditorInline();
|
|
assert.equal(host, area());
|
|
assert.equal(container().parentElement, modal());
|
|
assert.equal(container().classList.contains(INLINE_CONTAINER_CLASS), false);
|
|
assert.equal(area().classList.contains(INLINE_HOST_CLASS), false);
|
|
assert.equal(modal().classList.contains(INLINE_MODAL_CLASS), false);
|
|
assert.equal(isInlineEditorActive(), false);
|
|
assert.equal(area().children.length, 0, "host area left empty for the read view re-render");
|
|
});
|
|
|
|
test("unmount without an inline session is a no-op", () => {
|
|
reset();
|
|
assert.equal(unmountEditorInline(), null);
|
|
assert.equal(container().parentElement, modal());
|
|
});
|
|
|
|
test("queryEditor follows the container (modal → content area)", () => {
|
|
reset();
|
|
assert.equal(queryEditor(".editor-brand").textContent, "ObsiGate");
|
|
mountEditorInline(area());
|
|
assert.ok(queryEditor(".editor-brand"), "brand reachable while mounted inline");
|
|
assert.equal(document.getElementById("editor-modal").querySelector(".editor-brand"), null, "brand really left the overlay");
|
|
unmountEditorInline();
|
|
assert.ok(document.getElementById("editor-modal").querySelector(".editor-brand"), "brand back in the overlay");
|
|
reset();
|
|
});
|
|
|
|
test("host area follows the active pane in split view", () => {
|
|
reset();
|
|
const paneArea = document.createElement("div");
|
|
paneArea.className = "content-area pane-content";
|
|
document.body.appendChild(paneArea);
|
|
w.PaneManager = { isSplit: () => true };
|
|
w.getActiveContentArea = () => paneArea;
|
|
assert.equal(getInlineHostArea(), paneArea);
|
|
// A transient render override (pane-manager _renderFileInPane) wins.
|
|
w._activePaneContentArea = area();
|
|
assert.equal(getInlineHostArea(), area());
|
|
assert.equal(mountEditorInline(paneArea), true);
|
|
assert.equal(container().parentElement, paneArea);
|
|
unmountEditorInline();
|
|
paneArea.remove();
|
|
reset();
|
|
});
|
|
|
|
// ── Static wiring checks ───────────────────────────────────────────────────
|
|
|
|
const read = (...parts) => readFileSync(path.join(REPO_ROOT, ...parts), "utf8");
|
|
|
|
const viewerSrc = read("frontend", "js", "viewer.js");
|
|
const utilsSrc = read("frontend", "js", "utils.js");
|
|
const syncSrc = read("frontend", "js", "sync.js");
|
|
const bookslmSrc = read("frontend", "js", "bookslm.js");
|
|
const indexSrc = read("frontend", "index.html");
|
|
const cssSrc = read("frontend", "style.css");
|
|
const forgeSrc = read("frontend", "editor-poc.html");
|
|
|
|
console.log("Inline edition (#93) — wiring");
|
|
|
|
test("utils.js / viewer.js import every editor-inline helper they call", () => {
|
|
const helpers = ["getEditorContainer", "getInlineHostArea", "isInlineEditorActive", "mountEditorInline", "unmountEditorInline", "queryEditor"];
|
|
const strip = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
for (const [name, raw] of [["utils.js", utilsSrc], ["viewer.js", viewerSrc]]) {
|
|
const src = strip(raw);
|
|
const marker = "from './editor-inline.js';";
|
|
const end = raw.indexOf(marker);
|
|
const start = end === -1 ? -1 : raw.lastIndexOf("import {", end);
|
|
const imported =
|
|
end === -1 || start === -1
|
|
? []
|
|
: raw
|
|
.slice(start + "import {".length, end)
|
|
.replace(/\}/g, "")
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
for (const helper of helpers) {
|
|
if (new RegExp(`\\b${helper}\\(`).test(src)) {
|
|
assert.ok(imported.includes(helper), `${name} calls ${helper}() without importing it`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test("index.html gives the editor container a stable id", () => {
|
|
assert.match(indexSrc, /<div class="editor-container" id="editor-container">/);
|
|
});
|
|
|
|
test("viewer.js mounts the editor inline when rendering is taken over", () => {
|
|
assert.ok(viewerSrc.includes("import { isInlineEditorActive, queryEditor } from './editor-inline.js';"));
|
|
const guard = viewerSrc.match(/export function renderFile\(data\) \{([\s\S]*?)\n const area = getContentArea\(\);/);
|
|
assert.ok(guard, "renderFile head not found");
|
|
assert.match(guard[1], /if \(isInlineEditorActive\(\)\) detachInlineEditor\(\);/);
|
|
});
|
|
|
|
test("viewer.js Forge button mounts inline and sizes the iframe accordingly", () => {
|
|
assert.match(viewerSrc, /var inline = activateInlineEditor\(data\.vault, data\.path\);/);
|
|
assert.ok(viewerSrc.includes('var brand = queryEditor(".editor-brand");'), "header query must follow the container");
|
|
assert.equal(viewerSrc.includes('modal.querySelector(".editor-brand")'), false, "modal-scoped brand query would break inline mode");
|
|
assert.match(
|
|
viewerSrc,
|
|
/iframe\.style\.cssText = "width:100%;height:" \+ \(inline \? "100%" : "82vh"\) \+ ";border:none;display:block;";/,
|
|
);
|
|
});
|
|
|
|
test("utils.js activateInlineEditor only takes over the displayed document", () => {
|
|
const fn = utilsSrc.match(/export function activateInlineEditor\(([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "activateInlineEditor not found");
|
|
assert.match(fn[1], /state\.currentVault !== vaultName \|\| state\.currentPath !== filePath/);
|
|
assert.match(fn[1], /mountEditorInline\(area\)/);
|
|
});
|
|
|
|
test("utils.js openEditor mounts inline, closeEditor releases the host area", () => {
|
|
assert.match(utilsSrc, /activateInlineEditor\(vaultName, filePath\);/);
|
|
const close = utilsSrc.match(/function closeEditor\(\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(close, "closeEditor not found");
|
|
assert.match(close[1], /const host = unmountEditorInline\(\);/);
|
|
assert.match(close[1], /if \(host\) \{/);
|
|
});
|
|
|
|
test("utils.js detachInlineEditor tears the session down without re-rendering", () => {
|
|
const fn = utilsSrc.match(/export function detachInlineEditor\(\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "detachInlineEditor not found");
|
|
assert.match(fn[1], /if \(!isInlineEditorActive\(\)\) return;/);
|
|
assert.match(fn[1], /state\.editorView\.destroy\(\)/);
|
|
assert.match(fn[1], /unmountEditorInline\(\)/);
|
|
});
|
|
|
|
test("utils.js reloads the displayed document after an AI write", () => {
|
|
const fn = utilsSrc.match(/async function reloadExternalWrite\(vault, path, force = false\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "reloadExternalWrite not found");
|
|
assert.match(fn[1], /postMessage\(\{ type: 'parent-reload', force: !!force \}, '\*'\)/);
|
|
assert.match(fn[1], /suppressAutoSaveOnce = true;/);
|
|
assert.match(utilsSrc, /window\.addEventListener\("obsigate:file-written"/);
|
|
assert.match(utilsSrc, /if \(suppressAutoSaveOnce\) \{/, "autosave skipped on programmatic reload");
|
|
});
|
|
|
|
test("utils.js does not clobber unsaved edits on a non-forced reload (BUG-055)", () => {
|
|
const fn = utilsSrc.match(/async function reloadExternalWrite\(vault, path, force = false\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "reloadExternalWrite not found");
|
|
// Guard before the CodeMirror dispatch
|
|
assert.match(fn[1], /if \(!force && dirtyDot && dirtyDot\.classList\.contains\("dirty"\)\) return;/);
|
|
// The AI write forces the reload past unsaved local changes
|
|
assert.match(utilsSrc, /reloadExternalWrite\(detail\.vault, detail\.path, true\);/);
|
|
});
|
|
|
|
test("sync.js forge-close goes through the shared close path", () => {
|
|
assert.ok(syncSrc.includes("import { closeEditor, reloadExternalWrite } from './utils.js';"), "utils import missing");
|
|
const handler = syncSrc.match(/if \(e\.data\.type === 'forge-close'\) \{([\s\S]*?)\n \}/);
|
|
assert.ok(handler, "forge-close handler not found");
|
|
assert.match(handler[1], /closeEditor\(\);/);
|
|
});
|
|
|
|
test("sync.js keeps an open edition session alive on external file changes", () => {
|
|
const sse = syncSrc.match(/const changed = \(data\.changes \|\| \[\]\)([\s\S]*?)\n \}/);
|
|
assert.ok(sse, "SSE index_updated refresh block not found");
|
|
assert.ok(sse[1].includes("reloadExternalWrite(state.currentVault, state.currentPath);"), "SSE refresh must not re-render the read view");
|
|
assert.ok(!/^\s*openFile\(state\.currentVault/m.test(sse[1]), "openFile must not be called directly here");
|
|
});
|
|
|
|
test("tab activation releases an inline edition session first", () => {
|
|
const uiActivate = read("frontend", "js", "ui.js").match(/async activate\(tabId\) \{([\s\S]*?)\n \},/);
|
|
assert.ok(uiActivate, "TabManager.activate not found");
|
|
assert.match(uiActivate[1], /detachInlineEditor\(\);/);
|
|
});
|
|
|
|
test("pane activation releases an inline edition session first", () => {
|
|
const paneSrc = read("frontend", "js", "pane-manager.js");
|
|
const paneActivate = paneSrc.match(/async activate\(tabId\) \{([\s\S]*?)\n const cache/);
|
|
assert.ok(paneActivate, "PaneTabManager.activate not found");
|
|
assert.match(paneActivate[1], /detachInlineEditor\(\);/);
|
|
});
|
|
|
|
test("bookslm.js reports the edited document and the assistant writes", () => {
|
|
assert.match(bookslmSrc, /const editing = this\._editingDocument\(\);/);
|
|
assert.match(bookslmSrc, /if \(editing\) ctx\.editing = editing;/);
|
|
assert.match(bookslmSrc, /this\._notifyFileWritten\(data\);/);
|
|
const notify = bookslmSrc.match(/_notifyFileWritten\(data\) \{([\s\S]*?)\n \}/);
|
|
assert.ok(notify, "_notifyFileWritten not found");
|
|
for (const tool of ["edit_file", "append_to_file", "create_file", "restore_backup"]) {
|
|
assert.ok(notify[1].includes(`'${tool}'`), `${tool} missing from the write tools`);
|
|
}
|
|
assert.match(notify[1], /new CustomEvent\('obsigate:file-written'/);
|
|
});
|
|
|
|
test("editor-poc.html reloads Forge buffer on parent-reload", () => {
|
|
assert.match(forgeSrc, /if \(e\.data\.type === 'parent-reload'\) \{/);
|
|
const handler = forgeSrc.match(/if \(e\.data\.type === 'parent-reload'\) \{([\s\S]*?)\n \}/);
|
|
assert.ok(handler, "parent-reload handler not found");
|
|
assert.match(handler[1], /clearTimeout\(saveTimer\);/);
|
|
assert.match(handler[1], /loadFile\(\);/);
|
|
});
|
|
|
|
test("style.css lets the inline editor fill the content area", () => {
|
|
assert.match(cssSrc, /\.editor-modal\.editor-inline-mode \{/);
|
|
assert.match(cssSrc, /\.editor-modal\.editor-inline-mode \{[\s\S]*?pointer-events: none;/);
|
|
assert.match(cssSrc, /\.content-area\.editor-inline-host,[\s\S]*?display: flex;/);
|
|
const fill = cssSrc.match(/\.content-area\.editor-inline-host \.editor-container\.editor-inline,[\s\S]*?\{([\s\S]*?)\n\}/);
|
|
assert.ok(fill, "inline container rule not found");
|
|
assert.match(fill[1], /flex: 1;/);
|
|
assert.match(fill[1], /height: auto;/, "must win over the mobile 100vh rule");
|
|
assert.match(fill[1], /max-width: none;/);
|
|
});
|
|
// ── BUG-045: one scroll surface only in the CodeMirror editor ──
|
|
test("style.css: #editor-body scrolls through the CodeMirror scroller only", () => {
|
|
const body = cssSrc.match(/\.editor-body\.editor-body-cm \{([^}]*)\}/);
|
|
assert.ok(body, "combined .editor-body.editor-body-cm rule not found");
|
|
assert.match(body[1], /overflow:\s*hidden;/, "editor body must not scroll itself");
|
|
assert.match(body[1], /display:\s*flex;/);
|
|
assert.match(body[1], /flex-direction:\s*column;/);
|
|
const cm = cssSrc.match(/\.editor-body\.editor-body-cm \.cm-editor \{([^}]*)\}/);
|
|
assert.ok(cm, ".cm-editor fill rule not found");
|
|
assert.match(cm[1], /flex:\s*1 1 auto;/);
|
|
assert.match(cm[1], /height:\s*auto;/, "100% height below the AI toolbar row caused the double scrollbar");
|
|
// The global legacy override must be gone: scroller auto-height + forced overflow-y.
|
|
assert.ok(!/\.cm-scroller \{[^}]*min-height:\s*100%;/.test(cssSrc),
|
|
"global .cm-scroller min-height override reintroduces the double scrollbar");
|
|
});
|
|
|
|
// ── BUG-054: the shared save button must not stay stuck on the spinner ──
|
|
test("utils.js resetSaveButton restores the checkmark and re-enables the button", () => {
|
|
const fn = utilsSrc.match(/function resetSaveButton\(\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "resetSaveButton not found");
|
|
assert.match(fn[1], /saveBtn\.disabled = false;/);
|
|
assert.match(fn[1], /saveBtn\.innerHTML = '✓'/);
|
|
assert.match(fn[1], /saveBtn\.style\.background = '';/);
|
|
});
|
|
|
|
test("utils.js openEditor resets the save button before each session", () => {
|
|
const fn = utilsSrc.match(/async function openEditor\(vaultName, filePath\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "openEditor not found");
|
|
assert.match(fn[1], /resetSaveButton\(\);/);
|
|
});
|
|
|
|
test("utils.js closeEditor resets the save button on success/cancel/delete", () => {
|
|
const fn = utilsSrc.match(/function closeEditor\(\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(fn, "closeEditor not found");
|
|
assert.match(fn[1], /resetSaveButton\(\);/);
|
|
});
|
|
|
|
test("utils.js saveFile restores the save button when the request fails", () => {
|
|
assert.match(utilsSrc, /console\.error\("Save error:", err\);\s*\n\s*resetSaveButton\(\);/);
|
|
});
|
|
|
|
// ── #101: Forge uses the shared AI assistant + native fullscreen ──
|
|
test("editor-poc.html opens the shared AI assistant from its AI button", () => {
|
|
assert.ok(!/id="ai-panel"/.test(forgeSrc), "Forge mini AI panel markup must be gone");
|
|
assert.match(forgeSrc, /postMessage\(\{ type: 'forge-open-ai' \}, '\*'\)/);
|
|
assert.match(forgeSrc, /btnAI\.addEventListener\('click', openAssistant\)/);
|
|
});
|
|
|
|
test("sync.js routes forge-open-ai to the BooksLM assistant", () => {
|
|
const handler = syncSrc.match(/if \(e\.data\.type === 'forge-open-ai'\) \{([\s\S]*?)\n \}/);
|
|
assert.ok(handler, "forge-open-ai handler not found");
|
|
assert.match(handler[1], /import\('\.\/bookslm\.js'\)/);
|
|
assert.match(handler[1], /openForCurrentContext\(\)/);
|
|
});
|
|
|
|
test("editor-poc.html AI calls use the assistant's configured provider/model", () => {
|
|
const pick = forgeSrc.match(/function aiPickerSelection\(\) \{([\s\S]*?)\n \}/);
|
|
assert.ok(pick, "aiPickerSelection not found");
|
|
assert.match(pick[1], /obsigate_ai_picker/);
|
|
assert.match(forgeSrc, /var pick = aiPickerSelection\(\);/);
|
|
assert.match(forgeSrc, /Object\.keys\(pick\)\.forEach/);
|
|
});
|
|
|
|
test("editor-poc.html fixes AI endpoint names and translate param", () => {
|
|
assert.match(forgeSrc, /'ai-longer':\s*\{ ep: 'make-longer'/);
|
|
assert.match(forgeSrc, /'ai-shorter':\s*\{ ep: 'make-shorter'/);
|
|
assert.match(forgeSrc, /'ai-translate':\s*\{ ep: 'translate', extra: \{ target_lang: 'en' \}/);
|
|
});
|
|
|
|
test("editor-poc.html ghost completion follows the configured provider", () => {
|
|
assert.match(forgeSrc, /ghostBody\.provider = ghostPick\.provider \|\| 'ollama';/);
|
|
assert.match(forgeSrc, /ghostBody\.model = ghostPick\.model;/);
|
|
});
|
|
|
|
test("editor-poc.html has a native fullscreen button", () => {
|
|
assert.match(forgeSrc, /id="btn-fullscreen"/);
|
|
assert.match(forgeSrc, /document\.documentElement\.requestFullscreen/);
|
|
assert.match(forgeSrc, /document\.addEventListener\('fullscreenchange'/);
|
|
});
|
|
|
|
test("viewer.js allows fullscreen in the Forge iframe", () => {
|
|
assert.match(viewerSrc, /iframe\.setAttribute\("allow", "fullscreen"\)/);
|
|
});
|
|
|
|
test("index.html has an editor fullscreen button with i18n title", () => {
|
|
assert.match(indexSrc, /id="editor-fullscreen"/);
|
|
assert.match(indexSrc, /data-i18n-attr="title:editor\.fullscreen"/);
|
|
});
|
|
|
|
test("utils.js wires the editor fullscreen button and exits on close", () => {
|
|
assert.match(utilsSrc, /function toggleEditorFullscreen\(\)/);
|
|
assert.match(utilsSrc, /function updateFullscreenButton\(\)/);
|
|
assert.match(utilsSrc, /document\.addEventListener\("fullscreenchange", updateFullscreenButton\)/);
|
|
const close = utilsSrc.match(/function closeEditor\(\) \{([\s\S]*?)\n\}/);
|
|
assert.ok(close, "closeEditor not found");
|
|
assert.match(close[1], /document\.exitFullscreen\(\)/);
|
|
});
|
|
|
|
test("style.css makes the editor fill the screen in fullscreen", () => {
|
|
assert.match(cssSrc, /\.editor-container:fullscreen \{/);
|
|
assert.match(cssSrc, /\.editor-container:fullscreen \{[\s\S]*?height: 100vh;/);
|
|
});
|
|
|
|
test("locales expose the fullscreen labels (FR + EN)", () => {
|
|
const fr = JSON.parse(read("frontend", "locales", "fr.json"));
|
|
const en = JSON.parse(read("frontend", "locales", "en.json"));
|
|
for (const key of ["editor.fullscreen", "editor.exit_fullscreen"]) {
|
|
assert.ok(fr[key], `fr.json missing ${key}`);
|
|
assert.ok(en[key], `en.json missing ${key}`);
|
|
}
|
|
});
|
|
|
|
console.log(`\n${testCount - failCount}/${testCount} tests passed`);
|
|
if (failCount > 0) {
|
|
console.error(`${failCount} test(s) failed`);
|
|
process.exit(1);
|
|
}
|