Files
standardnotes-app-web/packages/snjs/mocha/item_manager.test.js
2022-10-11 23:54:00 +05:30

603 lines
22 KiB
JavaScript

/* eslint-disable no-unused-expressions */
/* eslint-disable no-undef */
import * as Factory from './lib/factory.js'
chai.use(chaiAsPromised)
const expect = chai.expect
describe('item manager', function () {
beforeEach(async function () {
this.payloadManager = new PayloadManager()
this.itemManager = new ItemManager(this.payloadManager)
this.createNote = async () => {
return this.itemManager.createItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
}
this.createTag = async (notes = []) => {
const references = notes.map((note) => {
return {
uuid: note.uuid,
content_type: note.content_type,
}
})
return this.itemManager.createItem(ContentType.Tag, {
title: 'thoughts',
references: references,
})
}
})
it('create item', async function () {
const item = await this.createNote()
expect(item).to.be.ok
expect(item.title).to.equal('hello')
})
it('emitting item through payload and marking dirty should have userModifiedDate', async function () {
const payload = Factory.createNotePayload()
const item = await this.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
const result = await this.itemManager.setItemDirty(item)
const appData = result.payload.content.appData
expect(appData[DecryptedItem.DefaultAppDomain()][AppDataField.UserModifiedDate]).to.be.ok
})
it('find items with valid uuid', async function () {
const item = await this.createNote()
const results = await this.itemManager.findItems([item.uuid])
expect(results.length).to.equal(1)
expect(results[0]).to.equal(item)
})
it('find items with invalid uuid no blanks', async function () {
const results = await this.itemManager.findItems([Factory.generateUuidish()])
expect(results.length).to.equal(0)
})
it('find items with invalid uuid include blanks', async function () {
const includeBlanks = true
const results = await this.itemManager.findItemsIncludingBlanks([Factory.generateUuidish()])
expect(results.length).to.equal(1)
expect(results[0]).to.not.be.ok
})
it('item state', async function () {
await this.createNote()
expect(this.itemManager.items.length).to.equal(1)
expect(this.itemManager.getDisplayableNotes().length).to.equal(1)
})
it('find item', async function () {
const item = await this.createNote()
const foundItem = this.itemManager.findItem(item.uuid)
expect(foundItem).to.be.ok
})
it('reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
expect(this.itemManager.collection.referenceMap.directMap[tag.uuid]).to.eql([note.uuid])
})
it('inverse reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
expect(this.itemManager.collection.referenceMap.inverseMap[note.uuid]).to.eql([tag.uuid])
})
it('inverse reference map should not have duplicates', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
await this.itemManager.changeItem(tag)
expect(this.itemManager.collection.referenceMap.inverseMap[note.uuid]).to.eql([tag.uuid])
})
it('deleting from reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
await this.itemManager.setItemToBeDeleted(note)
expect(this.itemManager.collection.referenceMap.directMap[tag.uuid]).to.eql([])
expect(this.itemManager.collection.referenceMap.inverseMap[note.uuid].length).to.equal(0)
})
it('deleting referenced item should update referencing item references', async function () {
const note = await this.createNote()
let tag = await this.createTag([note])
await this.itemManager.setItemToBeDeleted(note)
tag = this.itemManager.findItem(tag.uuid)
expect(tag.content.references.length).to.equal(0)
})
it('removing relationship should update reference map', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
await this.itemManager.changeItem(tag, (mutator) => {
mutator.removeItemAsRelationship(note)
})
expect(this.itemManager.collection.referenceMap.directMap[tag.uuid]).to.eql([])
expect(this.itemManager.collection.referenceMap.inverseMap[note.uuid]).to.eql([])
})
it('emitting discardable payload should remove it from our collection', async function () {
const note = await this.createNote()
const payload = new DeletedPayload({
...note.payload.ejected(),
content: undefined,
deleted: true,
dirty: false,
})
expect(payload.discardable).to.equal(true)
await this.itemManager.emitItemFromPayload(payload)
expect(this.itemManager.findItem(note.uuid)).to.not.be.ok
})
it('items that reference item', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
const itemsThatReference = this.itemManager.itemsReferencingItem(note)
expect(itemsThatReference.length).to.equal(1)
expect(itemsThatReference[0]).to.equal(tag)
})
it('observer', async function () {
const observed = []
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, source, sourceKey }) => {
observed.push({ changed, inserted, removed, source, sourceKey })
})
const note = await this.createNote()
const tag = await this.createTag([note])
expect(observed.length).to.equal(2)
const firstObserved = observed[0]
expect(firstObserved.inserted).to.eql([note])
const secondObserved = observed[1]
expect(secondObserved.inserted).to.eql([tag])
})
it('change existing item', async function () {
const note = await this.createNote()
const newTitle = String(Math.random())
await this.itemManager.changeItem(note, (mutator) => {
mutator.title = newTitle
})
const latestVersion = this.itemManager.findItem(note.uuid)
expect(latestVersion.title).to.equal(newTitle)
})
it('change non-existant item through uuid should fail', async function () {
const note = await this.itemManager.createTemplateItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
const changeFn = async () => {
const newTitle = String(Math.random())
return this.itemManager.changeItem(note, (mutator) => {
mutator.title = newTitle
})
}
await Factory.expectThrowsAsync(() => changeFn(), 'Attempting to change non-existant item')
})
it('set items dirty', async function () {
const note = await this.createNote()
await this.itemManager.setItemDirty(note)
const dirtyItems = this.itemManager.getDirtyItems()
expect(dirtyItems.length).to.equal(1)
expect(dirtyItems[0].uuid).to.equal(note.uuid)
expect(dirtyItems[0].dirty).to.equal(true)
})
it('dirty items should not include errored items', async function () {
const note = await this.itemManager.setItemDirty(await this.createNote())
const errorred = new EncryptedPayload({
...note.payload,
content: '004:...',
errorDecrypting: true,
})
await this.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
const dirtyItems = this.itemManager.getDirtyItems()
expect(dirtyItems.length).to.equal(0)
})
it('dirty items should include errored items if they are being deleted', async function () {
const note = await this.itemManager.setItemDirty(await this.createNote())
const errorred = new DeletedPayload({
...note.payload,
content: undefined,
errorDecrypting: true,
deleted: true,
})
await this.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged)
const dirtyItems = this.itemManager.getDirtyItems()
expect(dirtyItems.length).to.equal(1)
})
describe('duplicateItem', async function () {
const sandbox = sinon.createSandbox()
beforeEach(async function () {
this.emitPayloads = sandbox.spy(this.itemManager.payloadManager, 'emitPayloads')
})
afterEach(async function () {
sandbox.restore()
})
it('should duplicate the item and set the duplicate_of property', async function () {
const note = await this.createNote()
await this.itemManager.duplicateItem(note)
sinon.assert.calledTwice(this.emitPayloads)
const originalNote = this.itemManager.getDisplayableNotes()[0]
const duplicatedNote = this.itemManager.getDisplayableNotes()[1]
expect(this.itemManager.items.length).to.equal(2)
expect(this.itemManager.getDisplayableNotes().length).to.equal(2)
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
expect(duplicatedNote.conflictOf).to.be.undefined
expect(duplicatedNote.payload.content.conflict_of).to.be.undefined
})
it('should duplicate the item and set the duplicate_of and conflict_of properties', async function () {
const note = await this.createNote()
await this.itemManager.duplicateItem(note, true)
sinon.assert.calledTwice(this.emitPayloads)
const originalNote = this.itemManager.getDisplayableNotes()[0]
const duplicatedNote = this.itemManager.getDisplayableNotes()[1]
expect(this.itemManager.items.length).to.equal(2)
expect(this.itemManager.getDisplayableNotes().length).to.equal(2)
expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid)
expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of)
expect(originalNote.uuid).to.equal(duplicatedNote.conflictOf)
expect(originalNote.uuid).to.equal(duplicatedNote.payload.content.conflict_of)
})
it('duplicate item with relationships', async function () {
const note = await this.createNote()
const tag = await this.createTag([note])
const duplicate = await this.itemManager.duplicateItem(tag)
expect(duplicate.content.references).to.have.length(1)
expect(this.itemManager.items).to.have.length(3)
expect(this.itemManager.getDisplayableTags()).to.have.length(2)
})
it('adds duplicated item as a relationship to items referencing it', async function () {
const note = await this.createNote()
let tag = await this.createTag([note])
const duplicateNote = await this.itemManager.duplicateItem(note)
expect(tag.content.references).to.have.length(1)
tag = this.itemManager.findItem(tag.uuid)
const references = tag.content.references.map((ref) => ref.uuid)
expect(references).to.have.length(2)
expect(references).to.include(note.uuid, duplicateNote.uuid)
})
it('duplicates item with additional content', async function () {
const note = await this.itemManager.createItem(ContentType.Note, {
title: 'hello',
text: 'world',
})
const duplicateNote = await this.itemManager.duplicateItem(note, false, {
title: 'hello (copy)',
})
expect(duplicateNote.title).to.equal('hello (copy)')
expect(duplicateNote.text).to.equal('world')
})
})
it('set item deleted', async function () {
const note = await this.createNote()
await this.itemManager.setItemToBeDeleted(note)
/** Items should never be mutated directly */
expect(note.deleted).to.not.be.ok
const latestVersion = this.payloadManager.findOne(note.uuid)
expect(latestVersion.deleted).to.equal(true)
expect(latestVersion.dirty).to.equal(true)
expect(latestVersion.content).to.not.be.ok
/** Deleted items do not show up in item manager's public interface */
expect(this.itemManager.items.length).to.equal(0)
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
})
it('system smart views', async function () {
expect(this.itemManager.systemSmartViews.length).to.be.above(0)
})
it('find tag by title', async function () {
const tag = await this.createTag()
expect(this.itemManager.findTagByTitle(tag.title)).to.be.ok
})
it('find tag by title should be case insensitive', async function () {
const tag = await this.createTag()
expect(this.itemManager.findTagByTitle(tag.title.toUpperCase())).to.be.ok
})
it('find or create tag by title', async function () {
const title = 'foo'
expect(await this.itemManager.findOrCreateTagByTitle(title)).to.be.ok
})
it('note count', async function () {
await this.createNote()
expect(this.itemManager.noteCount).to.equal(1)
})
it('trash', async function () {
const note = await this.createNote()
const versionTwo = await this.itemManager.changeItem(note, (mutator) => {
mutator.trashed = true
})
expect(this.itemManager.trashSmartView).to.be.ok
expect(versionTwo.trashed).to.equal(true)
expect(versionTwo.dirty).to.equal(true)
expect(versionTwo.content).to.be.ok
expect(this.itemManager.items.length).to.equal(1)
expect(this.itemManager.trashedItems.length).to.equal(1)
await this.itemManager.emptyTrash()
const versionThree = this.payloadManager.findOne(note.uuid)
expect(versionThree.deleted).to.equal(true)
expect(this.itemManager.trashedItems.length).to.equal(0)
})
it('remove all items from memory', async function () {
const observed = []
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
observed.push({ changed, inserted, removed, ignored })
})
await this.createNote()
await this.itemManager.removeAllItemsFromMemory()
const deletionEvent = observed[1]
expect(deletionEvent.removed[0].deleted).to.equal(true)
expect(this.itemManager.items.length).to.equal(0)
})
it('remove item locally', async function () {
const observed = []
this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => {
observed.push({ changed, inserted, removed, ignored })
})
const note = await this.createNote()
await this.itemManager.removeItemLocally(note)
expect(observed.length).to.equal(1)
expect(this.itemManager.findItem(note.uuid)).to.not.be.ok
})
it('emitting a payload from within observer should queue to end', async function () {
/**
* From within an item observer, we want to emit some changes and await them.
* We expect that the end result is that whatever was most recently emitted,
* is propagated to listeners after any pending observation events. That is, when you
* emit items, it should be done serially, so that emitting while you're emitting does
* not interrupt the current emission, but instead queues it. This is so that changes
* are not propagated out of order.
*/
const payload = Factory.createNotePayload()
const changedTitle = 'changed title'
let didEmit = false
let latestVersion
this.itemManager.addObserver(ContentType.Note, ({ changed, inserted }) => {
const all = changed.concat(inserted)
if (!didEmit) {
didEmit = true
const changedPayload = payload.copy({
content: {
...payload.content,
title: changedTitle,
},
})
this.itemManager.emitItemFromPayload(changedPayload)
}
latestVersion = all[0]
})
await this.itemManager.emitItemFromPayload(payload)
expect(latestVersion.title).to.equal(changedTitle)
})
describe('searchTags', async function () {
it('should return tag with query matching title', async function () {
const tag = await this.itemManager.findOrCreateTagByTitle('tag')
const results = this.itemManager.searchTags('tag')
expect(results).lengthOf(1)
expect(results[0].title).to.equal(tag.title)
})
it('should return all tags with query partially matching title', async function () {
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag one')
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag two')
const results = this.itemManager.searchTags('tag')
expect(results).lengthOf(2)
expect(results[0].title).to.equal(firstTag.title)
expect(results[1].title).to.equal(secondTag.title)
})
it('should be case insensitive', async function () {
const tag = await this.itemManager.findOrCreateTagByTitle('Tag')
const results = this.itemManager.searchTags('tag')
expect(results).lengthOf(1)
expect(results[0].title).to.equal(tag.title)
})
it('should return tag with query matching delimiter separated component', async function () {
const tag = await this.itemManager.findOrCreateTagByTitle('parent.child')
const results = this.itemManager.searchTags('child')
expect(results).lengthOf(1)
expect(results[0].title).to.equal(tag.title)
})
it('should return tags with matching query including delimiter', async function () {
const tag = await this.itemManager.findOrCreateTagByTitle('parent.child')
const results = this.itemManager.searchTags('parent.chi')
expect(results).lengthOf(1)
expect(results[0].title).to.equal(tag.title)
})
it('should return tags in natural order', async function () {
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag 100')
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag 2')
const thirdTag = await this.itemManager.findOrCreateTagByTitle('tag b')
const fourthTag = await this.itemManager.findOrCreateTagByTitle('tag a')
const results = this.itemManager.searchTags('tag')
expect(results).lengthOf(4)
expect(results[0].title).to.equal(secondTag.title)
expect(results[1].title).to.equal(firstTag.title)
expect(results[2].title).to.equal(fourthTag.title)
expect(results[3].title).to.equal(thirdTag.title)
})
it('should not return tags associated with note', async function () {
const firstTag = await this.itemManager.findOrCreateTagByTitle('tag one')
const secondTag = await this.itemManager.findOrCreateTagByTitle('tag two')
const note = await this.createNote()
await this.itemManager.changeItem(firstTag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
})
const results = this.itemManager.searchTags('tag', note)
expect(results).lengthOf(1)
expect(results[0].title).to.equal(secondTag.title)
})
})
describe('smart views', async function () {
it('all view should not include archived notes by default', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
mutator.archived = true
})
this.itemManager.setPrimaryItemDisplayOptions({
views: [this.itemManager.allNotesSmartView],
})
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
})
it('archived view should not include trashed notes by default', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
mutator.archived = true
mutator.trashed = true
})
this.itemManager.setPrimaryItemDisplayOptions({
views: [this.itemManager.archivedSmartView],
})
expect(this.itemManager.getDisplayableNotes().length).to.equal(0)
})
it('trashed view should include archived notes by default', async function () {
const normal = await this.createNote()
await this.itemManager.changeItem(normal, (mutator) => {
mutator.archived = true
mutator.trashed = true
})
this.itemManager.setPrimaryItemDisplayOptions({
views: [this.itemManager.trashSmartView],
})
expect(this.itemManager.getDisplayableNotes().length).to.equal(1)
})
})
describe('getSortedTagsForNote', async function () {
it('should return tags associated with a note in natural order', async function () {
const tags = [
await this.itemManager.findOrCreateTagByTitle('tag 100'),
await this.itemManager.findOrCreateTagByTitle('tag 2'),
await this.itemManager.findOrCreateTagByTitle('tag b'),
await this.itemManager.findOrCreateTagByTitle('tag a'),
]
const note = await this.createNote()
tags.map(async (tag) => {
await this.itemManager.changeItem(tag, (mutator) => {
mutator.e2ePendingRefactor_addItemAsRelationship(note)
})
})
const results = this.itemManager.getSortedTagsForItem(note)
expect(results).lengthOf(tags.length)
expect(results[0].title).to.equal(tags[1].title)
expect(results[1].title).to.equal(tags[0].title)
expect(results[2].title).to.equal(tags[3].title)
expect(results[3].title).to.equal(tags[2].title)
})
})
describe('getTagParentChain', function () {
it('should return parent tags for a tag', async function () {
const [parent, child, grandchild, _other] = await Promise.all([
this.itemManager.findOrCreateTagByTitle('parent'),
this.itemManager.findOrCreateTagByTitle('parent.child'),
this.itemManager.findOrCreateTagByTitle('parent.child.grandchild'),
this.itemManager.findOrCreateTagByTitle('some other tag'),
])
await this.itemManager.setTagParent(parent, child)
await this.itemManager.setTagParent(child, grandchild)
const results = this.itemManager.getTagParentChain(grandchild)
expect(results).lengthOf(2)
expect(results[0].uuid).to.equal(parent.uuid)
expect(results[1].uuid).to.equal(child.uuid)
})
})
})