刘光辉
9 小时以前 cf11e63b659afcc9792ab3e395f1b45d844431a8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import { afterEach, describe, expect, it, vi } from 'vitest';
 
import { createOnlyOfficeBridge } from '../../src/onlyoffice/bridge';
 
describe('filler bridge', () => {
  afterEach(() => vi.unstubAllGlobals());
 
  it('binds init and executes inspect command', async () => {
    vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => [] }) });
    const asc: any = { scope: {}, plugin: { info: { options: { ticket: 'x' } }, callCommand: vi.fn((command, _a, _b, callback) => callback(command())), executeCommand: vi.fn(), executeMethod: vi.fn(), onThemeChangedBase: vi.fn() } };
    const bridge = createOnlyOfficeBridge(asc);
    const ready = vi.fn();
    bridge.bindLifecycle({ onControlChanged: vi.fn(), onControlFocused: vi.fn(), onReady: ready, onThemeChanged: vi.fn() });
    asc.plugin.init();
    expect(ready).toHaveBeenCalled();
    await expect(bridge.inspectFields()).resolves.toEqual([]);
  });
 
  it('reports an invalid editor command result when the SDK callback has no value', async () => {
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((_command, _a, _b, callback) => callback(undefined)),
        executeCommand: vi.fn(),
        executeMethod: vi.fn(),
        onThemeChangedBase: vi.fn(),
      },
    };
 
    await expect(createOnlyOfficeBridge(asc).inspectFields()).rejects.toThrow('编辑器命令返回无效结果');
    expect(asc.scope.elnRecordFillInspect).toBeUndefined();
  });
 
  it('does not serialize document commands outside record fill-only mode', async () => {
    vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => [] }) });
    const callbacks: Array<() => void> = [];
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((command, _a, _b, callback) => callbacks.push(() => callback(command()))),
        executeCommand: vi.fn(),
        executeMethod: vi.fn(),
        onThemeChangedBase: vi.fn(),
      },
    };
    const bridge = createOnlyOfficeBridge(asc);
 
    const first = bridge.inspectFields();
    const second = bridge.inspectFields();
    expect(asc.plugin.callCommand).toHaveBeenCalledTimes(2);
    callbacks.forEach((callback) => callback());
 
    await expect(Promise.all([first, second])).resolves.toEqual([[], []]);
  });
 
  it('temporarily lifts record fill restrictions while running controlled document commands', async () => {
    vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => [] }) });
    let restriction = 'none';
    const transitions: string[] = [];
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((command, _a, _b, callback) => {
          expect(restriction).toBe('none');
          callback(command());
        }),
        executeCommand: vi.fn(),
        executeMethod: vi.fn((name, args, callback) => {
          if (name === 'SetEditingRestrictions') {
            restriction = args[0];
            transitions.push(restriction);
          }
          callback(undefined);
        }),
        onThemeChangedBase: vi.fn(),
      },
    };
    const bridge = createOnlyOfficeBridge(asc);
 
    await bridge.setFillOnlyMode(true);
    await expect(bridge.inspectFields()).resolves.toEqual([]);
 
    expect(transitions).toEqual(['forms', 'none', 'forms']);
  });
 
  it('falls back to read-only when restoring fill-only restrictions fails', async () => {
    vi.stubGlobal('Api', { GetDocument: () => ({ GetAllContentControls: () => [] }) });
    let restriction = 'none';
    let formsCalls = 0;
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((command, _a, _b, callback) => callback(command())),
        executeCommand: vi.fn(),
        executeMethod: vi.fn((name, args, callback) => {
          if (name === 'SetEditingRestrictions') {
            if (args[0] === 'forms' && ++formsCalls === 2) throw new Error('restriction restore failed');
            restriction = args[0];
          }
          callback(undefined);
        }),
        onThemeChangedBase: vi.fn(),
      },
    };
    const bridge = createOnlyOfficeBridge(asc);
 
    await bridge.setFillOnlyMode(true);
    await expect(bridge.inspectFields()).rejects.toThrow('restriction restore failed');
 
    expect(restriction).toBe('readOnly');
  });
 
  it('passes a repeat row identity through command scope and cleans it after callback', async () => {
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn((_command, _a, _b, callback) => {
          expect(asc.scope.elnRecordFillRepeatRow).toEqual({ groupCode: 'results', rowId: 'row-1' });
          callback(JSON.stringify({ ok: true, tags: ['eln.repeat.results.row-1.value'] }));
        }),
        executeCommand: vi.fn(),
        executeMethod: vi.fn(),
        onThemeChangedBase: vi.fn(),
      },
    };
 
    await expect(createOnlyOfficeBridge(asc).appendRepeatRow('results', 'row-1')).resolves.toEqual([
      'eln.repeat.results.row-1.value',
    ]);
    expect(asc.scope.elnRecordFillRepeatRow).toBeUndefined();
 
    await expect(createOnlyOfficeBridge(asc).removeRepeatRow('results', 'row-1')).resolves.toEqual([
      'eln.repeat.results.row-1.value',
    ]);
    expect(asc.scope.elnRecordFillRepeatRow).toBeUndefined();
  });
 
  it('binds and detaches controlled-document change and focus events', () => {
    const handlers = new Map<string, (value: unknown) => void>();
    const attachEditorEvent = vi.fn((name, handler) => handlers.set(name, handler));
    const asc: any = {
      scope: {},
      plugin: {
        attachEditorEvent,
        callCommand: vi.fn(),
        detachEditorEvent: vi.fn((name) => handlers.delete(name)),
        executeCommand: vi.fn(),
        executeMethod: vi.fn(),
        onThemeChangedBase: vi.fn(),
      },
    };
    const onControlChanged = vi.fn();
    const onControlFocused = vi.fn();
    const cleanup = createOnlyOfficeBridge(asc).bindLifecycle({
      onControlChanged,
      onControlFocused,
      onReady: vi.fn(),
      onThemeChanged: vi.fn(),
    });
 
    expect(attachEditorEvent).not.toHaveBeenCalled();
    asc.plugin.init();
    handlers.get('onChangeContentControl')?.({ InternalId: '1_2', Tag: 'eln.field.result' });
    handlers.get('onFocusContentControl')?.({ InternalId: '1_2', Tag: 'eln.field.result' });
    expect(onControlChanged).toHaveBeenCalledWith({ InternalId: '1_2', Tag: 'eln.field.result' });
    expect(onControlFocused).toHaveBeenCalledWith({ InternalId: '1_2', Tag: 'eln.field.result' });
 
    cleanup();
    expect(asc.plugin.detachEditorEvent.mock.calls).toEqual([
      ['onChangeContentControl'],
      ['onFocusContentControl'],
    ]);
  });
 
  it('selects a controlled field by its internal id and rejects an editor failure', async () => {
    const executeMethod = vi.fn((_name, _args, callback) => callback(false));
    const asc: any = {
      scope: {},
      plugin: {
        callCommand: vi.fn(), executeCommand: vi.fn(), executeMethod, onThemeChangedBase: vi.fn(),
      },
    };
    const bridge = createOnlyOfficeBridge(asc);
 
    await expect(bridge.selectControl('')).rejects.toThrow('字段控件缺少内部 ID');
    await expect(bridge.selectControl('1_2')).rejects.toThrow('编辑器未能定位字段');
    expect(executeMethod).toHaveBeenCalledWith('SelectContentControl', ['1_2'], expect.any(Function));
  });
});