diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/addMarkStep.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/addMarkStep.js index 57a5f16e5e..2b56b4e2ef 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/addMarkStep.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/addMarkStep.js @@ -70,12 +70,14 @@ export const addMarkStep = ({ state, step, newTr, doc, user, date }) => { }); } } else { - before = liveMarks - .filter((mark) => ![TrackDeleteMarkName, TrackFormatMarkName].includes(mark.type.name)) - .map((mark) => ({ - type: mark.type.name, - attrs: { ...mark.attrs }, - })); + const existingMarkOfSameType = liveMarks.find( + (mark) => + mark.type.name === step.mark.type.name && + ![TrackDeleteMarkName, TrackFormatMarkName].includes(mark.type.name), + ); + before = existingMarkOfSameType + ? [{ type: existingMarkOfSameType.type.name, attrs: { ...existingMarkOfSameType.attrs } }] + : []; after = [ { diff --git a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/trackChangesHelpers.test.js b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/trackChangesHelpers.test.js index 35700e19f3..437bffb45a 100644 --- a/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/trackChangesHelpers.test.js +++ b/packages/super-editor/src/extensions/track-changes/trackChangesHelpers/trackChangesHelpers.test.js @@ -399,6 +399,53 @@ describe('trackChangesHelpers', () => { expect(meta?.formatMark?.attrs?.after).toEqual([{ type: 'highlight', attrs: { color: '#E4668C' } }]); }); + it('addMarkStep does not include unrelated marks in before (SD-2077)', () => { + const highlight = schema.marks.highlight.create({ color: '#FFFF00' }); + const doc = createDocWithText('Hello', [highlight]); + const state = createState(doc); + const boldMark = schema.marks.bold.create(); + const step = new AddMarkStep(1, 6, boldMark); + const newTr = state.tr; + + addMarkStep({ + state, + step, + newTr, + doc: state.doc, + user, + date, + }); + + const meta = newTr.getMeta(TrackChangesBasePluginKey); + expect(meta?.formatMark?.type.name).toBe(TrackFormatMarkName); + expect(meta?.formatMark?.attrs?.before).toEqual([]); + expect(meta?.formatMark?.attrs?.after).toEqual([{ type: 'bold', attrs: boldMark.attrs }]); + }); + + it('addMarkStep only captures same-type mark in before when replacing (SD-2077)', () => { + const highlight = schema.marks.highlight.create({ color: '#FFFF00' }); + const textStyle = schema.marks.textStyle.create({ color: '#112233', fontSize: '11pt' }); + const doc = createDocWithText('Hello', [highlight, textStyle]); + const state = createState(doc); + const changedTextStyle = schema.marks.textStyle.create({ color: '#FF0000', fontSize: '11pt' }); + const step = new AddMarkStep(1, 6, changedTextStyle); + const newTr = state.tr; + + addMarkStep({ + state, + step, + newTr, + doc: state.doc, + user, + date, + }); + + const meta = newTr.getMeta(TrackChangesBasePluginKey); + expect(meta?.formatMark?.type.name).toBe(TrackFormatMarkName); + expect(meta?.formatMark?.attrs?.before).toEqual([{ type: 'textStyle', attrs: textStyle.attrs }]); + expect(meta?.formatMark?.attrs?.after).toEqual([{ type: 'textStyle', attrs: changedTextStyle.attrs }]); + }); + it('removeMarkStep records previous formatting when mark removed', () => { const bold = schema.marks.bold.create(); const doc = createDocWithText('Styled', [bold]); diff --git a/tests/behavior/tests/comments/sd-2077-format-change-display.spec.ts b/tests/behavior/tests/comments/sd-2077-format-change-display.spec.ts new file mode 100644 index 0000000000..ef72631087 --- /dev/null +++ b/tests/behavior/tests/comments/sd-2077-format-change-display.spec.ts @@ -0,0 +1,134 @@ +import type { Page } from '@playwright/test'; +import { test, expect } from '../../fixtures/superdoc.js'; +import { assertDocumentApiReady } from '../../helpers/document-api.js'; + +test.use({ config: { toolbar: 'full', comments: 'on', trackChanges: true } }); + +type EditorCommand = [name: string, ...args: unknown[]]; + +async function runCommands(page: Page, commands: EditorCommand[]): Promise { + for (const [name, ...args] of commands) { + await page.evaluate(({ name, args }) => (window as any).editor.commands[name](...args), { name, args }); + } +} + +async function expectTrackedFormatDialog(page: Page) { + const dialog = page.locator('.comment-placeholder .comments-dialog', { + has: page.locator('.tracked-change-text'), + }); + await expect(dialog).toBeVisible({ timeout: 10_000 }); + return dialog; +} + +test.describe('SD-2077 tracked format change displays correct description', () => { + test('adding bold to highlighted text shows only bold addition', async ({ superdoc }) => { + await assertDocumentApiReady(superdoc.page); + + // Type text and apply highlight in editing mode + await superdoc.type('Hello world'); + await superdoc.waitForStable(); + await superdoc.selectAll(); + await runCommands(superdoc.page, [['setHighlight', '#FFFF00']]); + await superdoc.waitForStable(); + + // Switch to suggesting mode + await superdoc.setDocumentMode('suggesting'); + await superdoc.waitForStable(); + + // Select text and apply bold + await superdoc.selectAll(); + await superdoc.bold(); + await superdoc.waitForStable(); + + // Verify tracked format change exists + await superdoc.assertTrackedChangeExists('format'); + + // Wait for the tracked change comment dialog to appear + const dialog = await expectTrackedFormatDialog(superdoc.page); + + // The format description should mention bold but NOT mention highlight removal + const formatText = dialog.locator('.tracked-change-text'); + await expect(formatText).toContainText('bold'); + + const text = await formatText.textContent(); + expect(text).not.toContain('removed'); + expect(text).not.toContain('highlight'); + + await superdoc.snapshot('sd-2077-bold-on-highlighted'); + }); + + test('adding italic to bold text shows only italic addition', async ({ superdoc }) => { + await assertDocumentApiReady(superdoc.page); + + // Type text and apply bold in editing mode + await superdoc.type('Hello world'); + await superdoc.waitForStable(); + await superdoc.selectAll(); + await superdoc.bold(); + await superdoc.waitForStable(); + + // Verify bold is applied + await superdoc.assertTextHasMarks('Hello', ['bold']); + + // Switch to suggesting mode + await superdoc.setDocumentMode('suggesting'); + await superdoc.waitForStable(); + + // Select text and apply italic + await superdoc.selectAll(); + await superdoc.italic(); + await superdoc.waitForStable(); + + // Verify tracked format change exists + await superdoc.assertTrackedChangeExists('format'); + + const dialog = await expectTrackedFormatDialog(superdoc.page); + + // Should show italic addition, not bold removal + const formatText = dialog.locator('.tracked-change-text'); + await expect(formatText).toContainText('italic'); + + const text = await formatText.textContent(); + expect(text).not.toContain('removed'); + expect(text).not.toContain('bold'); + + await superdoc.snapshot('sd-2077-italic-on-bold'); + }); + + test('changing color on highlighted text shows only color change', async ({ superdoc }) => { + await assertDocumentApiReady(superdoc.page); + + // Type text and apply highlight + color in editing mode + await superdoc.type('Hello world'); + await superdoc.waitForStable(); + await superdoc.selectAll(); + await runCommands(superdoc.page, [ + ['setHighlight', '#FFFF00'], + ['setColor', '#112233'], + ]); + await superdoc.waitForStable(); + + // Switch to suggesting mode + await superdoc.setDocumentMode('suggesting'); + await superdoc.waitForStable(); + + // Select text and change color + await superdoc.selectAll(); + await runCommands(superdoc.page, [['setColor', '#FF0000']]); + await superdoc.waitForStable(); + + // Verify tracked format change exists + await superdoc.assertTrackedChangeExists('format'); + + const dialog = await expectTrackedFormatDialog(superdoc.page); + + // Should show color change, not highlight removal + const formatText = dialog.locator('.tracked-change-text'); + await expect(formatText).toContainText('color'); + + const text = await formatText.textContent(); + expect(text).not.toContain('removed highlight'); + + await superdoc.snapshot('sd-2077-color-on-highlighted'); + }); +});