mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-27 15:41:26 +00:00
e76bcbec8d
Add empty shell for React history view, viewable based on split test GitOrigin-RevId: 97241d56a052648c7a0d293d323dad4b00ad4159
650 lines
20 KiB
JavaScript
650 lines
20 KiB
JavaScript
/* eslint-disable
|
|
max-len,
|
|
no-return-assign,
|
|
*/
|
|
/* global inject, sinon */
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
import HistoryV2Manager from '../../../../frontend/js/ide/history/HistoryV2Manager'
|
|
|
|
export default describe('HistoryV2Manager', function () {
|
|
beforeEach(function (done) {
|
|
this.defaultHistoryScope = {
|
|
isV2: true,
|
|
updates: [],
|
|
viewMode: 'point_in_time',
|
|
nextBeforeTimestamp: null,
|
|
loading: false,
|
|
atEnd: false,
|
|
userHasFullFeature: undefined,
|
|
freeHistoryLimitHit: false,
|
|
selection: {
|
|
docs: {},
|
|
pathname: null,
|
|
range: {
|
|
fromV: null,
|
|
toV: null,
|
|
},
|
|
hoveredRange: {
|
|
fromV: null,
|
|
toV: null,
|
|
},
|
|
diff: null,
|
|
files: [],
|
|
file: null,
|
|
},
|
|
error: null,
|
|
showOnlyLabels: false,
|
|
labels: null,
|
|
loadingFileTree: true,
|
|
isReact: false,
|
|
}
|
|
|
|
this.sampleUpdates = [
|
|
{
|
|
fromV: 4,
|
|
toV: 5,
|
|
meta: {
|
|
users: [
|
|
{
|
|
first_name: 'john.doe',
|
|
last_name: '',
|
|
email: 'john.doe@domain.tld',
|
|
id: '5b57299087712202fb599ab4',
|
|
hue: 200,
|
|
},
|
|
],
|
|
start_ts: 1544021278346,
|
|
end_ts: 1544021278346,
|
|
},
|
|
labels: [
|
|
{
|
|
id: '5c07e822042e67003b448f18',
|
|
comment: 'My first label',
|
|
version: 5,
|
|
user_id: '5b57299087712202fb599ab4',
|
|
created_at: '2018-12-05T15:00:50.688Z',
|
|
},
|
|
],
|
|
pathnames: [],
|
|
project_ops: [
|
|
{
|
|
add: {
|
|
pathname: 'chapters/chapter1.tex',
|
|
},
|
|
atV: 4,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
fromV: 3,
|
|
toV: 4,
|
|
meta: {
|
|
users: [
|
|
{
|
|
first_name: 'john.doe',
|
|
last_name: '',
|
|
email: 'john.doe@domain.tld',
|
|
id: '5b57299087712202fb599ab4',
|
|
hue: 200,
|
|
},
|
|
],
|
|
start_ts: 1544021262622,
|
|
end_ts: 1544021262622,
|
|
},
|
|
labels: [],
|
|
pathnames: ['main.tex'],
|
|
project_ops: [],
|
|
},
|
|
{
|
|
fromV: 0,
|
|
toV: 3,
|
|
meta: {
|
|
users: [
|
|
{
|
|
first_name: 'john.doe',
|
|
last_name: '',
|
|
email: 'john.doe@domain.tld',
|
|
id: '5b57299087712202fb599ab4',
|
|
hue: 200,
|
|
},
|
|
],
|
|
start_ts: 1544021213540,
|
|
end_ts: 1544021213618,
|
|
},
|
|
labels: [],
|
|
pathnames: [],
|
|
project_ops: [
|
|
{
|
|
add: {
|
|
pathname: 'universe.jpg',
|
|
},
|
|
atV: 2,
|
|
},
|
|
{
|
|
add: {
|
|
pathname: 'references.bib',
|
|
},
|
|
atV: 1,
|
|
},
|
|
{
|
|
add: {
|
|
pathname: 'main.tex',
|
|
},
|
|
atV: 0,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
|
|
inject(($q, $http, $filter, $rootScope) => {
|
|
this.$scope = $rootScope.$new()
|
|
this.$scope.project = {
|
|
features: {
|
|
versioning: true,
|
|
},
|
|
}
|
|
this.$scope.user = {
|
|
isAdmin: false,
|
|
}
|
|
this.ide = {
|
|
globalEditorWatchdogManager: { attachToEditor() {} },
|
|
$q,
|
|
$http,
|
|
$filter,
|
|
}
|
|
this.eventTracking = {
|
|
sendMB: () => {},
|
|
}
|
|
this.localStorage = sinon.stub().returns(null)
|
|
this.historyManager = new HistoryV2Manager(
|
|
this.ide,
|
|
this.$scope,
|
|
this.localStorage,
|
|
this.eventTracking
|
|
)
|
|
done()
|
|
})
|
|
})
|
|
|
|
it('should setup the history scope on initialization', function () {
|
|
expect(this.$scope.history).to.deep.equal(this.defaultHistoryScope)
|
|
})
|
|
|
|
it('should keep history updates after performing a soft reset', function () {
|
|
const historyScopeWithUpdates = Object.assign(
|
|
{},
|
|
this.defaultHistoryScope,
|
|
{
|
|
updates: this.sampleUpdates,
|
|
}
|
|
)
|
|
this.$scope.history.updates = this.sampleUpdates
|
|
this.historyManager.softReset()
|
|
expect(this.$scope.history).to.deep.equal(historyScopeWithUpdates)
|
|
})
|
|
|
|
it('should discard history updates after performing a hard reset', function () {
|
|
this.$scope.history.updates = this.sampleUpdates
|
|
this.historyManager.hardReset()
|
|
expect(this.$scope.history).to.deep.equal(this.defaultHistoryScope)
|
|
})
|
|
|
|
it('should setup history with full access to the feature if the project has versioning', function () {
|
|
this.$scope.$digest()
|
|
expect(this.$scope.history.userHasFullFeature).to.equal(true)
|
|
})
|
|
|
|
it('should setup history without full access to the feature if the project does not have versioning', function () {
|
|
this.$scope.project.features.versioning = false
|
|
this.historyManager = new HistoryV2Manager(
|
|
this.ide,
|
|
this.$scope,
|
|
this.localStorage
|
|
)
|
|
this.$scope.$digest()
|
|
expect(this.$scope.history.userHasFullFeature).to.equal(false)
|
|
})
|
|
|
|
it('should setup history with full access to the feature for admin users even if the project does not have versioning', function () {
|
|
this.$scope.project.features.versioning = false
|
|
this.$scope.user.isAdmin = true
|
|
this.historyManager = new HistoryV2Manager(
|
|
this.ide,
|
|
this.$scope,
|
|
this.localStorage
|
|
)
|
|
this.$scope.$digest()
|
|
expect(this.$scope.history.userHasFullFeature).to.equal(true)
|
|
})
|
|
|
|
describe('autoSelectFile', function () {
|
|
describe('for compare mode', function () {
|
|
beforeEach(function () {
|
|
this.mockedFilesList = [
|
|
{
|
|
pathname: 'main.tex',
|
|
},
|
|
{
|
|
pathname: 'references.bib',
|
|
},
|
|
{
|
|
pathname: 'universe.jpg',
|
|
},
|
|
{
|
|
pathname: 'chapters/chapter2.tex',
|
|
},
|
|
{
|
|
pathname: 'chapters/draft.tex',
|
|
operation: 'removed',
|
|
deletedAtV: 47,
|
|
},
|
|
{
|
|
pathname: 'chapters/chapter3.tex',
|
|
operation: 'added',
|
|
},
|
|
{
|
|
pathname: 'chapters/chapter1.tex',
|
|
operation: 'edited',
|
|
},
|
|
{
|
|
pathname: 'chapters/foo.tex',
|
|
oldPathname: 'chapters/bar.tex',
|
|
operation: 'renamed',
|
|
},
|
|
]
|
|
this.mockedMainTex = this.mockedFilesList[0]
|
|
this.mockedReferencesFile = this.mockedFilesList[1]
|
|
this.mockedRemovedFile = this.mockedFilesList[4]
|
|
this.mockedAddedFile = this.mockedFilesList[5]
|
|
this.mockedEditedFile = this.mockedFilesList[6]
|
|
this.mockedRenamedFile = this.mockedFilesList[7]
|
|
|
|
this.$scope.history.viewMode = 'compare'
|
|
this.$scope.history.selection.files = this.mockedFilesList
|
|
})
|
|
|
|
describe('with a previously selected file', function () {
|
|
it('should prefer the previously selected file if it is available and has operations', function () {
|
|
this.historyManager._previouslySelectedPathname =
|
|
this.mockedAddedFile.pathname
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file).to.deep.equal(
|
|
this.mockedAddedFile
|
|
)
|
|
})
|
|
|
|
it('should prefer a file with ops if the previously selected file is available but has no operations', function () {
|
|
this.historyManager._previouslySelectedPathname =
|
|
this.mockedReferencesFile.pathname
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.operation).to.exist
|
|
})
|
|
|
|
it('should ignore the previously selected file if it is not available', function () {
|
|
this.historyManager._previouslySelectedPathname = 'non/existent.file'
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.not.equal(
|
|
'non/existent.file'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('without a previously selected file, with a list of files containing operations', function () {
|
|
it('should prefer edited files', function () {
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file).to.deep.equal(
|
|
this.mockedEditedFile
|
|
)
|
|
})
|
|
|
|
it('should prefer added files if no edited files are present', function () {
|
|
const indexOfEditedFile = this.$scope.history.selection.files.indexOf(
|
|
this.mockedEditedFile
|
|
)
|
|
this.$scope.history.selection.files.splice(indexOfEditedFile, 1)
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file).to.deep.equal(
|
|
this.mockedAddedFile
|
|
)
|
|
})
|
|
|
|
it('should prefer renamed files if no edited or added files are present', function () {
|
|
const indexOfEditedFile = this.$scope.history.selection.files.indexOf(
|
|
this.mockedEditedFile
|
|
)
|
|
this.$scope.history.selection.files.splice(indexOfEditedFile, 1)
|
|
const indexOfAddedFile = this.$scope.history.selection.files.indexOf(
|
|
this.mockedAddedFile
|
|
)
|
|
this.$scope.history.selection.files.splice(indexOfAddedFile, 1)
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file).to.deep.equal(
|
|
this.mockedRenamedFile
|
|
)
|
|
})
|
|
|
|
it('should prefer removed files if no edited, added or renamed files are present', function () {
|
|
const indexOfEditedFile = this.$scope.history.selection.files.indexOf(
|
|
this.mockedEditedFile
|
|
)
|
|
this.$scope.history.selection.files.splice(indexOfEditedFile, 1)
|
|
const indexOfAddedFile = this.$scope.history.selection.files.indexOf(
|
|
this.mockedAddedFile
|
|
)
|
|
this.$scope.history.selection.files.splice(indexOfAddedFile, 1)
|
|
const indexOfRenamedFile =
|
|
this.$scope.history.selection.files.indexOf(this.mockedRenamedFile)
|
|
this.$scope.history.selection.files.splice(indexOfRenamedFile, 1)
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file).to.deep.equal(
|
|
this.mockedRemovedFile
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('without a previously selected file, with a list of files without operations', function () {
|
|
beforeEach(function () {
|
|
this.mockedFilesListWithNoOps = [
|
|
{
|
|
pathname: 'main.tex',
|
|
},
|
|
{
|
|
pathname: 'references.bib',
|
|
},
|
|
{
|
|
pathname: 'other.tex',
|
|
},
|
|
{
|
|
pathname: 'universe.jpg',
|
|
},
|
|
]
|
|
this.mockedMainTex = this.mockedFilesListWithNoOps[0]
|
|
this.mockedReferencesFile = this.mockedFilesListWithNoOps[1]
|
|
this.mockedOtherTexFile = this.mockedFilesListWithNoOps[2]
|
|
|
|
this.$scope.history.selection.files = this.mockedFilesListWithNoOps
|
|
})
|
|
|
|
it('should prefer main.tex if it exists', function () {
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file).to.deep.equal(
|
|
this.mockedMainTex
|
|
)
|
|
})
|
|
|
|
it('should prefer another tex file if main.tex does not exist', function () {
|
|
const indexOfMainTex = this.$scope.history.selection.files.indexOf(
|
|
this.mockedMainTex
|
|
)
|
|
this.$scope.history.selection.files.splice(indexOfMainTex, 1)
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file).to.deep.equal(
|
|
this.mockedOtherTexFile
|
|
)
|
|
})
|
|
|
|
it('should pick the first available file if no tex files are available', function () {
|
|
const indexOfMainTex = this.$scope.history.selection.files.indexOf(
|
|
this.mockedMainTex
|
|
)
|
|
this.$scope.history.selection.files.splice(indexOfMainTex, 1)
|
|
const indexOfOtherTexFile =
|
|
this.$scope.history.selection.files.indexOf(this.mockedOtherTexFile)
|
|
this.$scope.history.selection.files.splice(indexOfOtherTexFile, 1)
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file).to.deep.equal(
|
|
this.mockedReferencesFile
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('for point-in-time mode', function () {
|
|
beforeEach(function () {
|
|
this.$scope.history.viewMode = 'point_in_time'
|
|
this.sampleUpdates[0] = {
|
|
fromV: 4,
|
|
toV: 5,
|
|
meta: {
|
|
users: [
|
|
{
|
|
first_name: 'john.doe',
|
|
last_name: '',
|
|
email: 'john.doe@domain.tld',
|
|
id: '5b57299087712202fb599ab4',
|
|
hue: 200,
|
|
},
|
|
],
|
|
start_ts: 1544021278346,
|
|
end_ts: 1544021278346,
|
|
},
|
|
pathnames: ['main.tex'],
|
|
project_ops: [
|
|
{
|
|
add: {
|
|
pathname: 'chapters/chapter1.tex',
|
|
},
|
|
atV: 4,
|
|
},
|
|
{
|
|
rename: {
|
|
pathname: 'foo.tex',
|
|
newPathname: 'bar.tex',
|
|
},
|
|
},
|
|
],
|
|
}
|
|
this.sampleUpdateEditedFile = this.sampleUpdates[0].pathnames[0]
|
|
this.sampleUpdateAddedFile =
|
|
this.sampleUpdates[0].project_ops[0].add.pathname
|
|
this.sampleUpdateRenamedFile =
|
|
this.sampleUpdates[0].project_ops[1].rename.newPathname
|
|
this.$scope.history.updates = this.sampleUpdates
|
|
this.$scope.history.selection.range = {
|
|
fromV: this.sampleUpdates[0].toV,
|
|
toV: this.sampleUpdates[0].toV,
|
|
}
|
|
this.mockedFilesList = [
|
|
{
|
|
pathname: 'main.tex',
|
|
},
|
|
{
|
|
pathname: 'references.bib',
|
|
},
|
|
{
|
|
pathname: 'universe.jpg',
|
|
},
|
|
{
|
|
pathname: 'chapters/chapter2.tex',
|
|
},
|
|
{
|
|
pathname: 'chapters/draft.tex',
|
|
},
|
|
{
|
|
pathname: 'chapters/chapter3.tex',
|
|
},
|
|
{
|
|
pathname: 'chapters/chapter1.tex',
|
|
},
|
|
{
|
|
pathname: 'bar.tex',
|
|
},
|
|
]
|
|
this.$scope.history.selection.files = this.mockedFilesList
|
|
})
|
|
|
|
describe('with a previously selected file', function () {
|
|
it('should prefer the previously selected file if it is available and has operations', function () {
|
|
this.historyManager._previouslySelectedPathname = 'main.tex'
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.equal(
|
|
this.sampleUpdateEditedFile
|
|
)
|
|
})
|
|
|
|
it('should prefer a file with ops if the previously selected file is available but has no operations', function () {
|
|
this.historyManager._previouslySelectedPathname = 'main.tex'
|
|
this.sampleUpdates[0].pathnames = []
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.equal(
|
|
this.sampleUpdateAddedFile
|
|
)
|
|
})
|
|
|
|
it('should ignore the previously selected file if it is not available', function () {
|
|
this.historyManager._previouslySelectedPathname = 'non/existent.file'
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.not.equal(
|
|
'non/existent.file'
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('without a previously selected file, with a list of files containing operations', function () {
|
|
it('should prefer edited files', function () {
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.equal(
|
|
this.sampleUpdateEditedFile
|
|
)
|
|
})
|
|
|
|
it('should prefer added files if no edited files are present', function () {
|
|
this.sampleUpdates[0].pathnames = []
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.equal(
|
|
this.sampleUpdateAddedFile
|
|
)
|
|
})
|
|
|
|
it('should prefer renamed files if no edited or added files are present', function () {
|
|
this.sampleUpdates[0].pathnames = []
|
|
this.sampleUpdates[0].project_ops.shift()
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.equal(
|
|
this.sampleUpdateRenamedFile
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('without a previously selected file, with a list of files without operations', function () {
|
|
beforeEach(function () {
|
|
this.sampleUpdates[0].pathnames = []
|
|
this.sampleUpdates[0].project_ops = []
|
|
this.mockedMainTex = this.mockedFilesList[0]
|
|
this.mockedReferencesFile = this.mockedFilesList[1]
|
|
})
|
|
|
|
it('should prefer main.tex if it exists', function () {
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.equal(
|
|
this.mockedMainTex.pathname
|
|
)
|
|
})
|
|
|
|
it('should prefer another tex file if main.tex does not exist', function () {
|
|
const indexOfMainTex = this.$scope.history.selection.files.indexOf(
|
|
this.mockedMainTex
|
|
)
|
|
this.$scope.history.selection.files.splice(indexOfMainTex, 1)
|
|
this.historyManager.autoSelectFile()
|
|
expect(this.$scope.history.selection.file.pathname).to.match(/.tex$/)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('_loadLabels', function () {
|
|
it('should return labels list as is if there is a label for the last version', function () {
|
|
const labels = [
|
|
{
|
|
id: '1',
|
|
version: 1,
|
|
comment: 'foo',
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
{
|
|
id: '2',
|
|
version: 2,
|
|
comment: 'bar',
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
{
|
|
id: '3',
|
|
version: 3,
|
|
comment: 'baz',
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
]
|
|
const lastUpdate = 3
|
|
|
|
const labelsResult = this.historyManager._loadLabels(labels, lastUpdate)
|
|
|
|
expect(labelsResult).to.have.members(labels)
|
|
})
|
|
|
|
it('should return a labels list with a pseudo current state label if there is no label for the last version', function () {
|
|
const labels = [
|
|
{
|
|
id: '1',
|
|
version: 1,
|
|
comment: 'foo',
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
{
|
|
id: '2',
|
|
version: 2,
|
|
comment: 'bar',
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
{
|
|
id: '3',
|
|
version: 3,
|
|
comment: 'baz',
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
]
|
|
const lastUpdate = 5
|
|
|
|
const labelsResult = this.historyManager._loadLabels(labels, lastUpdate)
|
|
|
|
expect(labelsResult).to.include.members(labels)
|
|
expect(labelsResult[0].isPseudoCurrentStateLabel).to.equal(true)
|
|
expect(labelsResult[0].version).to.equal(5)
|
|
})
|
|
|
|
it('should keep pseudo label when deleting label', function () {
|
|
this.historyManager.$scope.history.labels = [
|
|
{
|
|
id: '1',
|
|
version: 1,
|
|
comment: 'foo',
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
]
|
|
const lastUpdate = 5
|
|
|
|
this.historyManager.$scope.history.labels =
|
|
this.historyManager._loadLabels(
|
|
this.historyManager.$scope.history.labels,
|
|
lastUpdate
|
|
)
|
|
|
|
expect(
|
|
this.historyManager.$scope.history.labels[0].isPseudoCurrentStateLabel
|
|
).to.equal(true)
|
|
|
|
this.historyManager.$scope.history.labels =
|
|
this.historyManager._loadLabels([], lastUpdate)
|
|
expect(
|
|
this.historyManager.$scope.history.labels[0].isPseudoCurrentStateLabel
|
|
).to.equal(true)
|
|
expect(this.historyManager.$scope.history.labels[0].version).to.equal(5)
|
|
})
|
|
})
|
|
})
|
|
})
|