Skip to content
Merged
111 changes: 111 additions & 0 deletions __tests__/generate-office-fonts-path.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { afterEach, describe, expect, test } from 'bun:test';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { spawnSync } from 'child_process';

const SCRIPT_PATH = path.resolve(import.meta.dir, '..', 'scripts', 'generate_office_fonts.js');
const CONVERTER_DIR = path.resolve(import.meta.dir, '..', 'converter');
const DUMMY_BIN = path.join(CONVERTER_DIR, process.platform === 'win32' ? 'allfontsgen.exe' : 'allfontsgen');

const tempDirs = [];
let createdDummyBin = false;

function makeTempDir(prefix) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}

function writeMockPreload(dir) {
const preloadPath = path.join(dir, 'mock-spawn.js');
fs.writeFileSync(
preloadPath,
`const childProcess = require('child_process');
const fs = require('fs');
const path = require('path');

childProcess.spawnSync = (_bin, args) => {
const allFontsArg = args.find((arg) => arg.startsWith('--allfonts='));
const selectionArg = args.find((arg) => arg.startsWith('--selection='));
const allFontsPath = allFontsArg ? allFontsArg.slice('--allfonts='.length) : null;
const selectionPath = selectionArg ? selectionArg.slice('--selection='.length) : null;

if (process.env.MOCK_WRITE_OUTPUTS === '1' && allFontsPath && selectionPath) {
fs.mkdirSync(path.dirname(allFontsPath), { recursive: true });
fs.writeFileSync(allFontsPath, 'window.AllFonts = [];');
fs.writeFileSync(selectionPath, Buffer.alloc(0));
}

return { status: Number(process.env.MOCK_EXIT_CODE || 0) };
};
`
);

return preloadPath;
}

function ensureDummyBin() {
if (!fs.existsSync(DUMMY_BIN)) {
fs.mkdirSync(CONVERTER_DIR, { recursive: true });
fs.writeFileSync(DUMMY_BIN, '');
fs.chmodSync(DUMMY_BIN, 0o755);
createdDummyBin = true;
}
}

function runGenerator({ fontDataDir, writeOutputs }) {
ensureDummyBin();
const mockDir = makeTempDir('oo-editors-font-mock-');
const preloadPath = writeMockPreload(mockDir);

const env = {
...process.env,
FONT_DATA_DIR: fontDataDir,
MOCK_EXIT_CODE: '0',
MOCK_WRITE_OUTPUTS: writeOutputs ? '1' : '0',
NODE_OPTIONS: [process.env.NODE_OPTIONS, `--require=${preloadPath}`]
.filter(Boolean)
.join(' '),
};

return spawnSync('node', [SCRIPT_PATH], {
env,
encoding: 'utf8',
});
}

afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
fs.rmSync(dir, { recursive: true, force: true });
}
if (createdDummyBin) {
fs.rmSync(DUMMY_BIN, { force: true });
createdDummyBin = false;
}
});

describe('generate_office_fonts path handling', () => {
test('should succeed when FONT_DATA_DIR contains non-ASCII characters', () => {
const root = makeTempDir('oo-editors-fontdata-');
const fontDataDir = path.join(root, 'C', 'Users', 'دانيال', 'AppData', 'Roaming', 'interpreter', 'office-extension-fontdata');

const result = runGenerator({ fontDataDir, writeOutputs: true });

expect(result.status).toBe(0);
expect(fs.existsSync(path.join(fontDataDir, 'AllFonts.js'))).toBe(true);
expect(fs.existsSync(path.join(fontDataDir, 'font_selection.bin'))).toBe(true);
});

test('should fail when generator exits 0 but does not write metadata files', () => {
const root = makeTempDir('oo-editors-fontdata-');
const fontDataDir = path.join(root, 'C', 'Users', 'دانيال', 'AppData', 'Roaming', 'interpreter', 'office-extension-fontdata');

const result = runGenerator({ fontDataDir, writeOutputs: false });

expect(result.status).toBe(1);
expect(result.stderr).toContain('font metadata files were not created');
expect(fs.existsSync(path.join(fontDataDir, 'AllFonts.js'))).toBe(false);
});
});
122 changes: 122 additions & 0 deletions __tests__/logo-header.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { chromium } from 'playwright';
import fs from 'fs';
import path from 'path';
import os from 'os';

const SERVER_PORT = Number.parseInt(process.env.SERVER_PORT || '8080', 10);
const SERVER_URL = process.env.SERVER_URL || `http://localhost:${SERVER_PORT}`;
const LOAD_TIMEOUT = Number.parseInt(process.env.LOAD_TIMEOUT || '30000', 10);
const LOGO_TIMEOUT = Number.parseInt(process.env.LOGO_TIMEOUT || '30000', 10);
const DEFAULT_LIST_PATH = path.join(import.meta.dirname, '..', 'test', 'logo-files.txt');
const LIST_PATH = process.env.FILE_LIST || process.argv[2] || DEFAULT_LIST_PATH;

