import type { HmrContext, Plugin, ViteDevServer } from 'vite';
|
|
import { readFileSync } from 'node:fs';
|
import { resolve } from 'node:path';
|
import process from 'node:process';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { createDevelopmentManifest, onlyOfficeDevelopmentPlugin, resolveOnlyOfficeDevOptions } from '../../vite/onlyoffice-plugin';
|
|
const now = new Date('2026-08-11T01:02:03Z');
|
const manifestPath = resolve(process.cwd(), 'config.json');
|
|
describe('onlyoffice 开发配置', () => {
|
it('接受独立 HMR path', () => {
|
expect(resolveOnlyOfficeDevOptions({ ONLYOFFICE_PLUGIN_HMR_PATH: '/__hmr/eln-template-filler' }, now).hmrPath).toBe('/__hmr/eln-template-filler');
|
});
|
|
it.each(['__hmr/plugin', '/', '/bad path', '/bad?query'])('拒绝非法 HMR path %s', (hmrPath) => {
|
expect(() => resolveOnlyOfficeDevOptions({ ONLYOFFICE_PLUGIN_HMR_PATH: hmrPath }, now)).toThrow('ONLYOFFICE_PLUGIN_HMR_PATH');
|
});
|
|
it('生成保持 0.1.0 的默认开发配置', () => {
|
const options = resolveOnlyOfficeDevOptions({}, now);
|
|
expect(options).toEqual({
|
basePath: '/onlyoffice-plugins/0.1.0-dev/eln-template-filler',
|
docsUrl: 'http://localhost:20000',
|
hmrClientPort: 4173,
|
hmrHost: 'localhost',
|
hmrProtocol: 'ws',
|
host: '0.0.0.0',
|
manifestUrl: 'http://localhost:4173/onlyoffice-plugins/0.1.0-dev/eln-template-filler/config.json?v=20260811T010203',
|
port: 4173,
|
publicHost: 'localhost',
|
release: '0.1.0-dev',
|
startupId: '20260811T010203',
|
});
|
});
|
|
it('解析服务端环境变量并由 HMR 协议生成公开 URL', () => {
|
const options = resolveOnlyOfficeDevOptions(
|
{
|
ONLYOFFICE_DOCS_URL: 'https://docs.example.com/office///',
|
ONLYOFFICE_PLUGIN_DEV_HOST: '127.0.0.1',
|
ONLYOFFICE_PLUGIN_DEV_PORT: '5174',
|
ONLYOFFICE_PLUGIN_HMR_CLIENT_PORT: '443',
|
ONLYOFFICE_PLUGIN_HMR_HOST: 'hmr.example.com',
|
ONLYOFFICE_PLUGIN_HMR_PROTOCOL: 'wss',
|
ONLYOFFICE_PLUGIN_PUBLIC_HOST: 'plugins.example.com',
|
},
|
now,
|
);
|
|
expect(options).toMatchObject({
|
docsUrl: 'https://docs.example.com/office',
|
hmrClientPort: 443,
|
hmrHost: 'hmr.example.com',
|
hmrProtocol: 'wss',
|
host: '127.0.0.1',
|
manifestUrl: 'https://plugins.example.com:5174/onlyoffice-plugins/0.1.0-dev/eln-template-filler/config.json?v=20260811T010203',
|
port: 5174,
|
publicHost: 'plugins.example.com',
|
});
|
});
|
|
it('让 HMR host 和 client port 默认跟随公开 host 与开发端口', () => {
|
const options = resolveOnlyOfficeDevOptions(
|
{
|
ONLYOFFICE_PLUGIN_DEV_PORT: '5174',
|
ONLYOFFICE_PLUGIN_PUBLIC_HOST: 'plugins.example.com',
|
},
|
now,
|
);
|
|
expect(options.hmrHost).toBe('plugins.example.com');
|
expect(options.hmrClientPort).toBe(5174);
|
});
|
|
it('使用 UTC 生成固定长度的启动标识并支持 bracket IPv6 公开 host', () => {
|
const options = resolveOnlyOfficeDevOptions({ ONLYOFFICE_PLUGIN_PUBLIC_HOST: '[2001:db8::1]' }, new Date('2026-01-02T03:04:05+08:00'));
|
|
expect(options.startupId).toBe('20260101T190405');
|
expect(options.manifestUrl).toBe('http://[2001:db8::1]:4173/onlyoffice-plugins/0.1.0-dev/eln-template-filler/config.json?v=20260101T190405');
|
});
|
|
it('监听 host 使用裸 IPv6,URL host 使用 bracket IPv6', () => {
|
const options = resolveOnlyOfficeDevOptions(
|
{
|
ONLYOFFICE_PLUGIN_DEV_HOST: '::1',
|
ONLYOFFICE_PLUGIN_HMR_HOST: '[2001:db8::2]',
|
ONLYOFFICE_PLUGIN_PUBLIC_HOST: '[2001:db8::1]',
|
},
|
now,
|
);
|
|
expect(options.host).toBe('::1');
|
expect(options.hmrHost).toBe('[2001:db8::2]');
|
expect(options.publicHost).toBe('[2001:db8::1]');
|
});
|
|
it('拒绝带方括号的监听 IPv6 host', () => {
|
expect(() => resolveOnlyOfficeDevOptions({ ONLYOFFICE_PLUGIN_DEV_HOST: '[::1]' }, now)).toThrow('ONLYOFFICE_PLUGIN_DEV_HOST');
|
});
|
|
it.each([
|
['ONLYOFFICE_PLUGIN_DEV_PORT', '0'],
|
['ONLYOFFICE_PLUGIN_DEV_PORT', '65536'],
|
['ONLYOFFICE_PLUGIN_DEV_PORT', '1.5'],
|
['ONLYOFFICE_PLUGIN_DEV_PORT', ' 4173'],
|
['ONLYOFFICE_PLUGIN_HMR_CLIENT_PORT', ''],
|
['ONLYOFFICE_PLUGIN_HMR_CLIENT_PORT', '01'],
|
])('拒绝非法端口 %s=%j', (name, value) => {
|
expect(() => resolveOnlyOfficeDevOptions({ [name]: value }, now)).toThrow(name);
|
});
|
|
it.each(['file', 'http', 'WS', ''])('拒绝非法 HMR 协议 %j', (protocol) => {
|
expect(() => resolveOnlyOfficeDevOptions({ ONLYOFFICE_PLUGIN_HMR_PROTOCOL: protocol }, now)).toThrow('ONLYOFFICE_PLUGIN_HMR_PROTOCOL');
|
});
|
|
it.each([
|
['ONLYOFFICE_PLUGIN_PUBLIC_HOST', 'http://localhost'],
|
['ONLYOFFICE_PLUGIN_PUBLIC_HOST', 'localhost/path'],
|
['ONLYOFFICE_PLUGIN_PUBLIC_HOST', 'local host'],
|
['ONLYOFFICE_PLUGIN_PUBLIC_HOST', 'localhost:4173'],
|
['ONLYOFFICE_PLUGIN_PUBLIC_HOST', '2001:db8::1'],
|
['ONLYOFFICE_PLUGIN_HMR_HOST', 'hmr.example.com/path'],
|
['ONLYOFFICE_PLUGIN_DEV_HOST', '127.0.0.1\ninvalid'],
|
])('拒绝非法 host %s=%j', (name, value) => {
|
expect(() => resolveOnlyOfficeDevOptions({ [name]: value }, now)).toThrow(name);
|
});
|
|
it.each([
|
'file:///tmp/sdk',
|
'ftp://docs.example.com',
|
'https://user:secret@docs.example.com',
|
'https://docs.example.com/office?tenant=test',
|
'https://docs.example.com/office#sdk',
|
'not a URL',
|
])('拒绝非法 Docs URL %j', (docsUrl) => {
|
expect(() => resolveOnlyOfficeDevOptions({ ONLYOFFICE_DOCS_URL: docsUrl }, now)).toThrow('ONLYOFFICE_DOCS_URL');
|
});
|
|
it('拒绝无效启动时间', () => {
|
expect(() => resolveOnlyOfficeDevOptions({}, new Date(Number.NaN))).toThrow('now');
|
});
|
});
|
|
describe('onlyoffice 开发 manifest', () => {
|
const options = resolveOnlyOfficeDevOptions({}, now);
|
|
function readSourceManifest(): Record<string, unknown> {
|
return JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;
|
}
|
|
it('克隆正式 manifest 并只改写开发 iframe URL', () => {
|
const source = readSourceManifest();
|
const sourceSnapshot = structuredClone(source);
|
const manifest = createDevelopmentManifest(source, options);
|
|
expect(manifest).not.toBe(source);
|
expect(manifest).toMatchObject({
|
guid: source.guid,
|
minVersion: '9.4.0',
|
version: '0.1.0',
|
});
|
expect(manifest.variations).not.toBe(source.variations);
|
expect(manifest).toMatchObject({
|
variations: [
|
{
|
url: 'index.html?v=20260811T010203',
|
},
|
],
|
});
|
expect(source).toEqual(sourceSnapshot);
|
});
|
|
it('拒绝错误 GUID', () => {
|
const source = readSourceManifest();
|
source.guid = 'asc.{00000000-0000-0000-0000-000000000000}';
|
|
expect(() => createDevelopmentManifest(source, options)).toThrow('GUID');
|
});
|
|
it('拒绝错误正式版本', () => {
|
const source = readSourceManifest();
|
source.version = '0.1.1';
|
|
expect(() => createDevelopmentManifest(source, options)).toThrow('version');
|
});
|
|
it.each([undefined, [], [null], [{}], [{ url: 42 }]])('拒绝无有效首个 variation URL 的 manifest:%j', (variations) => {
|
const source = readSourceManifest();
|
source.variations = variations;
|
|
expect(() => createDevelopmentManifest(source, options)).toThrow('variation');
|
});
|
});
|
|
describe('onlyoffice 开发服务生命周期', () => {
|
const options = resolveOnlyOfficeDevOptions({}, now);
|
|
function getConfigureServerHook(plugin: Plugin): (server: ViteDevServer) => unknown {
|
const hook = plugin.configureServer;
|
if (!hook) throw new Error('缺少 configureServer hook');
|
return (typeof hook === 'function' ? hook : hook.handler) as (server: ViteDevServer) => unknown;
|
}
|
|
function getHandleHotUpdateHook(plugin: Plugin): (context: HmrContext) => unknown {
|
const hook = plugin.handleHotUpdate;
|
if (!hook) throw new Error('缺少 handleHotUpdate hook');
|
return (typeof hook === 'function' ? hook : hook.handler) as (context: HmrContext) => unknown;
|
}
|
|
it('http server 开始监听后只打印一次完整 manifest URL', () => {
|
const plugin = onlyOfficeDevelopmentPlugin(options);
|
let listeningHandler: (() => void) | undefined;
|
const httpServer = {
|
once(event: string, handler: () => void) {
|
if (event === 'listening') listeningHandler = handler;
|
},
|
};
|
const info = vi.fn();
|
const server = {
|
config: { logger: { info } },
|
httpServer,
|
middlewares: { use: vi.fn() },
|
} as unknown as ViteDevServer;
|
|
getConfigureServerHook(plugin)(server);
|
expect(info).not.toHaveBeenCalled();
|
|
listeningHandler?.();
|
listeningHandler?.();
|
|
expect(info).toHaveBeenCalledTimes(1);
|
expect(info).toHaveBeenCalledWith(options.manifestUrl);
|
});
|
|
it.each(['src/main.ts', 'src/onlyoffice/bridge.ts', 'src/onlyoffice/commands.ts', 'src/onlyoffice/types.ts', 'src/composables/useTemplateAnnotator.ts'])(
|
'%s 变化时完整刷新 iframe 并阻止旧模块继续热更新',
|
(relativePath) => {
|
const plugin = onlyOfficeDevelopmentPlugin(options);
|
const send = vi.fn();
|
const context = {
|
file: resolve(process.cwd(), relativePath),
|
server: { ws: { send } },
|
} as unknown as HmrContext;
|
|
const result = getHandleHotUpdateHook(plugin)(context);
|
|
expect(result).toEqual([]);
|
expect(send).toHaveBeenCalledOnce();
|
expect(send).toHaveBeenCalledWith({ path: '*', type: 'full-reload' });
|
},
|
);
|
|
it.each(['src/App.vue', 'src/components/FieldEditor.vue', 'src/styles.css'])('%s 交给 Vite 默认 HMR,不主动完整刷新', (relativePath) => {
|
const plugin = onlyOfficeDevelopmentPlugin(options);
|
const send = vi.fn();
|
const context = {
|
file: resolve(process.cwd(), relativePath),
|
server: { ws: { send } },
|
} as unknown as HmrContext;
|
|
const result = getHandleHotUpdateHook(plugin)(context);
|
|
expect(result).toBeUndefined();
|
expect(send).not.toHaveBeenCalled();
|
});
|
|
it('config.json 变化时提示重新打开会话、完整刷新且保持本次 startup ID', () => {
|
const plugin = onlyOfficeDevelopmentPlugin(options);
|
const send = vi.fn();
|
const warn = vi.fn();
|
const context = {
|
file: resolve(process.cwd(), 'config.json'),
|
server: { config: { logger: { warn } }, ws: { send } },
|
} as unknown as HmrContext;
|
|
const result = getHandleHotUpdateHook(plugin)(context);
|
|
expect(result).toEqual([]);
|
expect(send).toHaveBeenCalledWith({ path: '*', type: 'full-reload' });
|
expect(warn).toHaveBeenCalledWith('config.json 已变化,请重启 Dev Server 后重新打开编辑器会话。');
|
expect(options.startupId).toBe('20260811T010203');
|
expect(options.manifestUrl).toContain(`v=${options.startupId}`);
|
});
|
});
|
// @vitest-environment node
|