Skip to content

Commit 2d087f2

Browse files
authored
fix: behavior tests (#2436)
* test(behavior): fix stale comment edit selector and tracked-change history assertions * fix(editor): preserve highlight marks in resolved selection formatting
1 parent 2978507 commit 2d087f2

8 files changed

Lines changed: 190 additions & 192 deletions

File tree

packages/super-editor/src/core/helpers/getMarksFromSelection.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function getFormattingStateAtPos(state, pos, editor, options = {}) {
6262
const resolvedRunProperties = resolvedFromSelection?.resolvedRunProperties ?? inlineRunProperties;
6363
const styleRunProperties = resolvedFromSelection?.styleRunProperties ?? null;
6464
const resolvedMarksFromProperties = createMarksFromRunProperties(state, resolvedRunProperties, editor);
65-
resolvedMarks.push(...(resolvedMarksFromProperties.length ? resolvedMarksFromProperties : inlineMarks));
65+
resolvedMarks.push(...mergeResolvedMarksWithInlineFallback(resolvedMarksFromProperties, inlineMarks));
6666
if (storedMarks && includeCursorMarksWithStoredMarks) {
6767
resolvedMarks.push(...cursorMarks);
6868
}
@@ -99,16 +99,28 @@ function aggregateFormattingSegments(state, editor, segments) {
9999
const resolvedRunProperties = intersectRunProperties(segments.map((segment) => segment.resolvedRunProperties));
100100
const inlineRunProperties = intersectRunProperties(segments.map((segment) => segment.inlineRunProperties));
101101
const styleRunProperties = intersectRunProperties(segments.map((segment) => segment.styleRunProperties));
102+
const resolvedMarks = createMarksFromRunProperties(state, resolvedRunProperties, editor);
103+
const inlineMarks = createMarksFromRunProperties(state, inlineRunProperties, editor);
102104

103105
return {
104-
resolvedMarks: createMarksFromRunProperties(state, resolvedRunProperties, editor),
105-
inlineMarks: createMarksFromRunProperties(state, inlineRunProperties, editor),
106+
resolvedMarks: mergeResolvedMarksWithInlineFallback(resolvedMarks, inlineMarks),
107+
inlineMarks,
106108
resolvedRunProperties,
107109
inlineRunProperties,
108110
styleRunProperties,
109111
};
110112
}
111113

114+
function mergeResolvedMarksWithInlineFallback(resolvedMarks, inlineMarks) {
115+
if (!resolvedMarks.length) return inlineMarks;
116+
if (!inlineMarks.length) return resolvedMarks;
117+
118+
const resolvedMarkNames = new Set(resolvedMarks.map((mark) => mark.type.name));
119+
const missingInlineMarks = inlineMarks.filter((mark) => !resolvedMarkNames.has(mark.type.name));
120+
121+
return [...resolvedMarks, ...missingInlineMarks];
122+
}
123+
112124
function intersectRunProperties(runPropertiesList) {
113125
const filtered = runPropertiesList.filter((props) => props && typeof props === 'object');
114126
if (filtered.length === 0) return null;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
import { EditorState, TextSelection } from 'prosemirror-state';
3+
import { Schema } from 'prosemirror-model';
4+
5+
const resolveRunProperties = vi.fn(() => ({ bold: true }));
6+
7+
vi.mock('@superdoc/style-engine/ooxml', () => ({
8+
resolveRunProperties,
9+
}));
10+
11+
vi.mock('@extensions/paragraph/resolvedPropertiesCache.js', () => ({
12+
calculateResolvedParagraphProperties: vi.fn(() => ({})),
13+
}));
14+
15+
describe('getSelectionFormattingState resolved mark fallback', () => {
16+
it('preserves inline highlight when resolved marks omit it', async () => {
17+
const { getSelectionFormattingState } = await import('./getMarksFromSelection.js');
18+
19+
const schema = new Schema({
20+
nodes: {
21+
doc: { content: 'paragraph+' },
22+
paragraph: {
23+
content: 'inline*',
24+
group: 'block',
25+
toDOM() {
26+
return ['p', 0];
27+
},
28+
},
29+
run: {
30+
content: 'text*',
31+
group: 'inline',
32+
inline: true,
33+
attrs: { runProperties: { default: null } },
34+
toDOM() {
35+
return ['span', 0];
36+
},
37+
},
38+
text: { group: 'inline' },
39+
},
40+
marks: {
41+
bold: {
42+
attrs: { value: { default: true } },
43+
toDOM() {
44+
return ['strong', 0];
45+
},
46+
},
47+
highlight: {
48+
attrs: { color: { default: null } },
49+
toDOM() {
50+
return ['mark', 0];
51+
},
52+
},
53+
},
54+
});
55+
56+
const doc = schema.node('doc', null, [
57+
schema.node('paragraph', null, [
58+
schema.node('run', { runProperties: { highlight: { 'w:val': '#ECCF35' } } }, [schema.text('Hello')]),
59+
]),
60+
]);
61+
const baseState = EditorState.create({ schema, doc });
62+
const state = baseState.apply(baseState.tr.setSelection(TextSelection.create(doc, 2, 7)));
63+
64+
const result = getSelectionFormattingState(state, { converter: { convertedXml: {} } });
65+
66+
expect(resolveRunProperties).toHaveBeenCalled();
67+
expect(result.inlineMarks).toContainEqual(expect.objectContaining({ attrs: { color: '#ECCF35' } }));
68+
expect(result.resolvedMarks).toContainEqual(expect.objectContaining({ attrs: { value: true } }));
69+
expect(result.resolvedMarks).toContainEqual(expect.objectContaining({ attrs: { color: '#ECCF35' } }));
70+
});
71+
});

packages/super-editor/src/core/helpers/getMarksFromSelection.test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,52 @@ describe('getMarksFromSelection', () => {
202202
expect(result.inlineRunProperties).toEqual({ styleId: 'Heading1Char' });
203203
});
204204

205+
it('reconstructs highlight marks from hash-prefixed runProperties values', () => {
206+
const runSchema = new Schema({
207+
nodes: {
208+
doc: { content: 'paragraph+' },
209+
paragraph: {
210+
content: 'inline*',
211+
group: 'block',
212+
toDOM() {
213+
return ['p', 0];
214+
},
215+
},
216+
run: {
217+
content: 'text*',
218+
group: 'inline',
219+
inline: true,
220+
attrs: { runProperties: { default: null } },
221+
toDOM() {
222+
return ['span', 0];
223+
},
224+
},
225+
text: { group: 'inline' },
226+
},
227+
marks: {
228+
highlight: {
229+
attrs: { color: { default: null } },
230+
toDOM() {
231+
return ['mark', 0];
232+
},
233+
},
234+
},
235+
});
236+
const testDoc = runSchema.node('doc', null, [
237+
runSchema.node('paragraph', null, [
238+
runSchema.node('run', { runProperties: { highlight: { 'w:val': '#ECCF35' } } }, [runSchema.text('Hello')]),
239+
]),
240+
]);
241+
const state = EditorState.create({ schema: runSchema, doc: testDoc });
242+
const cursorState = state.apply(state.tr.setSelection(TextSelection.create(testDoc, 3)));
243+
244+
const result = getSelectionFormattingState(cursorState);
245+
246+
expect(result.inlineRunProperties).toEqual({ highlight: { 'w:val': '#ECCF35' } });
247+
expect(result.inlineMarks).toContainEqual(expect.objectContaining({ attrs: { color: '#ECCF35' } }));
248+
expect(result.resolvedMarks).toContainEqual(expect.objectContaining({ attrs: { color: '#ECCF35' } }));
249+
});
250+
205251
it('falls back to cursor marks when the surrounding run has no explicit runProperties', () => {
206252
const runSchema = new Schema({
207253
nodes: {

packages/super-editor/src/core/super-converter/styles.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import {
77
twipsToLines,
88
eighthPointsToPixels,
99
linesToTwips,
10+
isValidHexColor,
11+
getHexColorFromDocxSystem,
12+
normalizeHexColor,
1013
} from '@converter/helpers.js';
11-
import { isValidHexColor, getHexColorFromDocxSystem } from '@converter/helpers';
1214
import { SuperConverter } from '@converter/SuperConverter.js';
1315
import { getUnderlineCssString } from '@extensions/linked-styles/underline-css.js';
1416
import {
@@ -670,11 +672,35 @@ function getFontFamilyValue(attributes, docx) {
670672
* @returns {string|null} Hex color string, 'transparent', or null when unsupported.
671673
*/
672674
function getHighLightValue(attributes) {
673-
const fill = attributes['w:fill'];
674-
if (fill && fill !== 'auto') return `#${fill}`;
675-
if (attributes?.['w:val'] === 'none') return 'transparent';
676-
if (isValidHexColor(attributes?.['w:val'])) return `#${attributes['w:val']}`;
677-
return getHexColorFromDocxSystem(attributes?.['w:val']) || null;
675+
const fill = normalizeHighlightHex(attributes?.['w:fill']);
676+
if (fill) return `#${fill}`;
677+
678+
const value = attributes?.['w:val'];
679+
if (value === 'none') return 'transparent';
680+
681+
const normalizedValue = normalizeHighlightHex(value);
682+
if (normalizedValue) return `#${normalizedValue}`;
683+
684+
return getHexColorFromDocxSystem(value) || null;
685+
}
686+
687+
/**
688+
* Normalize a highlight token to a 6-digit hex string without a leading hash.
689+
* Returns null for non-hex values such as DOCX system color keywords.
690+
*
691+
* @param {unknown} rawValue
692+
* @returns {string|null}
693+
*/
694+
function normalizeHighlightHex(rawValue) {
695+
if (typeof rawValue !== 'string') return null;
696+
697+
const trimmedValue = rawValue.trim();
698+
if (!trimmedValue || trimmedValue.toLowerCase() === 'auto') return null;
699+
700+
const normalizedValue = normalizeHexColor(trimmedValue);
701+
if (!normalizedValue || !isValidHexColor(normalizedValue)) return null;
702+
703+
return normalizedValue;
678704
}
679705

680706
/**

packages/super-editor/src/core/super-converter/styles.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ describe('encodeMarksFromRPr', () => {
5151
});
5252
});
5353

54+
it('should encode highlight from a hash-prefixed w:val', () => {
55+
const rPr = { highlight: { 'w:val': '#ECCF35' } };
56+
const marks = encodeMarksFromRPr(rPr, {});
57+
expect(marks).toContainEqual({
58+
type: 'highlight',
59+
attrs: { color: '#ECCF35' },
60+
});
61+
});
62+
5463
it('should encode highlight from w:shd', () => {
5564
const rPr = { shading: { fill: 'FFA500' } };
5665
const marks = encodeMarksFromRPr(rPr, {});

0 commit comments

Comments
 (0)