Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions e2e/shim.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ declare global {
var __macros__: {
insert: typeof insert
}

var __applyDiff__: (markdown: string) => boolean
var __acceptAll__: () => boolean
var __rejectAll__: () => boolean
var __clearDiff__: () => boolean
var __acceptChunk__: (index: number) => boolean
var __rejectChunk__: (index: number) => boolean
}
11 changes: 11 additions & 0 deletions e2e/src/crepe-diff/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Crepe Diff</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/crepe-diff/main.ts"></script>
</body>
</html>
4 changes: 4 additions & 0 deletions e2e/src/crepe-diff/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const crepeDiff = {
title: 'Crepe Diff',
link: '/crepe-diff/',
}
34 changes: 34 additions & 0 deletions e2e/src/crepe-diff/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Crepe, CrepeFeature } from '@milkdown/crepe'
import '@milkdown/crepe/theme/common/style.css'
import '@milkdown/crepe/theme/frame.css'
import { callCommand } from '@milkdown/utils'

import { setup } from '../utils'

// Command names match the string IDs registered by $command() in plugin-diff.
// Using strings here because @milkdown/kit is not a direct dependency of e2e.
setup(async () => {
const crepe = new Crepe({
root: '#app',
features: {
[CrepeFeature.Diff]: true,
},
})
globalThis.__crepe__ = crepe
await crepe.create()

globalThis.__applyDiff__ = (markdown: string) =>
crepe.editor.action(callCommand('StartDiffReview', markdown))
globalThis.__acceptAll__ = () =>
crepe.editor.action(callCommand('AcceptAllDiffs'))
globalThis.__rejectAll__ = () =>
crepe.editor.action(callCommand('RejectAllDiffs'))
globalThis.__clearDiff__ = () =>
crepe.editor.action(callCommand('ClearDiffReview'))
globalThis.__acceptChunk__ = (index: number) =>
crepe.editor.action(callCommand('AcceptDiffChunk', index))
globalThis.__rejectChunk__ = (index: number) =>
crepe.editor.action(callCommand('RejectDiffChunk', index))

return crepe.editor
}).catch(console.error)
2 changes: 2 additions & 0 deletions e2e/src/data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { crepe } from './crepe'
import { crepeDiff } from './crepe-diff'
import { crepeTopBar } from './crepe-top-bar'
import { imageBlock } from './image-block'
import { multiEditor } from './multi-editor'
Expand All @@ -14,6 +15,7 @@ export const cases: { title: string; link: string }[] = [
listener,
automd,
crepe,
crepeDiff,
crepeTopBar,
imageBlock,
]
260 changes: 260 additions & 0 deletions e2e/tests/crepe/diff.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { expect, test } from '@playwright/test'

import { getMarkdown, setMarkdown, waitNextFrame } from '../misc'

test.beforeEach(async ({ page }) => {
await page.goto('/crepe-diff/')
})

async function applyDiff(page: any, original: string, modified: string) {
await setMarkdown(page, original)
await waitNextFrame(page)
await page.evaluate((md: string) => window.__applyDiff__(md), modified)
await waitNextFrame(page)
}

