Skip to content

Commit e80612a

Browse files
committed
chore: add tests to all document-rels functions, add safety checks
1 parent 81f9409 commit e80612a

2 files changed

Lines changed: 326 additions & 5 deletions

File tree

packages/super-editor/src/core/super-converter/docx-helpers/document-rels.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,24 @@ import { RELATIONSHIP_TYPES } from './docx-constants.js';
1010
* @param {Editor} editor The editor instance
1111
* @returns {XmlRelationshipElement[]} An array of relationship elements
1212
*/
13-
const getDocumentRelationshipElements = (editor) => {
14-
const docx = editor.converter.convertedXml;
13+
export const getDocumentRelationshipElements = (editor) => {
14+
const docx = editor.converter?.convertedXml;
15+
if (!docx) return [];
16+
1517
const documentRels = docx['word/_rels/document.xml.rels'];
16-
const relationshipTag = documentRels?.elements.find((el) => el.name === 'Relationships');
17-
return relationshipTag.elements || [];
18+
const elements = documentRels?.elements;
19+
if (!Array.isArray(elements)) return [];
20+
21+
const relationshipTag = elements.find((el) => el.name === 'Relationships');
22+
return relationshipTag?.elements || [];
1823
};
1924

2025
/**
2126
* Get the maximum relationship ID from existing relationships.
2227
* @param {XmlRelationshipElement[]} relationships The array of relationship elements
2328
* @returns {number} The maximum relationship ID integer
2429
*/
25-
const getMaxRelationshipIdInt = (relationships) => {
30+
export const getMaxRelationshipIdInt = (relationships) => {
2631
const ids = [];
2732
relationships.forEach((rel) => {
2833
const splitId = rel.attributes.Id.split('rId');
@@ -31,6 +36,8 @@ const getMaxRelationshipIdInt = (relationships) => {
3136
ids.push(parsedInt);
3237
}
3338
});
39+
40+
if (ids.length === 0) return 0;
3441
return Math.max(...ids);
3542
};
3643

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
getNewRelationshipId,
4+
getDocumentRelationshipElements,
5+
getMaxRelationshipIdInt,
6+
insertNewRelationship,
7+
findRelationshipIdFromTarget,
8+
} from './document-rels.js';
9+
import { RELATIONSHIP_TYPES } from './docx-constants.js';
10+
11+
describe('getNewRelationshipId', () => {
12+
it('returns rId1 when no existing relationships', () => {
13+
const editor = {
14+
converter: {
15+
convertedXml: {
16+
'word/_rels/document.xml.rels': {
17+
elements: [
18+
{
19+
name: 'Relationships',
20+
elements: [],
21+
},
22+
],
23+
},
24+
},
25+
},
26+
};
27+
28+
const result = getNewRelationshipId(editor);
29+
expect(result).toBe('rId1');
30+
});
31+
32+
it('returns rIdN+1 when max rId is rId11', () => {
33+
const editor = {
34+
converter: {
35+
convertedXml: {
36+
'word/_rels/document.xml.rels': {
37+
elements: [
38+
{
39+
name: 'Relationships',
40+
elements: [{ attributes: { Id: 'rId5' } }, { attributes: { Id: 'rId11' } }],
41+
},
42+
],
43+
},
44+
},
45+
},
46+
};
47+
48+
const result = getNewRelationshipId(editor);
49+
expect(result).toBe('rId12');
50+
});
51+
52+
it('returns rId1 if Relationships tag is missing', () => {
53+
const editor = {
54+
converter: {
55+
convertedXml: {
56+
'word/_rels/document.xml.rels': {
57+
elements: [], // no Relationships tag
58+
},
59+
},
60+
},
61+
};
62+
63+
const result = getNewRelationshipId(editor);
64+
expect(result).toBe('rId1');
65+
});
66+
});
67+
68+
describe('getMaxRelationshipIdInt', () => {
69+
it('returns 0 for an empty relationships array', () => {
70+
const result = getMaxRelationshipIdInt([]);
71+
expect(result).toBe(0);
72+
});
73+
74+
it('returns the max numeric value from valid rId strings', () => {
75+
const relationships = [
76+
{ attributes: { Id: 'rId2' } },
77+
{ attributes: { Id: 'rId10' } },
78+
{ attributes: { Id: 'rId5' } },
79+
];
80+
const result = getMaxRelationshipIdInt(relationships);
81+
expect(result).toBe(10);
82+
});
83+
84+
it('ignores malformed rIds and returns max from valid ones', () => {
85+
const relationships = [
86+
{ attributes: { Id: 'rId3' } },
87+
{ attributes: { Id: 'invalid' } },
88+
{ attributes: { Id: 'rIDX' } }, // Not a number
89+
{ attributes: { Id: 'rId42' } },
90+
{ attributes: { Id: 'rId' } }, // No number
91+
];
92+
const result = getMaxRelationshipIdInt(relationships);
93+
expect(result).toBe(42);
94+
});
95+
96+
it('returns 0 if no valid numeric rIds are present', () => {
97+
const relationships = [
98+
{ attributes: { Id: 'foo' } },
99+
{ attributes: { Id: 'rIdABC' } },
100+
{ attributes: { Id: 'bar' } },
101+
];
102+
const result = getMaxRelationshipIdInt(relationships);
103+
expect(result).toBe(0);
104+
});
105+
106+
it('handles rIds with prefixes like "word_rId23"', () => {
107+
const relationships = [{ attributes: { Id: 'word_rId7' } }, { attributes: { Id: 'word_rId12' } }];
108+
const result = getMaxRelationshipIdInt(relationships);
109+
expect(result).toBe(12); // split on 'rId', then parse
110+
});
111+
112+
it('handles rIds with leading zeros like "rId0012"', () => {
113+
const relationships = [{ attributes: { Id: 'rId0005' } }, { attributes: { Id: 'rId0012' } }];
114+
const result = getMaxRelationshipIdInt(relationships);
115+
expect(result).toBe(12);
116+
});
117+
});
118+
119+
describe('getDocumentRelationshipElements', () => {
120+
it('returns relationship elements when structure is valid', () => {
121+
const editor = {
122+
converter: {
123+
convertedXml: {
124+
'word/_rels/document.xml.rels': {
125+
elements: [
126+
{
127+
name: 'Relationships',
128+
elements: [{ attributes: { Id: 'rId1' } }, { attributes: { Id: 'rId2' } }],
129+
},
130+
],
131+
},
132+
},
133+
},
134+
};
135+
136+
const result = getDocumentRelationshipElements(editor);
137+
expect(result).toHaveLength(2);
138+
expect(result[0].attributes.Id).toBe('rId1');
139+
expect(result[1].attributes.Id).toBe('rId2');
140+
});
141+
142+
it('returns an empty array if document.xml.rels is missing', () => {
143+
const editor = {
144+
converter: {
145+
convertedXml: {},
146+
},
147+
};
148+
149+
const result = getDocumentRelationshipElements(editor);
150+
expect(result).toEqual([]);
151+
});
152+
153+
it('returns an empty array if Relationships tag is missing', () => {
154+
const editor = {
155+
converter: {
156+
convertedXml: {
157+
'word/_rels/document.xml.rels': {
158+
elements: [{ name: 'OtherTag', elements: [{}, {}] }],
159+
},
160+
},
161+
},
162+
};
163+
164+
const result = getDocumentRelationshipElements(editor);
165+
expect(result).toEqual([]);
166+
});
167+
168+
it('returns an empty array if Relationships tag has no elements', () => {
169+
const editor = {
170+
converter: {
171+
convertedXml: {
172+
'word/_rels/document.xml.rels': {
173+
elements: [{ name: 'Relationships' }],
174+
},
175+
},
176+
},
177+
};
178+
179+
const result = getDocumentRelationshipElements(editor);
180+
expect(result).toEqual([]);
181+
});
182+
183+
it('returns an empty array if documentRels has no elements key', () => {
184+
const editor = {
185+
converter: {
186+
convertedXml: {
187+
'word/_rels/document.xml.rels': {},
188+
},
189+
},
190+
};
191+
192+
const result = getDocumentRelationshipElements(editor);
193+
expect(result).toEqual([]);
194+
});
195+
});
196+
197+
describe('insertNewRelationship', () => {
198+
let mockEditor;
199+
200+
beforeEach(() => {
201+
mockEditor = {
202+
converter: {
203+
convertedXml: {
204+
'word/_rels/document.xml.rels': {
205+
elements: [
206+
{
207+
name: 'Relationships',
208+
elements: [
209+
{
210+
attributes: {
211+
Id: 'rId42',
212+
Target: 'foo',
213+
},
214+
},
215+
],
216+
},
217+
],
218+
},
219+
},
220+
},
221+
};
222+
223+
vi.spyOn(console, 'warn').mockImplementation(() => {});
224+
vi.spyOn(console, 'error').mockImplementation(() => {});
225+
vi.spyOn(console, 'info').mockImplementation(() => {});
226+
});
227+
228+
it('throws if target is not a non-empty string', () => {
229+
expect(() => insertNewRelationship(null, 'hyperlink', mockEditor)).toThrow();
230+
expect(() => insertNewRelationship(123, 'hyperlink', mockEditor)).toThrow();
231+
expect(() => insertNewRelationship('', 'hyperlink', mockEditor)).toThrow();
232+
});
233+
234+
it('throws if type is not a non-empty string', () => {
235+
expect(() => insertNewRelationship('foo', null, mockEditor)).toThrow();
236+
expect(() => insertNewRelationship('foo', 123, mockEditor)).toThrow();
237+
expect(() => insertNewRelationship('foo', '', mockEditor)).toThrow();
238+
});
239+
240+
it('throws if editor is not provided', () => {
241+
expect(() => insertNewRelationship('foo', 'hyperlink')).toThrow();
242+
});
243+
244+
it('returns null and warns on unsupported type', () => {
245+
const result = insertNewRelationship('foo', 'unsupportedType', mockEditor);
246+
expect(result).toBeNull();
247+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Unsupported relationship type'));
248+
});
249+
250+
it('returns existing relationship if already present', () => {
251+
vi.spyOn({ findRelationshipIdFromTarget }, 'findRelationshipIdFromTarget').mockReturnValueOnce('rId42');
252+
253+
const result = insertNewRelationship('foo', 'hyperlink', mockEditor);
254+
expect(result).toBe('rId42');
255+
expect(console.info).toHaveBeenCalledWith(expect.stringContaining('Reusing existing relationship for target'));
256+
});
257+
258+
it('returns null if editor.converter.convertedXml is missing', () => {
259+
const badEditor = { converter: {} };
260+
const result = insertNewRelationship('foo', 'hyperlink', badEditor);
261+
expect(result).toBeNull();
262+
expect(console.error).toHaveBeenCalledWith('No converted XML found in editor');
263+
});
264+
265+
it('returns null if documentRels is missing', () => {
266+
const badEditor = {
267+
converter: {
268+
convertedXml: {},
269+
},
270+
};
271+
const result = insertNewRelationship('foo', 'hyperlink', badEditor);
272+
expect(result).toBeNull();
273+
expect(console.error).toHaveBeenCalledWith('No document relationships found in the docx');
274+
});
275+
276+
it('returns null if Relationships tag is missing', () => {
277+
const editor = {
278+
converter: {
279+
convertedXml: {
280+
'word/_rels/document.xml.rels': {
281+
elements: [],
282+
},
283+
},
284+
},
285+
};
286+
287+
const result = insertNewRelationship('foo', 'hyperlink', editor);
288+
expect(result).toBeNull();
289+
expect(console.error).toHaveBeenCalledWith('No Relationships tag found in document relationships');
290+
});
291+
292+
it('returns null if getNewRelationshipId fails', () => {
293+
const result = insertNewRelationship('bar', 'hyperlink', mockEditor);
294+
expect(result).toBe('rId43');
295+
});
296+
297+
it('inserts a new relationship and returns the new rId', () => {
298+
const result = insertNewRelationship('bar', 'hyperlink', mockEditor);
299+
expect(result).toBe('rId43');
300+
301+
const relationshipsTag = mockEditor.converter.convertedXml['word/_rels/document.xml.rels'].elements[0];
302+
const lastElement = relationshipsTag.elements[relationshipsTag.elements.length - 1];
303+
304+
expect(lastElement).toEqual({
305+
type: 'element',
306+
name: 'Relationship',
307+
attributes: {
308+
Id: 'rId43',
309+
Type: RELATIONSHIP_TYPES.hyperlink,
310+
Target: 'bar',
311+
},
312+
});
313+
});
314+
});

0 commit comments

Comments
 (0)