mirror of
https://github.com/laurent22/joplin.git
synced 2026-10-25 02:47:28 +00:00
1452 lines
56 KiB
TypeScript
1452 lines
56 KiB
TypeScript
import { setupDatabaseAndSynchronizer, switchClient, createNTestNotes, createNTestFolders, createNTestTags } from './testing/test-utils';
|
|
import reducer, { defaultState, defaultWindowId, MAX_HISTORY, State } from './reducer';
|
|
import { BaseItemEntity, FolderEntity, NoteEntity, TagEntity } from './services/database/types';
|
|
import Note from './models/Note';
|
|
import BaseModel from './BaseModel';
|
|
import Folder from './models/Folder';
|
|
import ItemChange from './models/ItemChange';
|
|
import getConflictFolderId from './models/utils/getConflictFolderId';
|
|
import getTrashFolderId from './services/trash/getTrashFolderId';
|
|
import { ALL_NOTES_FILTER_ID } from './reserved-ids';
|
|
|
|
function initTestState(folders: FolderEntity[], selectedFolderIndex: number, notes: NoteEntity[], selectedNoteIndexes: number[], tags: TagEntity[] = null, selectedTagIndex: number = null) {
|
|
let state = defaultState;
|
|
|
|
if (selectedFolderIndex !== null) {
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[selectedFolderIndex].id });
|
|
}
|
|
if (folders !== null) {
|
|
state = reducer(state, { type: 'FOLDER_UPDATE_ALL', items: folders });
|
|
}
|
|
if (notes !== null) {
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, noteSource: 'test' });
|
|
}
|
|
if (tags !== null) {
|
|
state = reducer(state, { type: 'TAG_UPDATE_ALL', items: tags });
|
|
}
|
|
if (selectedTagIndex !== null) {
|
|
state = reducer(state, { type: 'TAG_SELECT', id: tags[selectedTagIndex].id });
|
|
}
|
|
// Select notes last: Selecting a tag or folder can clear the note selection
|
|
if (selectedNoteIndexes !== null) {
|
|
const selectedIds = [];
|
|
for (let i = 0; i < selectedNoteIndexes.length; i++) {
|
|
selectedIds.push(notes[selectedNoteIndexes[i]].id);
|
|
}
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: selectedIds });
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
function goToNote(notes: NoteEntity[], selectedNoteIndexes: number[], state: State) {
|
|
if (selectedNoteIndexes !== null) {
|
|
const selectedIds = [];
|
|
for (let i = 0; i < selectedNoteIndexes.length; i++) {
|
|
selectedIds.push(notes[selectedNoteIndexes[i]].id);
|
|
}
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: selectedIds });
|
|
}
|
|
return state;
|
|
}
|
|
|
|
function goBackWard(state: State) {
|
|
if (!state.backwardHistoryNotes.length) return state;
|
|
state = reducer(state, {
|
|
type: 'HISTORY_BACKWARD',
|
|
});
|
|
return state;
|
|
}
|
|
|
|
function goForward(state: State) {
|
|
if (!state.forwardHistoryNotes.length) return state;
|
|
state = reducer(state, {
|
|
type: 'HISTORY_FORWARD',
|
|
});
|
|
return state;
|
|
}
|
|
|
|
function createExpectedState(items: BaseItemEntity[], keepIndexes: number[], selectedIndexes: number[]) {
|
|
const expected = { items: [] as BaseItemEntity[], selectedIds: [] as string[] };
|
|
|
|
for (let i = 0; i < selectedIndexes.length; i++) {
|
|
expected.selectedIds.push(items[selectedIndexes[i]].id);
|
|
}
|
|
for (let i = 0; i < keepIndexes.length; i++) {
|
|
expected.items.push(items[keepIndexes[i]]);
|
|
}
|
|
return expected;
|
|
}
|
|
|
|
const createBackgroundWindow = (state: State, windowId: string, selectedNote: NoteEntity, notes: NoteEntity[]) => {
|
|
state = reducer(state, {
|
|
type: 'WINDOW_OPEN',
|
|
windowId,
|
|
folderId: selectedNote.parent_id,
|
|
noteId: selectedNote.id,
|
|
});
|
|
const previousWindowId = state.windowId;
|
|
|
|
state = reducer(state, {
|
|
type: 'WINDOW_FOCUS',
|
|
windowId,
|
|
});
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ALL',
|
|
notes: notes,
|
|
notesSource: 'test',
|
|
});
|
|
state = reducer(state, {
|
|
type: 'WINDOW_FOCUS',
|
|
windowId: previousWindowId,
|
|
});
|
|
|
|
return state;
|
|
};
|
|
|
|
function getIds(items: BaseItemEntity[], indexes: number[]|null = null) {
|
|
const ids = [];
|
|
for (let i = 0; i < items.length; i++) {
|
|
if (!indexes || i in indexes) {
|
|
ids.push(items[i].id);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
describe('reducer', () => {
|
|
|
|
beforeEach(async () => {
|
|
await setupDatabaseAndSynchronizer(1);
|
|
await switchClient(1);
|
|
});
|
|
|
|
// tests for NOTE_DELETE
|
|
it('should delete selected note', (async () => {
|
|
// create 1 folder
|
|
const folders = await createNTestFolders(1);
|
|
// create 5 notes
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
// select the 1st folder and the 3rd note
|
|
let state = initTestState(folders, 0, notes, [2]);
|
|
|
|
// test action
|
|
// delete the third note
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
|
|
|
// expect that the third note is missing, and the 4th note is now selected
|
|
const expected = createExpectedState(notes, [0, 1, 3, 4], [3]);
|
|
|
|
// check the ids of all the remaining notes
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
// check the ids of the selected notes
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete selected note at top', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [1]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
|
|
|
const expected = createExpectedState(notes, [1, 2, 3, 4], [1]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete last remaining note', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
|
|
|
const expected = createExpectedState(notes, [], []);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete selected note at bottom', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [4]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
|
|
|
const expected = createExpectedState(notes, [0, 1, 2, 3], [3]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete note when a note below is selected', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [3]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
|
|
|
const expected = createExpectedState(notes, [0, 2, 3, 4], [3]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete note when a note above is selected', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [1]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
|
|
|
const expected = createExpectedState(notes, [0, 1, 2, 4], [1]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete selected notes', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [1, 2]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
|
|
|
const expected = createExpectedState(notes, [0, 3, 4], [3]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete note when a notes below it are selected', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [3, 4]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
|
|
|
|
const expected = createExpectedState(notes, [0, 2, 3, 4], [3, 4]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete note when a notes above it are selected', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [1, 2]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
|
|
|
const expected = createExpectedState(notes, [0, 1, 2, 4], [1, 2]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete notes at end', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [3, 4]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
|
|
|
const expected = createExpectedState(notes, [0, 1, 2], [2]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete notes when non-contiguous selection', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [0, 2, 4]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
|
|
|
|
const expected = createExpectedState(notes, [1, 3], [1]);
|
|
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should delete notes from background window states', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [0, 2, 4]);
|
|
|
|
const backgroundWindowId = 'window1';
|
|
state = createBackgroundWindow(state, backgroundWindowId, notes[2], notes);
|
|
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
|
|
|
|
let expected = createExpectedState(notes, [1, 3, 4], [1]);
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
|
|
state = reducer(state, {
|
|
type: 'WINDOW_FOCUS',
|
|
windowId: backgroundWindowId,
|
|
});
|
|
expected = createExpectedState(notes, [1, 3, 4], [3]);
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
// tests for FOLDER_DELETE
|
|
it('should delete selected notebook', (async () => {
|
|
const folders = await createNTestFolders(5);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 2, notes, [2]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
|
|
|
const expected = createExpectedState(folders, [0, 1, 3, 4], [3]);
|
|
|
|
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
|
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
|
}));
|
|
|
|
it('should delete notebook when a book above is selected', (async () => {
|
|
const folders = await createNTestFolders(5);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 1, notes, [2]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
|
|
|
const expected = createExpectedState(folders, [0, 1, 3, 4], [1]);
|
|
|
|
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
|
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
|
}));
|
|
|
|
it('should delete notebook when a book below is selected', (async () => {
|
|
const folders = await createNTestFolders(5);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 4, notes, [2]);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
|
|
|
|
const expected = createExpectedState(folders, [0, 1, 3, 4], [4]);
|
|
|
|
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
|
expect(state.selectedFolderId).toEqual(expected.selectedIds[0]);
|
|
}));
|
|
|
|
// tests for TAG_DELETE
|
|
it('should delete selected tag', (async () => {
|
|
const tags = await createNTestTags(5);
|
|
let state = initTestState(null, null, null, null, tags, 2);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'TAG_DELETE', id: tags[2].id });
|
|
|
|
const expected = createExpectedState(tags, [0, 1, 3, 4], [3]);
|
|
|
|
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
|
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
|
}));
|
|
|
|
it('should delete tag when a tag above is selected', (async () => {
|
|
const tags = await createNTestTags(5);
|
|
let state = initTestState(null, null, null, null, tags, 2);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'TAG_DELETE', id: tags[4].id });
|
|
|
|
const expected = createExpectedState(tags, [0, 1, 2, 3], [2]);
|
|
|
|
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
|
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
|
}));
|
|
|
|
it('should delete tag when a tag below is selected', (async () => {
|
|
const tags = await createNTestTags(5);
|
|
let state = initTestState(null, null, null, null, tags, 2);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'TAG_DELETE', id: tags[0].id });
|
|
|
|
const expected = createExpectedState(tags, [1, 2, 3, 4], [2]);
|
|
|
|
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
|
expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
|
|
}));
|
|
|
|
it('should remove note from list when tag is removed', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(3, folders[0]);
|
|
const tags = await createNTestTags(1);
|
|
|
|
// Current view is the tag we're about to remove from a note
|
|
let state = initTestState(folders, 0, notes, [0], tags, 0);
|
|
|
|
expect(state.notes.length).toBe(3);
|
|
expect(state.notesParentType).toBe('Tag');
|
|
expect(state.selectedTagId).toBe(tags[0].id);
|
|
|
|
// Remove tag from the first note
|
|
state = reducer(state, { type: 'NOTE_TAG_REMOVE', item: tags[0], noteId: notes[0].id });
|
|
|
|
// Expect the note to be removed from state.notes
|
|
expect(state.notes.length).toBe(2);
|
|
expect(state.notes.map(n => n.id)).not.toContain(notes[0].id);
|
|
}));
|
|
|
|
it.each([false, true])('should select multiple folders (extend:%j)', async (extendSelection) => {
|
|
const folders = await createNTestFolders(3);
|
|
let state = initTestState(folders, 0, [], []);
|
|
|
|
if (extendSelection) {
|
|
state = reducer(state, { type: 'FOLDER_SELECT_ADD', id: folders[1].id });
|
|
} else {
|
|
state = reducer(state, { type: 'FOLDER_SELECT', ids: [folders[0].id, folders[1].id] });
|
|
}
|
|
|
|
const expected = createExpectedState(folders, [0, 1, 2], [0, 1]);
|
|
|
|
expect(getIds(state.folders)).toEqual(getIds(expected.items));
|
|
expect(state.selectedFolderIds).toEqual(expected.selectedIds);
|
|
// Should match the last-added item
|
|
expect(state.selectedFolderId).toBe(expected.selectedIds[expected.selectedIds.length - 1]);
|
|
});
|
|
|
|
it.each([false, true])('should select multiple tags (extend:%j)', async (extendSelection) => {
|
|
const tags = await createNTestTags(3);
|
|
let state = initTestState([], null, [], [], tags, 0);
|
|
|
|
if (extendSelection) {
|
|
state = reducer(state, { type: 'TAG_SELECT_ADD', id: tags[2].id });
|
|
} else {
|
|
state = reducer(state, { type: 'TAG_SELECT', ids: [tags[0].id, tags[2].id] });
|
|
}
|
|
|
|
const expected = createExpectedState(tags, [0, 1, 2], [0, 2]);
|
|
|
|
expect(getIds(state.tags)).toEqual(getIds(expected.items));
|
|
expect(state.selectedTagIds).toEqual(expected.selectedIds);
|
|
expect(state.selectedTagId).toBe(expected.selectedIds[expected.selectedIds.length - 1]);
|
|
});
|
|
|
|
it('should not clear the selected note IDs when adding folders to the selection', async () => {
|
|
const folders = await createNTestFolders(3);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const expectedSelection = createExpectedState(notes, [0], [0]).selectedIds;
|
|
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
expect(state.selectedNoteIds).toEqual(expectedSelection);
|
|
|
|
state = reducer(state, { type: 'FOLDER_SELECT_ADD', id: folders[1].id });
|
|
|
|
expect(state.selectedNoteIds).toEqual(expectedSelection);
|
|
});
|
|
|
|
it('should clear the selected note IDs if adding a folder to the selection changes the selection type', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const tags = await createNTestTags(1);
|
|
const expectedSelection = createExpectedState(notes, [0], [0]).selectedIds;
|
|
|
|
let state = initTestState(folders, 0, notes, [0], tags, 0);
|
|
|
|
expect(state.selectedNoteIds).toEqual(expectedSelection);
|
|
expect(state.notesParentType).toBe('Tag');
|
|
|
|
state = reducer(state, { type: 'FOLDER_SELECT_ADD', id: folders[0].id });
|
|
|
|
expect(state.notesParentType).toBe('Folder');
|
|
expect(state.selectedNoteIds).toEqual([]);
|
|
});
|
|
|
|
it('should add and remove a folders from the selection', async () => {
|
|
const folders = await createNTestFolders(3);
|
|
let state = initTestState(folders, 0, [], []);
|
|
|
|
state = reducer(state, { type: 'FOLDER_SELECT_ADD', ids: [folders[1].id, folders[2].id] });
|
|
expect(state.selectedFolderIds).toEqual([folders[0].id, folders[1].id, folders[2].id]);
|
|
expect(state.selectedFolderId).toBe(folders[2].id);
|
|
|
|
state = reducer(state, { type: 'FOLDER_SELECT_REMOVE', id: folders[0].id });
|
|
expect(state.selectedFolderIds).toEqual([folders[1].id, folders[2].id]);
|
|
expect(state.selectedFolderId).toBe(folders[2].id);
|
|
});
|
|
|
|
it('should select all notes', (async () => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = [];
|
|
for (let i = 0; i < folders.length; i++) {
|
|
notes.push(...await createNTestNotes(3, folders[i]));
|
|
}
|
|
|
|
let state = initTestState(folders, 0, notes.slice(0, 3), [0]);
|
|
|
|
let expected = createExpectedState(notes, [0, 1, 2], [0]);
|
|
|
|
expect(state.notes.length).toEqual(expected.items.length);
|
|
expect(getIds(state.notes.slice(0, 4))).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
|
|
// test action
|
|
state = reducer(state, { type: 'NOTE_SELECT_ALL' });
|
|
|
|
expected = createExpectedState(notes.slice(0, 3), [0, 1, 2], [0, 1, 2]);
|
|
expect(getIds(state.notes)).toEqual(getIds(expected.items));
|
|
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
|
|
}));
|
|
|
|
it('should remove deleted note from history', (async () => {
|
|
|
|
// create 1 folder
|
|
const folders = await createNTestFolders(1);
|
|
// create 5 notes
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
// select the 1st folder and the 1st note
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
|
|
// select second note
|
|
state = goToNote(notes, [1], state);
|
|
// select third note
|
|
state = goToNote(notes, [2], state);
|
|
// select fourth note
|
|
state = goToNote(notes, [3], state);
|
|
|
|
// expect history to contain first, second and third note
|
|
expect(state.backwardHistoryNotes.length).toEqual(3);
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 3)));
|
|
|
|
// delete third note
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
|
|
|
// expect history to not contain third note
|
|
expect(getIds(state.backwardHistoryNotes)).not.toContain(notes[2].id);
|
|
}));
|
|
|
|
it('should remove deleted note from history in background window', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
|
|
const windowId1 = 'window1';
|
|
state = createBackgroundWindow(state, windowId1, notes[0], notes);
|
|
|
|
state = goToNote(notes, [1], state);
|
|
state = goToNote(notes, [2], state);
|
|
state = goToNote(notes, [3], state);
|
|
state = goToNote(notes, [4], state);
|
|
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual([notes[0].id, notes[1].id, notes[2].id, notes[3].id]);
|
|
|
|
// Remove a note in another window
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: windowId1 });
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: defaultWindowId });
|
|
|
|
// should have removed the note from history in the unfocused window
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual([notes[0].id, notes[1].id, notes[3].id]);
|
|
});
|
|
|
|
it('should remove all notes of a deleted notebook from history', (async () => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = [];
|
|
for (let i = 0; i < folders.length; i++) {
|
|
notes.push(...await createNTestNotes(3, folders[i]));
|
|
}
|
|
|
|
let state = initTestState(folders, 0, notes.slice(0, 3), [0]);
|
|
state = goToNote(notes, [1], state);
|
|
state = goToNote(notes, [2], state);
|
|
|
|
|
|
// go to second folder
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[1].id });
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 3)));
|
|
|
|
// delete the first folder
|
|
state = reducer(state, { type: 'FOLDER_DELETE', id: folders[0].id });
|
|
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual([]);
|
|
}));
|
|
|
|
it('should maintain history correctly when going backward and forward', (async () => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = [];
|
|
for (let i = 0; i < folders.length; i++) {
|
|
notes.push(...await createNTestNotes(5, folders[i]));
|
|
}
|
|
|
|
let state = initTestState(folders, 0, notes.slice(0, 5), [0]);
|
|
state = goToNote(notes, [1], state);
|
|
state = goToNote(notes, [2], state);
|
|
state = goToNote(notes, [3], state);
|
|
state = goToNote(notes, [4], state);
|
|
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 4)));
|
|
|
|
state = goBackWard(state);
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 3)));
|
|
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds(notes.slice(4, 5)));
|
|
|
|
state = goBackWard(state);
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 2)));
|
|
// because we push the last seen note to stack.
|
|
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds([notes[4], notes[3]]));
|
|
|
|
state = goForward(state);
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 3)));
|
|
expect(getIds(state.forwardHistoryNotes)).toEqual(getIds([notes[4]]));
|
|
|
|
state = goForward(state);
|
|
expect(getIds(state.backwardHistoryNotes)).toEqual(getIds(notes.slice(0, 4)));
|
|
expect(getIds(state.forwardHistoryNotes)).toEqual([]);
|
|
}));
|
|
|
|
it('should remember the last seen note of a notebook', (async () => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = [];
|
|
for (let i = 0; i < folders.length; i++) {
|
|
notes.push(...await createNTestNotes(5, folders[i]));
|
|
}
|
|
|
|
let state = initTestState(folders, 0, notes.slice(0, 5), [0]);
|
|
|
|
state = goToNote(notes, [1], state);
|
|
state = goToNote(notes, [2], state);
|
|
state = goToNote(notes, [3], state);
|
|
state = goToNote(notes, [4], state); // last seen note is notes[4]
|
|
// go to second folder
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[1].id });
|
|
state = goToNote(notes, [5], state);
|
|
state = goToNote(notes, [6], state);
|
|
|
|
// return to first folder
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[0].id });
|
|
|
|
expect(state.lastSelectedNotesIds.Folder[folders[0].id]).toEqual([notes[4].id]);
|
|
|
|
// return to second folder
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[1].id });
|
|
expect(state.lastSelectedNotesIds.Folder[folders[1].id]).toEqual([notes[6].id]);
|
|
|
|
}));
|
|
|
|
it('should ensure that history is free of adjacent duplicates', (async () => {
|
|
// create 1 folder
|
|
const folders = await createNTestFolders(1);
|
|
// create 5 notes
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
// select the 1st folder and the 1st note
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
|
|
// backward = 0 1 2 3 2 3 2 3 2 3 2
|
|
// forward =
|
|
// current = 3
|
|
state = goToNote(notes, [1], state);
|
|
state = goToNote(notes, [2], state);
|
|
state = goToNote(notes, [3], state);
|
|
state = goToNote(notes, [2], state);
|
|
state = goToNote(notes, [3], state);
|
|
state = goToNote(notes, [2], state);
|
|
state = goToNote(notes, [3], state);
|
|
state = goToNote(notes, [2], state);
|
|
state = goToNote(notes, [3], state);
|
|
state = goToNote(notes, [2], state);
|
|
state = goToNote(notes, [3], state);
|
|
|
|
// backward = 0 1 2 3 2 3 2 3 2 3
|
|
// forward = 3
|
|
// current = 2
|
|
state = goBackWard(state);
|
|
|
|
// backward = 0 1 2 3 2 3 2 3 2
|
|
// forward = 3 2
|
|
// current = 3
|
|
state = goBackWard(state);
|
|
|
|
// backward = 0 1 2 3 2 3 2 3
|
|
// forward = 3 2 3
|
|
// current = 2
|
|
state = goBackWard(state);
|
|
|
|
// backward = 0 1 2 3 2 3 2
|
|
// forward = 3 2 3 2
|
|
// current = 3
|
|
state = goBackWard(state);
|
|
|
|
expect(state.backwardHistoryNotes.map(n=>n.id)).toEqual([notes[0], notes[1], notes[2], notes[3], notes[2], notes[3], notes[2]].map(n=>n.id));
|
|
expect(state.forwardHistoryNotes.map(n=>n.id)).toEqual([notes[3], notes[2], notes[3], notes[2]].map(n=>n.id));
|
|
expect(state.selectedNoteIds).toEqual([notes[3].id]);
|
|
|
|
// delete third note
|
|
state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
|
|
|
|
// if adjacent duplicates not removed
|
|
// backward = 0 1 3 3
|
|
// forward = 3 3
|
|
// current = 3
|
|
|
|
// if adjacent duplicates are removed
|
|
// backward = 0 1 3
|
|
// forward = 3
|
|
// current = 3
|
|
|
|
// Expected: adjacent duplicates are removed and latest history does not contain current note
|
|
// backward = 0 1
|
|
// forward =
|
|
// current = 3
|
|
expect(state.backwardHistoryNotes.map(x => x.id)).toEqual([notes[0].id, notes[1].id]);
|
|
expect(state.forwardHistoryNotes.map(x => x.id)).toEqual([]);
|
|
expect(state.selectedNoteIds).toEqual([notes[3].id]);
|
|
}));
|
|
|
|
it('should ensure history max limit is maintained', (async () => {
|
|
const folders = await createNTestFolders(1);
|
|
// create 5 notes
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
// select the 1st folder and the 1st note
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
|
|
for (let i = 0; i < 2 * MAX_HISTORY; i++) {
|
|
state = goToNote(notes, [i % 5], state);
|
|
}
|
|
|
|
expect(state.backwardHistoryNotes.length).toEqual(MAX_HISTORY);
|
|
expect(state.forwardHistoryNotes.map(x => x.id)).toEqual([]);
|
|
|
|
for (let i = 0; i < 2 * MAX_HISTORY; i++) {
|
|
state = goBackWard(state);
|
|
}
|
|
|
|
expect(state.backwardHistoryNotes).toEqual([]);
|
|
expect(state.forwardHistoryNotes.length).toEqual(MAX_HISTORY);
|
|
|
|
for (let i = 0; i < 2 * MAX_HISTORY; i++) {
|
|
state = goForward(state);
|
|
}
|
|
|
|
expect(state.backwardHistoryNotes.length).toEqual(MAX_HISTORY);
|
|
expect(state.forwardHistoryNotes.map(x => x.id)).toEqual([]);
|
|
}));
|
|
|
|
// it('should not change folders when all notes filter is on', async () => {
|
|
// const folders = await createNTestFolders(2);
|
|
// const notes = [];
|
|
// for (let i = 0; i < folders.length; i++) {
|
|
// notes.push(...await createNTestNotes(1, folders[i]));
|
|
// }
|
|
// // initialize state with no folders selected
|
|
// let state = initTestState(folders, null, notes.slice(0,2), null);
|
|
|
|
// // turn on 'All Notes' filter
|
|
// state = reducer(state, { type: 'SMART_FILTER_SELECT', id: ALL_NOTES_FILTER_ID });
|
|
|
|
// // change folder
|
|
// state = reducer(state, { type: 'FOLDER_AND_NOTE_SELECT', folderId: folders[1].id, noteId: notes[1].id });
|
|
|
|
// expect(state.selectedFolderId).toEqual(null);
|
|
// expect(state.selectedNoteIds[0]).toEqual(notes[1].id);
|
|
// });
|
|
|
|
// tests for NOTE_UPDATE_ALL about issue #5447
|
|
it('should not change selectedNoteIds object when selections are not changed', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
{
|
|
// Case 1. Selected notes are changed when one of selected notes is deleted.
|
|
let state = initTestState(folders, 0, notes, [0, 2, 4]);
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes.slice(0, 4), notesSource: 'test' });
|
|
const expected = [notes[0].id, notes[2].id].sort();
|
|
expect([...state.selectedNoteIds].sort()).toEqual(expected);
|
|
}
|
|
{
|
|
// Case 2. Selected notes and object identity are unchanged when notes are not changed.
|
|
let state = initTestState(folders, 0, notes, [0, 2, 4]);
|
|
const expected = state.selectedNoteIds;
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, notesSource: 'test' });
|
|
// Object identity is checked. Don't use toEqual() or toStrictEqual() here.
|
|
expect(state.selectedNoteIds).toBe(expected);
|
|
}
|
|
});
|
|
|
|
// tests for TAG_UPDATE_ALL about PR #6451
|
|
it('should not change tags when a new value is deep equal to the old value', async () => {
|
|
const tags = await createNTestTags(6);
|
|
const oldTags = tags.slice(0, 5);
|
|
{
|
|
// Case 1. The input which is deep equal to the current state.tags doesn't change state.tags.
|
|
const oldState = initTestState(null, null, null, null, oldTags, 2);
|
|
const newTags = oldTags.slice();
|
|
// test action
|
|
const newState = reducer(oldState, { type: 'TAG_UPDATE_ALL', items: newTags });
|
|
expect(newState.tags).toBe(oldState.tags);
|
|
}
|
|
{
|
|
// Case 2. A different input changes state.tags.
|
|
const oldState = initTestState(null, null, null, null, oldTags, 2);
|
|
const newTags = oldTags.slice().splice(3, 1, tags[5]);
|
|
// test action
|
|
const newState = reducer(oldState, { type: 'TAG_UPDATE_ALL', items: newTags });
|
|
expect(newState.tags).not.toBe(oldState.tags);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
[true, false],
|
|
[undefined, false],
|
|
[undefined, true],
|
|
[false, true],
|
|
])('should preserve note selection if specified with an option (preserveSelection: %j, allowSelectionInOtherFolders: %j)', async (
|
|
preserveSelectionOption, allowSelectionInOtherFolders,
|
|
) => {
|
|
const folders = await createNTestFolders(3);
|
|
const notes = await createNTestNotes(5, folders[0]);
|
|
|
|
// select the 1st folder and the 1st note
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = { ...state, allowSelectionInOtherFolders };
|
|
state = goToNote(notes, [0], state);
|
|
|
|
expect(state.selectedNoteIds).toHaveLength(1);
|
|
|
|
BaseModel.dispatch = jest.fn((action: unknown) => {
|
|
state = reducer(state, action);
|
|
});
|
|
|
|
// Dispatching with preserveSelection should preserve the selected note (as is done on
|
|
// mobile).
|
|
await Note.moveToFolder(
|
|
state.selectedNoteIds[0],
|
|
folders[1].id,
|
|
{ dispatchOptions: { preserveSelection: preserveSelectionOption } },
|
|
);
|
|
|
|
expect(BaseModel.dispatch).toHaveBeenCalled();
|
|
|
|
// preserveSelectionOption takes precedence over allowSelectionInOtherFolders
|
|
const shouldPreserveSelection = preserveSelectionOption ?? allowSelectionInOtherFolders;
|
|
|
|
if (shouldPreserveSelection) {
|
|
expect(state.selectedNoteIds).toMatchObject([notes[0].id]);
|
|
} else {
|
|
expect(state.selectedNoteIds).toMatchObject([notes[1].id]);
|
|
}
|
|
// Original note should no longer be present in the sidebar
|
|
expect(state.notes.every(n => n.id !== notes[0].id)).toBe(true);
|
|
expect(state.selectedFolderId).toBe(folders[0].id);
|
|
});
|
|
|
|
test('when selection is allowed in unselected folders, NOTE_UPDATE_ALL should not remove items from the selection', async () => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = await createNTestNotes(2, folders[0]);
|
|
|
|
// select the 1st folder and the 1st note
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = { ...state, allowSelectionInOtherFolders: true };
|
|
state = goToNote(notes, [0], state);
|
|
|
|
expect(state.selectedNoteIds).toEqual([notes[0].id]);
|
|
expect(state.notes).toHaveLength(2);
|
|
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ALL',
|
|
notes: [],
|
|
});
|
|
|
|
expect(state.notes).toHaveLength(0);
|
|
expect(state.selectedNoteIds).toEqual([notes[0].id]);
|
|
});
|
|
|
|
// window tests
|
|
test('switching windows should move state from background to the foreground', async () => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes1 = await createNTestNotes(3, folders[0]);
|
|
const noteIds1 = getIds(notes1);
|
|
const notes2 = await createNTestNotes(4, folders[1]);
|
|
const noteIds2 = getIds(notes2);
|
|
let state = initTestState(folders, 0, notes1, [0]);
|
|
|
|
const window1Id = 'window1';
|
|
const window2Id = 'window2';
|
|
state = createBackgroundWindow(state, window1Id, notes1[2], notes1);
|
|
state = createBackgroundWindow(state, window2Id, notes2[1], notes2);
|
|
|
|
const checkCurrentState = (windowId: string) => {
|
|
expect(state.windowId).toBe(windowId);
|
|
expect(getIds(state.notes)).toEqual(windowId === window2Id ? noteIds2 : noteIds1);
|
|
expect(state.selectedFolderId).toBe(windowId === window2Id ? folders[1].id : folders[0].id);
|
|
|
|
let expectedSelectedNoteIds = [notes1[0].id];
|
|
if (windowId === window2Id) {
|
|
expectedSelectedNoteIds = [notes2[1].id];
|
|
} else if (windowId === window1Id) {
|
|
expectedSelectedNoteIds = [notes1[2].id];
|
|
}
|
|
expect(state.selectedNoteIds).toEqual(expectedSelectedNoteIds);
|
|
};
|
|
|
|
const navigationPattern = [
|
|
window1Id, window2Id, defaultWindowId, defaultWindowId, window2Id, window1Id, defaultWindowId,
|
|
];
|
|
|
|
for (const windowId of navigationPattern) {
|
|
state = reducer(state, {
|
|
type: 'WINDOW_FOCUS',
|
|
windowId,
|
|
});
|
|
checkCurrentState(windowId);
|
|
}
|
|
});
|
|
|
|
test('closing the focused window should set the focus to a different window', async () => {
|
|
const folder = await Folder.save({ title: 'Test' });
|
|
const notes = await createNTestNotes(3, folder);
|
|
let state = initTestState([folder], 0, notes, [0]);
|
|
|
|
const secondaryWindowId = 'window1';
|
|
state = createBackgroundWindow(state, secondaryWindowId, notes[2], notes);
|
|
|
|
state = reducer(state, {
|
|
type: 'WINDOW_FOCUS',
|
|
windowId: secondaryWindowId,
|
|
});
|
|
|
|
expect(state.windowId).toBe(secondaryWindowId);
|
|
expect(state.selectedNoteIds).toEqual([notes[2].id]);
|
|
|
|
|
|
// Closing the focused window should set focus to the other window
|
|
state = reducer(state, {
|
|
type: 'WINDOW_CLOSE',
|
|
windowId: secondaryWindowId,
|
|
});
|
|
|
|
// There should be no background windows
|
|
expect(state.backgroundWindows).toEqual({});
|
|
|
|
// The other window should be focused
|
|
expect(state.windowId).toBe(defaultWindowId);
|
|
expect(state.selectedNoteIds).toEqual([notes[0].id]);
|
|
});
|
|
|
|
it.each([
|
|
undefined,
|
|
false,
|
|
])('should not change selected note in background window when active window note moves folders (preserveSelection: %j)', async (
|
|
preserveSelectionOption,
|
|
) => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = await createNTestNotes(3, folders[0]);
|
|
|
|
// select the 1st folder and the 1st note in the primary window
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
|
|
// open note[2] in a background (secondary) window
|
|
const secondaryWindowId = 'window1';
|
|
state = createBackgroundWindow(state, secondaryWindowId, notes[2], notes);
|
|
|
|
// background window should be on notes[2]
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedNoteIds).toEqual([notes[2].id]);
|
|
|
|
BaseModel.dispatch = jest.fn((action: unknown) => {
|
|
state = reducer(state, action);
|
|
});
|
|
|
|
// move notes[0] (selected in primary window) to a different folder
|
|
await Note.moveToFolder(
|
|
state.selectedNoteIds[0],
|
|
folders[1].id,
|
|
{ dispatchOptions: { preserveSelection: preserveSelectionOption } },
|
|
);
|
|
|
|
expect(BaseModel.dispatch).toHaveBeenCalled();
|
|
|
|
// primary window should have switched away from the moved note
|
|
expect(state.notes.every(n => n.id !== notes[0].id)).toBe(true);
|
|
|
|
// background window should still be on notes[2], not have jumped to whatever
|
|
// the primary window selected next
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedNoteIds).toEqual([notes[2].id]);
|
|
});
|
|
|
|
it.each([
|
|
['sync moving an unselected note should not change the selection', 1, 0],
|
|
['sync moving the selected note should select the next note', 0, 1],
|
|
])('%s', async (_, movedNoteIndex, expectedSelectedNoteIndex) => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = await createNTestNotes(3, folders[0]);
|
|
|
|
// Select note[0]
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = goToNote(notes, [0], state);
|
|
|
|
const movedNote = {
|
|
...notes[movedNoteIndex],
|
|
parent_id: folders[1].id,
|
|
};
|
|
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: movedNote,
|
|
changeSource: ItemChange.SOURCE_SYNC,
|
|
});
|
|
|
|
// Moved note should either remain selected or change
|
|
expect(state.selectedNoteIds).toEqual([notes[expectedSelectedNoteIndex].id]);
|
|
|
|
// Moved note should no longer be visible
|
|
expect(state.notes.every(n => n.id !== notes[movedNoteIndex].id)).toBe(true);
|
|
});
|
|
|
|
test('conflict notes should only be added to the Conflicts note list', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const conflictNote = {
|
|
...notes[0],
|
|
id: '12345678901234567890123456789012',
|
|
is_conflict: 1,
|
|
};
|
|
|
|
let folderState = initTestState(folders, 0, notes, [0]);
|
|
folderState = reducer(folderState, { type: 'NOTE_UPDATE_ONE', note: conflictNote });
|
|
expect(folderState.notes.map(note => note.id)).toEqual([notes[0].id]);
|
|
|
|
let allNotesState = initTestState(folders, null, notes, [0]);
|
|
allNotesState = reducer(allNotesState, { type: 'SMART_FILTER_SELECT', id: ALL_NOTES_FILTER_ID });
|
|
allNotesState = reducer(allNotesState, { type: 'NOTE_UPDATE_ONE', note: conflictNote });
|
|
expect(allNotesState.notes.map(note => note.id)).toEqual([notes[0].id]);
|
|
|
|
let conflictState = initTestState(folders, null, [], []);
|
|
conflictState = reducer(conflictState, { type: 'FOLDER_SELECT', id: getConflictFolderId() });
|
|
conflictState = reducer(conflictState, { type: 'NOTE_UPDATE_ONE', note: conflictNote });
|
|
expect(conflictState.notes.map(note => note.id)).toEqual([conflictNote.id]);
|
|
});
|
|
|
|
it.each([
|
|
['locally', ItemChange.SOURCE_UNSPECIFIED, 2, 1],
|
|
['during sync', ItemChange.SOURCE_SYNC, 0, 0],
|
|
])('moving the selected note %s should keep it open in a background window', async (_description, changeSource, primarySelectedIndex, expectedPrimarySelectedIndex) => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = await createNTestNotes(3, folders[0]);
|
|
|
|
let state = initTestState(folders, 0, notes, [primarySelectedIndex]);
|
|
|
|
// Background window selects note[2]
|
|
const secondaryWindowId = 'window1';
|
|
state = createBackgroundWindow(state, secondaryWindowId, notes[2], notes);
|
|
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedNoteIds).toEqual([notes[2].id]);
|
|
|
|
const movedNote = {
|
|
...notes[2],
|
|
parent_id: folders[1].id,
|
|
};
|
|
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: movedNote,
|
|
changeSource,
|
|
});
|
|
|
|
// The background window should retain both the selection and the note metadata used
|
|
// to determine whether editor toolbar commands are enabled.
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedNoteIds).toEqual([notes[2].id]);
|
|
expect(state.backgroundWindows[secondaryWindowId].notes).toEqual([movedNote]);
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedFolderId).toBe(folders[1].id);
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedFolderIds).toEqual([folders[1].id]);
|
|
expect(state.backgroundWindows[secondaryWindowId].notesSource).toBe('');
|
|
|
|
expect(state.selectedNoteIds).toEqual([notes[expectedPrimarySelectedIndex].id]);
|
|
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: secondaryWindowId });
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: [movedNote], notesSource: 'test' });
|
|
expect(state.selectedNoteIds).toEqual([movedNote.id]);
|
|
expect(state.notes).toContainEqual(movedNote);
|
|
});
|
|
|
|
test('moving one of multiple selected notes should not change a background window folder', async () => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = await createNTestNotes(3, folders[0]);
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, notes[2], notes);
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: secondaryWindowId });
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: [notes[1].id, notes[2].id] });
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: defaultWindowId });
|
|
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: { ...notes[2], parent_id: folders[1].id },
|
|
});
|
|
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedFolderId).toBe(folders[0].id);
|
|
expect(state.backgroundWindows[secondaryWindowId].notes.map(n => n.id)).toEqual([notes[0].id, notes[1].id]);
|
|
});
|
|
|
|
it.each([
|
|
['locally', ItemChange.SOURCE_UNSPECIFIED],
|
|
['during sync', ItemChange.SOURCE_SYNC],
|
|
])('moving a selected note without a parent %s should switch its background window to All Notes', async (_description, changeSource) => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, notes[0], notes);
|
|
|
|
const movedNote = { ...notes[0], parent_id: '' };
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ONE', note: movedNote, changeSource });
|
|
|
|
const secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.notesParentType).toBe('SmartFilter');
|
|
expect(secondaryWindow.selectedSmartFilterId).toBe(ALL_NOTES_FILTER_ID);
|
|
expect(secondaryWindow.selectedFolderId).toBeNull();
|
|
expect(secondaryWindow.selectedFolderIds).toEqual([]);
|
|
expect(secondaryWindow.notes).toEqual([movedNote]);
|
|
});
|
|
|
|
test('moving a selected note during sync should follow its folder ID even if the folder is not loaded', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, notes[0], notes);
|
|
|
|
const movedNote = { ...notes[0], parent_id: 'missing-folder' };
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: movedNote,
|
|
changeSource: ItemChange.SOURCE_SYNC,
|
|
});
|
|
|
|
const secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.notesParentType).toBe('Folder');
|
|
expect(secondaryWindow.selectedFolderId).toBe(movedNote.parent_id);
|
|
expect(secondaryWindow.selectedFolderIds).toEqual([movedNote.parent_id]);
|
|
expect(secondaryWindow.selectedSmartFilterId).toBeNull();
|
|
expect(secondaryWindow.notes).toEqual([movedNote]);
|
|
expect(secondaryWindow.selectedNoteIds).toEqual([movedNote.id]);
|
|
expect(secondaryWindow.notesSource).toBe('');
|
|
});
|
|
|
|
it.each([
|
|
['locally', ItemChange.SOURCE_UNSPECIFIED, false],
|
|
['during sync', ItemChange.SOURCE_SYNC, false],
|
|
['during encrypted sync', ItemChange.SOURCE_SYNC, true],
|
|
])('resolving a conflict %s should keep it open in its original folder in a background window', async (_description, changeSource, encrypted) => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const conflictNote = { ...notes[0], is_conflict: 1 };
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, conflictNote, [conflictNote]);
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: secondaryWindowId });
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: getConflictFolderId() });
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: [conflictNote], notesSource: 'test' });
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: [conflictNote.id] });
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: defaultWindowId });
|
|
|
|
let resolvedNote = { ...conflictNote, is_conflict: 0, encryption_applied: encrypted ? 1 : 0 };
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ONE', note: resolvedNote, changeSource });
|
|
if (encrypted) {
|
|
resolvedNote = { ...resolvedNote, encryption_applied: 0 };
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ONE', note: resolvedNote, changeSource: ItemChange.SOURCE_DECRYPTION });
|
|
}
|
|
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedFolderId).toBe(folders[0].id);
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedFolderIds).toEqual([folders[0].id]);
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedNoteIds).toEqual([resolvedNote.id]);
|
|
expect(state.backgroundWindows[secondaryWindowId].notes).toEqual([resolvedNote]);
|
|
});
|
|
|
|
test('moving a conflict to another folder should keep it open in its background window', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const conflictNote = { ...notes[0], is_conflict: 1 };
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, conflictNote, [conflictNote]);
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: secondaryWindowId });
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: getConflictFolderId() });
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: [conflictNote], notesSource: 'test' });
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: [conflictNote.id] });
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: defaultWindowId });
|
|
|
|
const movedNote = { ...conflictNote, is_conflict: 0 };
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ONE', note: movedNote, noteMovedToFolder: true });
|
|
|
|
const secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.selectedFolderId).toBe(folders[0].id);
|
|
expect(secondaryWindow.selectedFolderIds).toEqual([folders[0].id]);
|
|
expect(secondaryWindow.notes).toContainEqual(movedNote);
|
|
});
|
|
|
|
it.each([
|
|
['decrypted', false],
|
|
['encrypted', true],
|
|
])('sync moving a conflict to another folder (%s) should keep its background editor enabled', async (_description, encrypted) => {
|
|
const folders = await createNTestFolders(2);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const conflictNote = { ...notes[0], is_conflict: 1 };
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, conflictNote, [conflictNote]);
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: secondaryWindowId });
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: getConflictFolderId() });
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: [conflictNote], notesSource: 'test' });
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: [conflictNote.id] });
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: defaultWindowId });
|
|
|
|
const movedNote = {
|
|
...conflictNote,
|
|
parent_id: folders[1].id,
|
|
is_conflict: 0,
|
|
encryption_applied: encrypted ? 1 : 0,
|
|
};
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: movedNote,
|
|
changeSource: ItemChange.SOURCE_SYNC,
|
|
});
|
|
|
|
let secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.selectedFolderId).toBe(folders[1].id);
|
|
expect(secondaryWindow.selectedNoteIds).toEqual([movedNote.id]);
|
|
expect(secondaryWindow.notes).toEqual([movedNote]);
|
|
|
|
if (encrypted) {
|
|
const decryptedMovedNote = { ...movedNote, encryption_applied: 0 };
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: decryptedMovedNote,
|
|
changeSource: ItemChange.SOURCE_DECRYPTION,
|
|
});
|
|
secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.selectedFolderId).toBe(folders[1].id);
|
|
expect(secondaryWindow.selectedNoteIds).toEqual([decryptedMovedNote.id]);
|
|
expect(secondaryWindow.notes).toEqual([decryptedMovedNote]);
|
|
}
|
|
});
|
|
|
|
test('trashing a selected note should keep it open in the trash in its background window', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(3, folders[0]);
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, notes[2], notes);
|
|
|
|
const trashedNote = { ...notes[2], deleted_time: Date.now() };
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: trashedNote,
|
|
});
|
|
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedFolderId).toBe(getTrashFolderId());
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedFolderIds).toEqual([getTrashFolderId()]);
|
|
expect(state.backgroundWindows[secondaryWindowId].selectedNoteIds).toEqual([trashedNote.id]);
|
|
expect(state.backgroundWindows[secondaryWindowId].notes).toEqual([trashedNote]);
|
|
});
|
|
|
|
test('updating a trashed note should not switch its background window out of the trash', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const trashedNote = { ...notes[0], deleted_time: Date.now() };
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, trashedNote, [trashedNote]);
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: secondaryWindowId });
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: getTrashFolderId() });
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: [trashedNote], notesSource: 'test' });
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: [trashedNote.id] });
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: defaultWindowId });
|
|
|
|
const updatedNote = { ...trashedNote, title: 'Updated title' };
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ONE', note: updatedNote });
|
|
|
|
const secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.selectedFolderId).toBe(getTrashFolderId());
|
|
expect(secondaryWindow.selectedFolderIds).toEqual([getTrashFolderId()]);
|
|
expect(secondaryWindow.notes).toContainEqual(updatedNote);
|
|
});
|
|
|
|
it.each([
|
|
['locally', ItemChange.SOURCE_UNSPECIFIED],
|
|
['during sync', ItemChange.SOURCE_SYNC],
|
|
])('restoring a deleted conflict %s should keep it open in Conflicts in its background window', async (_description, changeSource) => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const deletedConflict = { ...notes[0], is_conflict: 1, deleted_time: Date.now() };
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, deletedConflict, [deletedConflict]);
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: secondaryWindowId });
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: getTrashFolderId() });
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: [deletedConflict], notesSource: 'test' });
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: [deletedConflict.id] });
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: defaultWindowId });
|
|
|
|
const restoredConflict = { ...deletedConflict, deleted_time: 0 };
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ONE', note: restoredConflict, changeSource });
|
|
|
|
const secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.selectedFolderId).toBe(getConflictFolderId());
|
|
expect(secondaryWindow.selectedFolderIds).toEqual([getConflictFolderId()]);
|
|
expect(secondaryWindow.selectedNoteIds).toEqual([restoredConflict.id]);
|
|
expect(secondaryWindow.notes).toEqual([restoredConflict]);
|
|
});
|
|
|
|
test('restoring an encrypted deleted conflict during sync should keep its background editor enabled after decryption', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const deletedConflict = { ...notes[0], is_conflict: 1, deleted_time: Date.now() };
|
|
const secondaryWindowId = 'window1';
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, secondaryWindowId, deletedConflict, [deletedConflict]);
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: secondaryWindowId });
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: getTrashFolderId() });
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: [deletedConflict], notesSource: 'test' });
|
|
state = reducer(state, { type: 'NOTE_SELECT', ids: [deletedConflict.id] });
|
|
state = reducer(state, { type: 'WINDOW_FOCUS', windowId: defaultWindowId });
|
|
|
|
const encryptedRestoredConflict = {
|
|
...deletedConflict,
|
|
deleted_time: 0,
|
|
encryption_applied: 1,
|
|
// The encrypted sync representation does not expose is_conflict.
|
|
is_conflict: 0,
|
|
};
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: encryptedRestoredConflict,
|
|
changeSource: ItemChange.SOURCE_SYNC,
|
|
});
|
|
|
|
let secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.selectedFolderId).toBe(getConflictFolderId());
|
|
expect(secondaryWindow.selectedNoteIds).toEqual([deletedConflict.id]);
|
|
expect(secondaryWindow.notes).toEqual([encryptedRestoredConflict]);
|
|
|
|
const decryptedRestoredConflict = { ...encryptedRestoredConflict, encryption_applied: 0, is_conflict: 1 };
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: decryptedRestoredConflict,
|
|
changeSource: ItemChange.SOURCE_DECRYPTION,
|
|
});
|
|
|
|
secondaryWindow = state.backgroundWindows[secondaryWindowId];
|
|
expect(secondaryWindow.selectedFolderId).toBe(getConflictFolderId());
|
|
expect(secondaryWindow.selectedNoteIds).toEqual([decryptedRestoredConflict.id]);
|
|
expect(secondaryWindow.notes).toEqual([decryptedRestoredConflict]);
|
|
});
|
|
|
|
test('decrypting a deleted conflict should keep it in the Trash note list', async () => {
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
const encryptedConflict = {
|
|
...notes[0],
|
|
deleted_time: Date.now(),
|
|
encryption_applied: 1,
|
|
// is_conflict is encrypted and is not known until the note is decrypted.
|
|
is_conflict: 0,
|
|
};
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = reducer(state, { type: 'FOLDER_SELECT', id: getTrashFolderId() });
|
|
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: [encryptedConflict], notesSource: 'test' });
|
|
|
|
const decryptedConflict = { ...encryptedConflict, encryption_applied: 0, is_conflict: 1 };
|
|
state = reducer(state, {
|
|
type: 'NOTE_UPDATE_ONE',
|
|
note: decryptedConflict,
|
|
changeSource: ItemChange.SOURCE_DECRYPTION,
|
|
});
|
|
|
|
expect(state.selectedFolderId).toBe(getTrashFolderId());
|
|
expect(state.notes).toEqual([decryptedConflict]);
|
|
});
|
|
|
|
it.each([
|
|
['without a noteId', undefined, true],
|
|
['for the selected note', 0, true],
|
|
['for a different note', 1, false],
|
|
])('should request an editor reload %s', async (_, noteIndex, shouldReload) => {
|
|
jest.useFakeTimers();
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(2, folders[0]);
|
|
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
|
|
const previousReloadTime = state.editorNoteReloadTimeRequest;
|
|
const previousWindowReloadTime = state.windowEditorNoteReloadTimeRequest;
|
|
const now = Date.now();
|
|
|
|
state = reducer(state, {
|
|
type: 'EDITOR_NOTE_NEEDS_RELOAD',
|
|
noteId: noteIndex === undefined ? undefined : notes[noteIndex].id,
|
|
});
|
|
|
|
expect(state.editorNoteReloadTimeRequest).toBe(
|
|
shouldReload ? now : previousReloadTime,
|
|
);
|
|
expect(state.windowEditorNoteReloadTimeRequest).toBe(
|
|
noteIndex === 0 ? now : previousWindowReloadTime,
|
|
);
|
|
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('should request reload only in windows displaying the specified note', async () => {
|
|
jest.useFakeTimers();
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(2, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
state = createBackgroundWindow(state, 'secondary', notes[1], notes);
|
|
const previousPrimaryReloadTime = state.windowEditorNoteReloadTimeRequest;
|
|
const now = Date.now();
|
|
|
|
state = reducer(state, {
|
|
type: 'EDITOR_NOTE_NEEDS_RELOAD',
|
|
noteId: notes[1].id,
|
|
});
|
|
|
|
expect(state.windowEditorNoteReloadTimeRequest).toBe(previousPrimaryReloadTime);
|
|
expect(state.backgroundWindows.secondary.windowEditorNoteReloadTimeRequest).toBe(now);
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it('should generate unique reload tokens within the same millisecond', async () => {
|
|
jest.useFakeTimers();
|
|
const folders = await createNTestFolders(1);
|
|
const notes = await createNTestNotes(1, folders[0]);
|
|
let state = initTestState(folders, 0, notes, [0]);
|
|
const now = Date.now();
|
|
|
|
state = reducer(state, { type: 'EDITOR_NOTE_NEEDS_RELOAD', noteId: notes[0].id });
|
|
expect(state.editorNoteReloadTimeRequest).toBe(now);
|
|
expect(state.windowEditorNoteReloadTimeRequest).toBe(now);
|
|
|
|
state = reducer(state, { type: 'EDITOR_NOTE_NEEDS_RELOAD', noteId: notes[0].id });
|
|
expect(state.editorNoteReloadTimeRequest).toBe(now + 1);
|
|
expect(state.windowEditorNoteReloadTimeRequest).toBe(now + 1);
|
|
|
|
jest.useRealTimers();
|
|
});
|
|
});
|