mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
* [web] add preview payload to track-change notification types * [doc-updater] pass accepted/rejected change data through to web for previews * [web] snapshot change preview onto accepted/rejected notifications * [web] render resolved change previews instead of accepted/rejected count lines * [web] add unit coverage for accepted/rejected change preview flow * fix: notification carries multiple previews per accept/reject batch; extract shared group helper * fix: one preview per accept/reject action, drop adjacency grouping * fix: move buildResolvedChangePreviews out of the email builder class * fix: type error, make tests more explicit * fix: render resolver below preview * fix: overflow text, alignment * fix: test error * fix: resolved count * [document-updater/web] send sparse change preview instead of full doc lines * fix: store sparse preview, hydrate at render, drop CE gate * fix: advance slice cursor past tracked-delete text in sparse preview * fix: inline buildChangeGroupPreview * fix: changePreviewBuilder -> sparseChangePreview * fix: rm change_contributors * fix: copilot flagged testing issues * fix: hydrate and truncate change preview at write time * fix: avoid materializing full doc text when slicing preview window * fix: update reject preview test to hydrated shape * fix: accepted delete removing context * [document-updater/web] split resolved change previews into one per change cluster * fix: narrow cluster userIds to string[] for typecheck * feat: updating to use zod schema for previews --------- Co-authored-by: Jimmy Domagala-Tang <jimmy.domagala-tang@overleaf.com> GitOrigin-RevId: 0740713c715a61a220180f353e497b27da6a5b0f
207 lines
7.2 KiB
JavaScript
207 lines
7.2 KiB
JavaScript
const { expect } = require('chai')
|
|
const {
|
|
buildSparseChangePreviews,
|
|
} = require('../../../app/js/TrackedChangePreview')
|
|
|
|
describe('TrackedChangePreview', function () {
|
|
describe('buildSparseChangePreviews', function () {
|
|
it('returns no previews for empty inputs', function () {
|
|
expect(
|
|
buildSparseChangePreviews({ changes: [], lines: ['a'] })
|
|
).to.deep.equal([])
|
|
expect(
|
|
buildSparseChangePreviews({ changes: null, lines: ['a'] })
|
|
).to.deep.equal([])
|
|
expect(
|
|
buildSparseChangePreviews({
|
|
changes: [{ op: { i: 'x', p: 0 } }],
|
|
lines: null,
|
|
})
|
|
).to.deep.equal([])
|
|
})
|
|
|
|
it('finds LaTeX section path from the enclosing header', function () {
|
|
const lines = [
|
|
'\\section{Intro}',
|
|
'Some intro text.',
|
|
'\\subsection{Details}',
|
|
'target line here',
|
|
]
|
|
const position = lines.slice(0, 3).join('\n').length + 5
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [{ op: { i: 'x', p: position } }],
|
|
lines,
|
|
})
|
|
expect(preview.sectionPath).to.deep.equal(['Intro', 'Details'])
|
|
})
|
|
|
|
it('returns empty sectionPath when no header precedes the change', function () {
|
|
const lines = ['plain text', 'more text']
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [{ op: { i: 'x', p: 5 } }],
|
|
lines,
|
|
})
|
|
expect(preview.sectionPath).to.deep.equal([])
|
|
})
|
|
|
|
it('computes 1-based startLine for the first change', function () {
|
|
const lines = ['first', 'second', 'third']
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [{ op: { i: 'x', p: 'first\nsecond\n'.length + 1 } }],
|
|
lines,
|
|
})
|
|
expect(preview.startLine).to.equal(3)
|
|
})
|
|
|
|
it('attributes a position on a line-terminating newline to that line', function () {
|
|
const lines = ['ab', 'cd']
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [{ op: { i: 'x', p: 2 } }],
|
|
lines,
|
|
})
|
|
expect(preview.startLine).to.equal(1)
|
|
})
|
|
|
|
it('clamps a position past the end of the document to the last line', function () {
|
|
const lines = ['ab', 'cd']
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [{ op: { i: 'x', p: 500 } }],
|
|
lines,
|
|
})
|
|
expect(preview.startLine).to.equal(2)
|
|
})
|
|
|
|
it('projects changes to bare ranges-op shape', function () {
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [
|
|
{
|
|
id: 'c1',
|
|
op: { i: 'hi', p: 0 },
|
|
metadata: { user_id: 'u1' },
|
|
},
|
|
{
|
|
id: 'c2',
|
|
op: { d: 'x', p: 5 },
|
|
metadata: { user_id: 'u2' },
|
|
},
|
|
],
|
|
lines: ['abcdefghij'],
|
|
})
|
|
expect(preview.changes).to.deep.equal([
|
|
{ i: 'hi', d: undefined, p: 0 },
|
|
{ i: undefined, d: 'x', p: 5 },
|
|
])
|
|
})
|
|
|
|
it('slices a bounded window of surrounding doc text', function () {
|
|
const filler = 'x'.repeat(2000)
|
|
const lines = [filler, filler, filler]
|
|
const fullText = lines.join('\n')
|
|
const changePos = filler.length + 1 + 5
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [{ op: { i: 'y', p: changePos } }],
|
|
lines,
|
|
})
|
|
// slice should be far shorter than the full doc
|
|
expect(preview.slice.length).to.be.lessThan(fullText.length)
|
|
expect(preview.sliceStart).to.be.lessThanOrEqual(changePos)
|
|
const sliceEndPos = preview.sliceStart + preview.slice.length
|
|
expect(sliceEndPos).to.be.greaterThanOrEqual(changePos)
|
|
})
|
|
|
|
it('treats a tracked delete as zero-width when sizing the window', function () {
|
|
const lines = ['Overleaf is and features are listed below.']
|
|
const deletePos = 'Overleaf is '.length
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [{ op: { d: 'a great tool ', p: deletePos } }],
|
|
lines,
|
|
})
|
|
// The deleted text is not in `lines`, so the window must not extend by
|
|
// its length past the deletion point.
|
|
expect(preview.slice).to.equal(lines[0])
|
|
expect(preview.sliceStart).to.equal(0)
|
|
})
|
|
|
|
describe('clustering', function () {
|
|
it('keeps nearby changes in one preview', function () {
|
|
const lines = ['one two three four five six seven eight nine ten']
|
|
const previews = buildSparseChangePreviews({
|
|
changes: [
|
|
{ op: { i: 'a', p: 0 }, metadata: { user_id: 'u1' } },
|
|
{ op: { i: 'b', p: 20 }, metadata: { user_id: 'u2' } },
|
|
],
|
|
lines,
|
|
})
|
|
expect(previews).to.have.length(1)
|
|
expect(previews[0].changes).to.have.length(2)
|
|
expect(previews[0].userIds).to.deep.equal(['u1', 'u2'])
|
|
})
|
|
|
|
it('splits distant changes into separate previews', function () {
|
|
const filler = 'x'.repeat(5000)
|
|
const lines = ['\\section{Top}', filler, '\\section{Bottom}', filler]
|
|
const topPos = '\\section{Top}\n'.length + 3
|
|
const bottomPos =
|
|
`\\section{Top}\n${filler}\n\\section{Bottom}\n`.length + 3
|
|
const previews = buildSparseChangePreviews({
|
|
changes: [
|
|
{ op: { i: 'a', p: topPos }, metadata: { user_id: 'u1' } },
|
|
{ op: { i: 'b', p: bottomPos }, metadata: { user_id: 'u2' } },
|
|
],
|
|
lines,
|
|
})
|
|
expect(previews).to.have.length(2)
|
|
expect(previews[0].sectionPath).to.deep.equal(['Top'])
|
|
expect(previews[0].startLine).to.equal(2)
|
|
expect(previews[0].userIds).to.deep.equal(['u1'])
|
|
expect(previews[1].sectionPath).to.deep.equal(['Bottom'])
|
|
expect(previews[1].startLine).to.equal(4)
|
|
expect(previews[1].userIds).to.deep.equal(['u2'])
|
|
})
|
|
|
|
it('starts the slice on a line boundary without losing the newline', function () {
|
|
const lines = ['first line', 'second line', 'third line']
|
|
const secondLineStart = 'first line\n'.length
|
|
const [preview] = buildSparseChangePreviews({
|
|
changes: [{ op: { i: 'x', p: secondLineStart } }],
|
|
lines,
|
|
})
|
|
expect(preview.sliceStart).to.equal(0)
|
|
expect(preview.slice).to.equal(lines.join('\n'))
|
|
})
|
|
|
|
it('bounds each preview slice instead of spanning the whole doc', function () {
|
|
const filler = 'x'.repeat(5000)
|
|
const lines = [filler, filler, filler]
|
|
const lastLineStart = (filler.length + 1) * 2
|
|
const previews = buildSparseChangePreviews({
|
|
changes: [
|
|
{ op: { i: 'a', p: 0 } },
|
|
{ op: { i: 'b', p: lastLineStart + 10 } },
|
|
],
|
|
lines,
|
|
})
|
|
expect(previews).to.have.length(2)
|
|
for (const preview of previews) {
|
|
expect(preview.slice.length).to.be.lessThan(1200)
|
|
}
|
|
})
|
|
|
|
it('orders previews by position regardless of input order', function () {
|
|
const lines = ['a'.repeat(100), 'b'.repeat(100), 'c'.repeat(100)]
|
|
const previews = buildSparseChangePreviews({
|
|
changes: [
|
|
{ op: { i: 'late', p: 250 } },
|
|
{ op: { i: 'early', p: 10 } },
|
|
],
|
|
lines,
|
|
})
|
|
expect(previews.map(p => p.changes[0].i)).to.deep.equal([
|
|
'early',
|
|
'late',
|
|
])
|
|
})
|
|
})
|
|
})
|
|
})
|