function normalizeListEntry(raw) {
if (!raw) return null;
let value = raw.trim();
if (!value || value.startsWith('#')) return null;
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1).trim();
}
if (!value) return null;
if (value === '~') value = os.homedir();
else if (value.startsWith('~/')) value = path.join(os.homedir(), value.slice(2));
if (!path.isAbsolute(value)) value = path.resolve(process.cwd(), value);
return value;
}

function loadFileList(listPath) {
if (!fs.existsSync(listPath)) {
throw new Error(`File list not found: ${listPath}`);
}
const lines = fs.readFileSync(listPath, 'utf8').split(/\r?\n/);
const files = lines.map(normalizeListEntry).filter(Boolean);
if (files.length === 0) {
throw new Error(`File list is empty: ${listPath}`);
}
return files;
}

function assertFilesExist(files) {
const missing = files.filter((filePath) => !fs.existsSync(filePath) || !fs.statSync(filePath).isFile());
if (missing.length) {
throw new Error(`Missing files:\n${missing.join('\n')}`);
}
}

function getDocType(ext) {
if (['xlsx', 'xls', 'ods', 'csv'].includes(ext)) return 'cell';
if (['docx', 'doc', 'odt', 'txt', 'rtf', 'html'].includes(ext)) return 'word';
if (['pptx', 'ppt', 'odp'].includes(ext)) return 'slide';
return 'slide';
}

function buildOfflineLoaderUrl(filePath) {
const ext = path.extname(filePath).slice(1).toLowerCase();
const doctype = getDocType(ext);
const convertUrl = `${SERVER_URL}/api/convert?filepath=${encodeURIComponent(filePath)}`;
const params = new URLSearchParams({
url: convertUrl,
title: path.basename(filePath),
filepath: filePath,
filetype: ext,
doctype,
});
return `${SERVER_URL}/offline-loader-proper.html?${params.toString()}`;
}

async function assertServer() {
const response = await fetch(`${SERVER_URL}/healthcheck`).catch(() => null);
if (!response || !response.ok) {
throw new Error(`Server not running at ${SERVER_URL}. Start with: bun run server`);
}
}

async function assertLogoVisible(page, filePath) {
const url = buildOfflineLoaderUrl(filePath);
await page.goto(url, { waitUntil: 'load', timeout: LOAD_TIMEOUT });
await page.waitForFunction(() => {
const iframe = document.querySelector('iframe[id*="placeholder"]') ||
document.querySelector('iframe[id*="frameEditor"]') ||
document.querySelector('iframe');
if (!iframe || !iframe.contentDocument) return false;

const doc = iframe.contentDocument;
const logo = doc.querySelector('#oo-desktop-logo') ||
doc.querySelector('#box-document-title .extra img[src*="header-logo_s.svg"]') ||
doc.querySelector('#box-document-title img[src*="header-logo_s.svg"]');
if (!logo) return false;

const style = doc.defaultView.getComputedStyle(logo);
const rect = logo.getBoundingClientRect();
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
}, { timeout: LOGO_TIMEOUT });
}

async function run() {
await assertServer();
const files = loadFileList(LIST_PATH);
assertFilesExist(files);

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });

try {
for (const filePath of files) {
await assertLogoVisible(page, filePath);
console.log(`PASS logo visible: ${filePath}`);
}
console.log(`Logo header check passed for ${files.length} files`);
await browser.close();
process.exit(0);
} catch (err) {
await browser.close();
console.error('Logo header check failed');
console.error(err && err.message ? err.message : err);
process.exit(1);
}
}