test.describe('diff commands', () => {
const original = `# Hello World\n\nFirst paragraph.`
const modified = `# Hello Milkdown\n\nUpdated paragraph.`

test('accept all applies the new document', async ({ page }) => {
await applyDiff(page, original, modified)

await page.evaluate(() => window.__acceptAll__())
await waitNextFrame(page)

const markdown = await getMarkdown(page)
expect(markdown).toContain('Hello Milkdown')
expect(markdown).not.toContain('Hello World')
})

test('reject all keeps the original document', async ({ page }) => {
await applyDiff(page, original, modified)

await page.evaluate(() => window.__rejectAll__())
await waitNextFrame(page)

const markdown = await getMarkdown(page)
expect(markdown).toContain('Hello World')
expect(markdown).not.toContain('Hello Milkdown')
})

test('clear diff removes all decorations', async ({ page }) => {
await applyDiff(page, original, modified)

const editor = page.locator('.editor')
await expect(editor.locator('.milkdown-diff-added').first()).toBeVisible()

await page.evaluate(() => window.__clearDiff__())
await waitNextFrame(page)

await expect(editor.locator('.milkdown-diff-added')).toHaveCount(0)
await expect(editor.locator('.milkdown-diff-controls')).toHaveCount(0)
})

test('accept chunk reduces pending changes', async ({ page }) => {
await applyDiff(page, original, modified)

const editor = page.locator('.editor')
const before = await editor
.locator('.milkdown-diff-controls, .milkdown-diff-controls-block')
.count()

await page.evaluate(() => window.__acceptChunk__(0))
await waitNextFrame(page)

const after = await editor
.locator('.milkdown-diff-controls, .milkdown-diff-controls-block')
.count()
expect(after).toBeLessThan(before)
})

test('reject chunk reduces pending changes', async ({ page }) => {
await applyDiff(page, original, modified)

const editor = page.locator('.editor')
const before = await editor
.locator('.milkdown-diff-controls, .milkdown-diff-controls-block')
.count()

await page.evaluate(() => window.__rejectChunk__(0))
await waitNextFrame(page)

const after = await editor
.locator('.milkdown-diff-controls, .milkdown-diff-controls-block')
.count()
expect(after).toBeLessThan(before)
})

test('lock on review prevents document editing', async ({ page }) => {
await applyDiff(page, original, modified)

const editor = page.locator('.editor')
await editor.click()
await page.keyboard.type('SHOULD NOT APPEAR')
await waitNextFrame(page)

const markdown = await getMarkdown(page)
expect(markdown).not.toContain('SHOULD NOT APPEAR')
})
})

test.describe('inline diff', () => {
test('text change within a paragraph shows inline decorations', async ({
page,
}) => {
await applyDiff(
page,
'Hello world, this is a test.',
'Hello milkdown, this is a test.'
)

const editor = page.locator('.editor')
const removed = editor.locator('.milkdown-diff-removed')
const added = editor.locator('.milkdown-diff-added')
await expect(removed.first()).toBeVisible()
await expect(added.first()).toBeVisible()
})

test('format change with text difference shows diff', async ({ page }) => {
await applyDiff(
page,
'This is **bold** and normal text.',
'This is **bold** and *italic* text.'
)

const editor = page.locator('.editor')
const added = editor.locator('.milkdown-diff-added')
await expect(added.first()).toBeVisible()

await page.evaluate(() => window.__acceptAll__())
await waitNextFrame(page)

const markdown = await getMarkdown(page)
expect(markdown).toContain('*italic*')
})

test('accept inline diff updates the text', async ({ page }) => {
await applyDiff(page, 'Hello world.', 'Hello milkdown.')

await page.evaluate(() => window.__acceptAll__())
await waitNextFrame(page)

const markdown = await getMarkdown(page)
expect(markdown).toContain('milkdown')
expect(markdown).not.toContain('world')
})
})

test.describe('block diff', () => {
test('new paragraph shows as block insertion', async ({ page }) => {
await applyDiff(
page,
'# Heading\n\nFirst paragraph.',
'# Heading\n\nFirst paragraph.\n\nSecond paragraph.'
)

const editor = page.locator('.editor')
const addedBlock = editor.locator('.milkdown-diff-added-block')
await expect(addedBlock.first()).toBeVisible()
})

test('deleted heading shows strikethrough', async ({ page }) => {
await applyDiff(
page,
'# First\n\n## Second\n\nParagraph.',
'# First\n\nParagraph.'
)

const editor = page.locator('.editor')
const removed = editor.locator('.milkdown-diff-removed')
await expect(removed.first()).toBeVisible()
})

test('new list item shows as block insertion', async ({ page }) => {
await applyDiff(page, '- Item 1\n- Item 2', '- Item 1\n- Item 2\n- Item 3')

const editor = page.locator('.editor')
const addedBlock = editor.locator('.milkdown-diff-added-block')
await expect(addedBlock.first()).toBeVisible()

await page.evaluate(() => window.__acceptAll__())
await waitNextFrame(page)

const markdown = await getMarkdown(page)
expect(markdown).toContain('Item 3')
})
})

