Skip to content

Commit dbdf306

Browse files
committed
feat: numbering validator adding unit tests
1 parent 778c941 commit dbdf306

3 files changed

Lines changed: 202 additions & 4 deletions

File tree

packages/super-editor/src/core/super-validator/super-validator.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,17 +194,14 @@ export class SuperValidator {
194194
const { tr } = this.#editor.state;
195195
const { dispatch } = this.#editor.view;
196196

197-
const documentAnalysis = this.#analyzeDocument();
198-
this.logger.debug('Document analysis for export:', documentAnalysis);
199-
200197
let hasModifiedDocument = false;
201198
const validationResults = [];
202199

203200
// Run XML validators
204201
Object.entries(this.#xmlValidators).forEach(([key, validator]) => {
205202
this.logger.debug(`🕵 Validating export with ${key}...`);
206203

207-
const { results, modified } = validator(tr, documentAnalysis);
204+
const { results, modified } = validator();
208205
validationResults.push({ key, results });
209206

210207
hasModifiedDocument = hasModifiedDocument || modified;

packages/super-editor/src/core/super-validator/super-validator.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { SuperValidator } from './super-validator.js';
33
import { StateValidators } from './validators/state/index.js';
4+
import { XmlValidators } from './validators/xml/index.js';
45

56
vi.mock('./logger/logger.js', () => ({
67
createLogger: vi.fn(() => ({
@@ -18,6 +19,13 @@ vi.mock('./validators/state/index.js', () => ({
1819
},
1920
}));
2021

22+
vi.mock('./validators/xml/index.js', () => ({
23+
XmlValidators: {
24+
xmlA: vi.fn(),
25+
xmlB: vi.fn(),
26+
},
27+
}));
28+
2129
describe('SuperValidator', () => {
2230
let mockEditor, mockDoc, mockView, mockTr;
2331

@@ -114,4 +122,54 @@ describe('SuperValidator', () => {
114122

115123
expect(result.results).toHaveLength(2);
116124
});
125+
126+
describe('validateDocumentExport', () => {
127+
it('calls all XML validators and aggregates results; dispatches when modified', () => {
128+
const xmlValidatorA = vi.fn(() => ({ modified: true, results: ['fixed numbering'] }));
129+
const xmlValidatorB = vi.fn(() => ({ modified: false, results: [] }));
130+
131+
XmlValidators.xmlA.mockReturnValue(xmlValidatorA);
132+
XmlValidators.xmlB.mockReturnValue(xmlValidatorB);
133+
134+
const instance = new SuperValidator({ editor: mockEditor });
135+
136+
const result = instance.validateDocumentExport();
137+
138+
expect(result.modified).toBe(true);
139+
expect(result.results).toEqual([
140+
{ key: 'xmlA', results: ['fixed numbering'] },
141+
{ key: 'xmlB', results: [] },
142+
]);
143+
144+
expect(mockView.dispatch).toHaveBeenCalled();
145+
});
146+
147+
it('does not dispatch if no XML validator modified the document', () => {
148+
const xmlValidatorA = vi.fn(() => ({ modified: false, results: [] }));
149+
const xmlValidatorB = vi.fn(() => ({ modified: false, results: [] }));
150+
151+
XmlValidators.xmlA.mockReturnValue(xmlValidatorA);
152+
XmlValidators.xmlB.mockReturnValue(xmlValidatorB);
153+
154+
const instance = new SuperValidator({ editor: mockEditor });
155+
const result = instance.validateDocumentExport();
156+
157+
expect(result.modified).toBe(false);
158+
expect(mockView.dispatch).not.toHaveBeenCalled();
159+
});
160+
161+
it('does not dispatch if dryRun is true even when modified', () => {
162+
const xmlValidatorA = vi.fn(() => ({ modified: true, results: ['something'] }));
163+
const xmlValidatorB = vi.fn(() => ({ modified: false, results: [] }));
164+
165+
XmlValidators.xmlA.mockReturnValue(xmlValidatorA);
166+
XmlValidators.xmlB.mockReturnValue(xmlValidatorB);
167+
168+
const instance = new SuperValidator({ editor: mockEditor, dryRun: true });
169+
const result = instance.validateDocumentExport();
170+
171+
expect(result.modified).toBe(true);
172+
expect(mockView.dispatch).not.toHaveBeenCalled();
173+
});
174+
});
117175
});
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { createNumberingValidator } from './numbering-validator.js';
3+
4+
function makeEditorWithNumbering(numberingXmlLike) {
5+
return {
6+
converter: {
7+
convertedXml: {
8+
'word/numbering.xml': numberingXmlLike,
9+
},
10+
},
11+
};
12+
}
13+
14+
function makeLogger() {
15+
return { debug: vi.fn(), withPrefix: vi.fn(() => ({ debug: vi.fn() })) };
16+
}
17+
18+
describe('numbering-validator', () => {
19+
it('returns invalid when numbering.xml is missing or malformed', () => {
20+
const cases = [undefined, null, {}, { elements: [] }, { elements: [{}] }, { elements: [{ elements: [] }] }];
21+
22+
for (const numbering of cases) {
23+
const editor = makeEditorWithNumbering(numbering);
24+
const logger = makeLogger();
25+
const validator = createNumberingValidator({ editor, logger });
26+
const result = validator();
27+
28+
expect(result.modified).toBe(false);
29+
expect(result.results).toEqual(['word/numbering.xml is not a valid xml']);
30+
expect(logger.debug).not.toHaveBeenCalled();
31+
}
32+
});
33+
34+
it('no changes when all <w:num> have valid numeric w:numId', () => {
35+
const numbering = {
36+
elements: [
37+
{
38+
elements: [
39+
{
40+
type: 'element',
41+
name: 'w:abstractNum',
42+
attributes: { 'w:abstractNumId': '1' },
43+
elements: [],
44+
},
45+
{
46+
type: 'element',
47+
name: 'w:num',
48+
attributes: { 'w:numId': '1' },
49+
elements: [],
50+
},
51+
{
52+
type: 'element',
53+
name: 'w:pPr',
54+
elements: [
55+
{
56+
type: 'element',
57+
name: 'w:num',
58+
attributes: { 'w:numId': '42' },
59+
elements: [],
60+
},
61+
],
62+
},
63+
],
64+
},
65+
],
66+
};
67+
68+
const editor = makeEditorWithNumbering(numbering);
69+
const logger = makeLogger();
70+
71+
const validator = createNumberingValidator({ editor, logger });
72+
const result = validator();
73+
74+
expect(result.modified).toBe(false);
75+
expect(result.results).toEqual(['No <w:num> entries with null/invalid numId found.']);
76+
expect(logger.debug).not.toHaveBeenCalled();
77+
// Ensure structure preserved
78+
const root = numbering.elements[0].elements;
79+
expect(root.find((e) => e.name === 'w:num' && e.attributes['w:numId'] === '1')).toBeTruthy();
80+
const nested = root.find((e) => e.name === 'w:pPr').elements;
81+
expect(nested.find((e) => e.name === 'w:num' && e.attributes['w:numId'] === '42')).toBeTruthy();
82+
});
83+
84+
it('removes <w:num> elements with invalid/missing w:numId and reports them', () => {
85+
const numbering = {
86+
elements: [
87+
{
88+
elements: [
89+
// valid
90+
{ type: 'element', name: 'w:num', attributes: { 'w:numId': '10' }, elements: [] },
91+
// invalid: missing attr
92+
{ type: 'element', name: 'w:num', elements: [] },
93+
// invalid: empty string
94+
{ type: 'element', name: 'w:num', attributes: { 'w:numId': '' }, elements: [] },
95+
// invalid: literal "null"
96+
{ type: 'element', name: 'w:num', attributes: { 'w:numId': 'null' }, elements: [] },
97+
// invalid: non-numeric
98+
{ type: 'element', name: 'w:num', attributes: { 'w:numId': '12a' }, elements: [] },
99+
// nested invalid
100+
{
101+
type: 'element',
102+
name: 'w:pPr',
103+
elements: [
104+
{ type: 'element', name: 'w:num', attributes: { 'w:numId': ' ' }, elements: [] },
105+
{ type: 'element', name: 'w:num', attributes: { 'w:numId': '5' }, elements: [] }, // valid nested
106+
],
107+
},
108+
],
109+
},
110+
],
111+
};
112+
113+
const editor = makeEditorWithNumbering(numbering);
114+
const logger = makeLogger();
115+
116+
const validator = createNumberingValidator({ editor, logger });
117+
const result = validator();
118+
119+
expect(result.modified).toBe(true);
120+
expect(result.results).toHaveLength(1);
121+
expect(result.results[0]).toMatch(/^Removed invalid <w:num> by numId:/);
122+
123+
// The order should match traversal order:
124+
// missing node, '', 'null', '12a', '' (trim of spaces)
125+
expect(result.results[0]).toContain('missing node');
126+
expect(result.results[0]).toContain(', ,');
127+
expect(result.results[0]).toContain('null');
128+
expect(result.results[0]).toContain('12a');
129+
130+
expect(logger.debug).toHaveBeenCalledTimes(1);
131+
132+
// Ensure invalid ones are removed but valid remain
133+
const root = numbering.elements[0].elements;
134+
const rootNums = root.filter((e) => e.name === 'w:num');
135+
expect(rootNums).toHaveLength(1);
136+
expect(rootNums[0].attributes['w:numId']).toBe('10');
137+
138+
const nested = root.find((e) => e.name === 'w:pPr').elements;
139+
const nestedNums = nested.filter((e) => e.name === 'w:num');
140+
expect(nestedNums).toHaveLength(1);
141+
expect(nestedNums[0].attributes['w:numId']).toBe('5');
142+
});
143+
});

0 commit comments

Comments
 (0)