run().catch((err) => {
console.error(err && err.message ? err.message : err);
process.exit(1);
});
7 changes: 6 additions & 1 deletion editors/desktop-stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -541,14 +541,19 @@
// Editor configuration - CRITICAL for fixing customization errors
GetEditorConfig: function() {
console.log('[BROWSER] GetEditorConfig called');
var headerLogoUrl = window.location.origin + '/web-apps/apps/common/main/resources/img/header/header-logo_s.svg';
return JSON.stringify({
customization: {
autosave: true,
chat: false,
comments: false,
help: false,
hideRightMenu: false,
compactHeader: true
compactHeader: true,
logo: {
visible: true,
image: headerLogoUrl
}
},
mode: 'edit',
canCoAuthoring: false,
Expand Down
89 changes: 88 additions & 1 deletion editors/offline-loader-proper.html
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,83 @@
// Run dialog dismissal every 2 seconds (much less aggressive)
console.log('[DISMISS] Starting dialog dismissal checker');
setInterval(dismissDialogsOnce, 2000);

function ensureDesktopLogoVisible() {
try {
var iframe = document.querySelector('iframe[id*="placeholder"]') ||
document.querySelector('iframe[id*="frameEditor"]') ||
document.querySelector('iframe');
if (!iframe || !iframe.contentDocument) return;

var iframeDoc = iframe.contentDocument;
var logoSrc = window.location.origin + '/web-apps/apps/common/main/resources/img/header/header-logo_s.svg';
var logo = iframeDoc.getElementById('oo-desktop-logo');
if (!logo) {
logo = iframeDoc.createElement('img');
logo.id = 'oo-desktop-logo';
logo.alt = 'ONLYOFFICE';
logo.src = logoSrc;
}

var host = iframeDoc.querySelector('#box-document-title .extra') ||
iframeDoc.querySelector('.extra');

if (host) {
if (logo.parentNode !== host) {
host.insertBefore(logo, host.firstChild);
}
logo.style.cssText = 'height:20px;max-width:120px;margin:2px 12px 0 6px;display:block;pointer-events:none;';
} else {
if (logo.parentNode !== iframeDoc.body) {
iframeDoc.body.appendChild(logo);
}
logo.style.cssText = 'position:fixed;top:12px;left:12px;z-index:2147483647;height:20px;max-width:120px;pointer-events:none;';
}
} catch (e) {
// NOTE(victor): best-effort -- cross-origin iframe access may throw
}
}

function scheduleLogoEnsure(frames) {
var remaining = typeof frames === 'number' ? frames : 4;
function tick() {
ensureDesktopLogoVisible();
remaining -= 1;
if (remaining > 0 && window.requestAnimationFrame) {
window.requestAnimationFrame(tick);
}
}

if (window.requestAnimationFrame) {
window.requestAnimationFrame(tick);
} else {
ensureDesktopLogoVisible();
}
}

function bindLogoRefreshHooks() {
var iframe = document.querySelector('iframe[id*="placeholder"]') ||
document.querySelector('iframe[id*="frameEditor"]') ||
document.querySelector('iframe');
if (!iframe || iframe.dataset.logoHooksBound === 'true') return;

iframe.dataset.logoHooksBound = 'true';
iframe.addEventListener('load', function() {
scheduleLogoEnsure(6);
});

try {
if (iframe.contentDocument && iframe.contentDocument.body && window.MutationObserver) {
var observer = new MutationObserver(function() {
scheduleLogoEnsure(2);
});
observer.observe(iframe.contentDocument.body, { childList: true, subtree: true });
}
} catch (e) {
// NOTE(victor): best-effort -- cross-origin iframe access may throw
}
}
setInterval(ensureDesktopLogoVisible, 3000);
// ========================================================================

// Parse URL parameters
Expand Down Expand Up @@ -337,6 +414,7 @@
}

function getEditorConfig(urlParams) {
var headerLogoUrl = window.location.origin + '/web-apps/apps/common/main/resources/img/header/header-logo_s.svg';
return {
customization : {
goback: { url: "onlyoffice.com" },
Expand All @@ -347,7 +425,11 @@
showReviewChanges: false,
toolbarNoTabs: false,
uiTheme: 'theme-classic-light', // Prevent theme switching dialogs
autosave: false // Disable autosave completely
autosave: false, // Disable autosave completely
logo: {
visible: true,
image: headerLogoUrl
}
},
mode : urlParams["mode"] || 'edit',
lang : urlParams["lang"] || 'en',
Expand Down Expand Up @@ -453,6 +535,10 @@
}
} catch (e) { console.error('[FIX] Error setting iframe ID:', e); } }, 100);

// NOTE(victor): SDK hides the header in desktop mode, so we force-inject the logo
bindLogoRefreshHooks();
scheduleLogoEnsure(8);

// NOTE(victor): Poll every 50ms instead of fixed 5s delay - SDK is usually ready immediately
var SDK_POLL_INTERVAL = 50;
var SDK_MAX_WAIT = 10000;
Expand Down Expand Up @@ -628,6 +714,7 @@
console.log('=== DOCUMENT READY ===');
console.log('[TIMING] Document ready! Total time:', (PERF.documentReady - PERF.loaderStart).toFixed(0), 'ms');
logTimings();
scheduleLogoEnsure(8);

// CRITICAL FIX: Initialize change tracking to stop infinite polling
// The SDK polls LocalFileGetSaved() and LocalFileGetOpenChangesCount()
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"server": "cross-env FONT_DATA_DIR=assets/onlyoffice-fontdata node server.js",
"test": "bun run test:unit && bun run test:e2e",
"test:all": "bun run test:unit && bun run test:e2e",
"test:unit": "bun test __tests__/server-utils.test.js __tests__/desktop-stub-utils.test.js && node test-url-scheme.js",
"test:e2e": "bun run test:console-batch && bun run test:save",
"test:unit": "bun test __tests__/server-utils.test.js __tests__/desktop-stub-utils.test.js __tests__/generate-office-fonts-path.test.js && node test-url-scheme.js",
"test:e2e": "bun run test:console-batch && bun run test:logo && bun run test:save",
"test:console-batch": "node test-console-batch.js",
"test:logo": "node __tests__/logo-header.test.js",
"test:save": "bun test __tests__/save.test.js",
"build": "cd sdkjs/build && npm install && npx grunt --desktop=true && cd ../.. && for e in cell word slide visio; do mkdir -p editors/sdkjs/$e && cp sdkjs/deploy/sdkjs/$e/sdk-all.js editors/sdkjs/$e/ && cp sdkjs/deploy/sdkjs/$e/sdk-all-min.js editors/sdkjs/$e/; done",
"build:minify": "bunx esbuild server.js --minify --outfile=dist/server.js"
Expand Down
Loading