|
4 | 4 | * Tests for the FieldUpdate extension's updateFieldsInSelection command. |
5 | 5 | * |
6 | 6 | * Uses the numwords.docx fixture which contains NUMWORDS, NUMCHARS, and |
7 | | - * NUMPAGES fields with known imported values. |
| 7 | + * NUMPAGES fields with known imported values for the stat-field path. The |
| 8 | + * TOC path is exercised via direct command-function invocation against a |
| 9 | + * synthetic doc/editor — no docx fixture required. |
8 | 10 | */ |
9 | 11 |
|
10 | | -import { afterEach, beforeAll, describe, expect, it } from 'vitest'; |
| 12 | +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; |
| 13 | +import { Schema } from 'prosemirror-model'; |
11 | 14 | import { initTestEditor, loadTestDataForEditorTests } from '@tests/helpers/helpers.js'; |
12 | 15 | import { getWordStatistics } from '../../document-api-adapters/helpers/word-statistics.js'; |
| 16 | +import { FieldUpdate } from './field-update.js'; |
13 | 17 |
|
14 | 18 | describe('FieldUpdate extension', () => { |
15 | 19 | let docData; |
@@ -107,3 +111,135 @@ describe('FieldUpdate extension', () => { |
107 | 111 | expect(numcharsField.attrs.resolvedText).toBe(expectedValue); |
108 | 112 | }); |
109 | 113 | }); |
| 114 | + |
| 115 | +// --------------------------------------------------------------------------- |
| 116 | +// TOC path — invoked directly against synthetic state to avoid needing a |
| 117 | +// fully-imported TOC fixture. |
| 118 | +// --------------------------------------------------------------------------- |
| 119 | + |
| 120 | +const tocSchema = new Schema({ |
| 121 | + nodes: { |
| 122 | + doc: { content: 'block+' }, |
| 123 | + paragraph: { group: 'block', content: 'inline*', toDOM: () => ['p', 0] }, |
| 124 | + tableOfContents: { |
| 125 | + group: 'block', |
| 126 | + content: 'paragraph*', |
| 127 | + attrs: { sdBlockId: { default: null } }, |
| 128 | + toDOM: () => ['div', 0], |
| 129 | + }, |
| 130 | + text: { group: 'inline' }, |
| 131 | + }, |
| 132 | +}); |
| 133 | + |
| 134 | +const buildTocDoc = (sdBlockIds) => { |
| 135 | + const para = (txt) => tocSchema.nodes.paragraph.create({}, txt ? tocSchema.text(txt) : null); |
| 136 | + const tocs = sdBlockIds.map((id) => tocSchema.nodes.tableOfContents.create({ sdBlockId: id }, [para('entry')])); |
| 137 | + return tocSchema.nodes.doc.create({}, [para('intro'), ...tocs, para('outro')]); |
| 138 | +}; |
| 139 | + |
| 140 | +const runUpdateFields = (overrides) => { |
| 141 | + const { doc, editor } = overrides; |
| 142 | + const dispatch = 'dispatch' in overrides ? overrides.dispatch : () => {}; |
| 143 | + // FieldUpdate is wrapped by Extension.create(); reach into config.addCommands |
| 144 | + // to invoke the raw command function the same way ExtensionService does. |
| 145 | + const commands = FieldUpdate.config.addCommands.call({ editor }); |
| 146 | + const command = commands.updateFieldsInSelection(); |
| 147 | + const tr = { setMeta: vi.fn() }; |
| 148 | + const state = { doc, selection: { from: 0, to: 0 }, schema: tocSchema, tr }; |
| 149 | + return { result: command({ editor, state, tr, dispatch }), tr }; |
| 150 | +}; |
| 151 | + |
| 152 | +describe('updateFieldsInSelection — TOC path', () => { |
| 153 | + it('calls editor.doc.toc.update for every tableOfContents node in document order', () => { |
| 154 | + const update = vi.fn(() => ({ success: true })); |
| 155 | + const editor = { doc: { toc: { update } } }; |
| 156 | + const doc = buildTocDoc(['toc-a', 'toc-b']); |
| 157 | + |
| 158 | + const { result } = runUpdateFields({ doc, editor }); |
| 159 | + |
| 160 | + expect(result).toBe(true); |
| 161 | + expect(update).toHaveBeenCalledTimes(2); |
| 162 | + expect(update.mock.calls[0][0]).toEqual({ |
| 163 | + target: { kind: 'block', nodeType: 'tableOfContents', nodeId: 'toc-a' }, |
| 164 | + mode: 'all', |
| 165 | + }); |
| 166 | + expect(update.mock.calls[1][0]).toEqual({ |
| 167 | + target: { kind: 'block', nodeType: 'tableOfContents', nodeId: 'toc-b' }, |
| 168 | + mode: 'all', |
| 169 | + }); |
| 170 | + }); |
| 171 | + |
| 172 | + it('sets preventDispatch on the framework tr so CommandService skips its auto-dispatch', () => { |
| 173 | + const update = vi.fn(() => ({ success: true })); |
| 174 | + const editor = { doc: { toc: { update } } }; |
| 175 | + const doc = buildTocDoc(['toc-a']); |
| 176 | + |
| 177 | + const { tr } = runUpdateFields({ doc, editor }); |
| 178 | + expect(tr.setMeta).toHaveBeenCalledWith('preventDispatch', true); |
| 179 | + }); |
| 180 | + |
| 181 | + it('returns true on a can()-style probe (no dispatch) when any TOC exists', () => { |
| 182 | + const update = vi.fn(); |
| 183 | + const editor = { doc: { toc: { update } } }; |
| 184 | + const doc = buildTocDoc(['toc-a']); |
| 185 | + |
| 186 | + const { result } = runUpdateFields({ doc, editor, dispatch: undefined }); |
| 187 | + expect(result).toBe(true); |
| 188 | + expect(update).not.toHaveBeenCalled(); |
| 189 | + }); |
| 190 | + |
| 191 | + it('skips a TOC whose sdBlockId is missing or empty', () => { |
| 192 | + const update = vi.fn(() => ({ success: true })); |
| 193 | + const editor = { doc: { toc: { update } } }; |
| 194 | + const doc = buildTocDoc([null, '', 'toc-real']); |
| 195 | + |
| 196 | + runUpdateFields({ doc, editor }); |
| 197 | + expect(update).toHaveBeenCalledTimes(1); |
| 198 | + expect(update.mock.calls[0][0].target.nodeId).toBe('toc-real'); |
| 199 | + }); |
| 200 | + |
| 201 | + it('swallows toc.update errors and continues with the remaining TOCs', () => { |
| 202 | + const update = vi |
| 203 | + .fn() |
| 204 | + .mockImplementationOnce(() => { |
| 205 | + throw new Error('boom'); |
| 206 | + }) |
| 207 | + .mockImplementationOnce(() => ({ success: true })); |
| 208 | + const editor = { doc: { toc: { update } } }; |
| 209 | + const doc = buildTocDoc(['toc-a', 'toc-b']); |
| 210 | + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); |
| 211 | + |
| 212 | + const { result } = runUpdateFields({ doc, editor }); |
| 213 | + expect(result).toBe(true); |
| 214 | + expect(update).toHaveBeenCalledTimes(2); |
| 215 | + expect(warnSpy).toHaveBeenCalled(); |
| 216 | + warnSpy.mockRestore(); |
| 217 | + }); |
| 218 | + |
| 219 | + it('falls through to the stat-field path when the doc has no TOCs', () => { |
| 220 | + const update = vi.fn(); |
| 221 | + const editor = { doc: { toc: { update } } }; |
| 222 | + const para = (txt) => tocSchema.nodes.paragraph.create({}, txt ? tocSchema.text(txt) : null); |
| 223 | + const doc = tocSchema.nodes.doc.create({}, [para('hello world')]); |
| 224 | + |
| 225 | + const { tr } = runUpdateFields({ doc, editor }); |
| 226 | + expect(update).not.toHaveBeenCalled(); |
| 227 | + expect(tr.setMeta).not.toHaveBeenCalled(); // no preventDispatch when not taking the TOC path |
| 228 | + }); |
| 229 | +}); |
| 230 | + |
| 231 | +describe('FieldUpdate extension shortcuts', () => { |
| 232 | + it('binds F9 to updateFieldsInSelection', () => { |
| 233 | + const ed = { commands: { updateFieldsInSelection: vi.fn(() => true) } }; |
| 234 | + const shortcuts = FieldUpdate.config.addShortcuts.call({ editor: ed }); |
| 235 | + expect(typeof shortcuts.F9).toBe('function'); |
| 236 | + shortcuts.F9(); |
| 237 | + expect(ed.commands.updateFieldsInSelection).toHaveBeenCalledTimes(1); |
| 238 | + }); |
| 239 | + |
| 240 | + it('does not bind any other key alongside F9', () => { |
| 241 | + const ed = { commands: { updateFieldsInSelection: vi.fn() } }; |
| 242 | + const shortcuts = FieldUpdate.config.addShortcuts.call({ editor: ed }); |
| 243 | + expect(Object.keys(shortcuts)).toEqual(['F9']); |
| 244 | + }); |
| 245 | +}); |
0 commit comments