test.describe('image diff', () => {
test('image src change shows new image widget and marks old as removed', async ({
page,
}) => {
await applyDiff(page, '![]()', '![1.0](https://example.com/new.png)')

const editor = page.locator('.editor')
// Old image-block should have removed-block decoration
const removedBlock = editor.locator('.milkdown-diff-removed-block')
await expect(removedBlock.first()).toBeVisible()

// New image should appear in the added widget
const addedBlock = editor.locator('.milkdown-diff-added-block')
await expect(addedBlock.first()).toBeVisible()
})

test('accept image diff updates the document', async ({ page }) => {
await applyDiff(page, '![]()', '![1.0](https://example.com/new.png)')

await page.evaluate(() => window.__acceptAll__())
await waitNextFrame(page)

const markdown = await getMarkdown(page)
expect(markdown).toContain('example.com/new.png')
})
})

test.describe('table diff', () => {
test('table with new column shows as block replacement', async ({ page }) => {
await applyDiff(
page,
'| A | B |\n| - | - |\n| 1 | 2 |',
'| A | B | C |\n| - | - | - |\n| 1 | 2 | 3 |'
)

const editor = page.locator('.editor')
// Old table should be marked as removed block
const removedBlock = editor.locator('.milkdown-diff-removed-block')
await expect(removedBlock.first()).toBeVisible()

// New table should appear in the added widget
const addedBlock = editor.locator('.milkdown-diff-added-block')
await expect(addedBlock.first()).toBeVisible()
})

test('table with new row shows as block replacement', async ({ page }) => {
await applyDiff(
page,
'| A | B |\n| - | - |\n| 1 | 2 |',
'| A | B |\n| - | - |\n| 1 | 2 |\n| 3 | 4 |'
)

const editor = page.locator('.editor')
const addedBlock = editor.locator('.milkdown-diff-added-block')
await expect(addedBlock.first()).toBeVisible()
})

test('accept table diff updates to new table', async ({ page }) => {
await applyDiff(
page,
'| A | B |\n| - | - |\n| 1 | 2 |',
'| A | B | C |\n| - | - | - |\n| 1 | 2 | 3 |'
)

await page.evaluate(() => window.__acceptAll__())
await waitNextFrame(page)

const markdown = await getMarkdown(page)
expect(markdown).toContain('C')
expect(markdown).toContain('3')
})
})
11 changes: 11 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
},
"./table-block": {
"import": "./src/table-block/index.ts"
},
"./diff": {
"import": "./src/diff/index.ts"
}
},
"publishConfig": {
Expand Down Expand Up @@ -70,6 +73,10 @@
"./table-block": {
"types": "./lib/table-block/index.d.ts",
"import": "./lib/table-block/index.js"
},
"./diff": {
"types": "./lib/diff/index.d.ts",
"import": "./lib/diff/index.js"
}
},
"main": "./lib/index.js",
Expand All @@ -93,6 +100,9 @@
],
"table-block": [
"./lib/table-block/index.d.ts"
],
"diff": [
"./lib/diff/index.d.ts"
]
}
}
Expand All @@ -106,6 +116,7 @@
"@milkdown/core": "workspace:*",
"@milkdown/ctx": "workspace:*",
"@milkdown/exception": "workspace:*",
"@milkdown/plugin-diff": "workspace:*",
"@milkdown/plugin-tooltip": "workspace:*",
"@milkdown/preset-commonmark": "workspace:*",
"@milkdown/preset-gfm": "workspace:*",
Expand Down
Loading
Loading