When accepting tracked move revisions in OOXML, a move source can remain without a matching destination in the same document part. Accepting that revision requires removing the source wrapper and its range markers, because leaving orphaned markers behind can make later document processing target stale revision boundaries.
acceptChanges applies that cleanup by removing w:moveFrom content, removing move range markers, and preserving ordinary paragraph content that is outside the move source. The scenario below checks that the fallback path resolves the orphaned move without throwing and without removing unrelated text.[1]
Below is a test scenario of the orphaned move fallback case of acceptChanges: orphaned move wrappers are removed while non-orphan text remains.
The scenario
Given a document with orphaned move wrappers,
When orphaned move wrappers are removed without throwing,
Then
- one move is resolved.
moveFromRangeStartis removed.moveFromRangeEndis removed.- orphan source text is removed.
- non-orphan text is preserved.
The test fixture
The fixture builds a paragraph with a tracked move source, its source range markers, and ordinary text that is not part of the move. The scenario then runs acceptChanges through the test helper and checks both the summary counter and the serialized XML.[2]
Below is the test fixture code.
humanReadableTest.openspec('orphaned moves handled with safe fallback')(
'orphaned moves handled with safe fallback',
async ({ given, when, then, and, attachPrettyJson }: AllureBddContext) => {
const input = [
'<w:p>',
'<w:moveFromRangeStart w:id="91"/>',
'<w:moveFrom w:id="91"><w:r><w:t>Orphan source</w:t></w:r></w:moveFrom>',
'<w:r><w:t>Still here</w:t></w:r>',
'<w:moveFromRangeEnd w:id="91"/>',
'</w:p>',
].join('');
let result: { xml: string; summary: ReturnType<typeof acceptChanges> };
await given('a document with orphaned move wrappers', async () => {});
await when('orphaned move wrappers are removed without throwing', async () => {
result = runAcceptChanges(input);
await attachPrettyJson('accept-orphaned-moves-result', result);
});
await then('one move is resolved', async () => {
expect(result.summary.movesResolved).toBe(1);
});
await and('moveFromRangeStart is removed', async () => {
expect(result.xml.includes('moveFromRangeStart')).toBe(false);
});
await and('moveFromRangeEnd is removed', async () => {
expect(result.xml.includes('moveFromRangeEnd')).toBe(false);
});
await and('orphan source text is removed', async () => {
expect(result.xml.includes('Orphan source')).toBe(false);
});
await and('non-orphan text is preserved', async () => {
expect(result.xml.includes('Still here')).toBe(true);
});
},
);
The expected outcome
The scenario asserts on the summary returned by the helper and on predicate checks against the serialized XML, so the expected outcome is the set of literal checks below.
Below is the result that acceptChanges is expected to produce for this scenario.
expect(result.summary.movesResolved).toBe(1);
expect(result.xml.includes('moveFromRangeStart')).toBe(false);
expect(result.xml.includes('moveFromRangeEnd')).toBe(false);
expect(result.xml.includes('Orphan source')).toBe(false);
expect(result.xml.includes('Still here')).toBe(true);
Below is a description of the expected fields:
summary.movesResolvedis expected to be1, because removing the singlew:moveFromwrapper counts as resolving one move source.xmlis expected not to includemoveFromRangeStart,moveFromRangeEnd, orOrphan source, because the orphaned move source and its range markers are removed during acceptance.xmlis expected to includeStill here, because that run sits outside the move source and remains part of the accepted paragraph.
A non-obvious detail
The move conformance entry describes tracked moves as paired w:moveFrom and w:moveTo content with range markers. This scenario covers the malformed or partial case where the move source is present without a destination, and acceptChanges treats the source side as removed content instead of requiring a complete pair.[3]