1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-10-31 00:07:48 +02:00

Tests: Integration harness fixes and refactors. (#2569)

* Test harness fixes and integration test refactor and addition.

* Clean up.

* Address review comments.

* Improve method names.
This commit is contained in:
mic704b
2020-02-28 05:25:42 +11:00
committed by GitHub
parent a576ad2a39
commit 54dc2219fe
10 changed files with 221 additions and 173 deletions

View File

@@ -0,0 +1,93 @@
/* eslint-disable no-unused-vars */
require('app-module-path').addPath(__dirname);
const { setupDatabaseAndSynchronizer, switchClient, asyncTest, id, ids, sortedIds, at, createNTestFolders, createNTestNotes, createNTestTags, TestApp } = require('test-utils.js');
const Setting = require('lib/models/Setting.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const { time } = require('lib/time-utils.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids.js');
//
// The integration tests are to test the integration of the core system, comprising the
// base application with middleware, reducer and models in response to dispatched events.
//
// The general strategy for each integration test is:
// - create a starting application state,
// - inject the event to be tested
// - check the resulting application state
//
// Important: sleep must be used after TestApp dispatch to allow the async processing
// to complete
//
// use this until Javascript arr.flat() function works in Travis
function flatten(arr) {
return (arr.reduce((acc, val) => acc.concat(val), []));
}
let testApp = null;
describe('integration_ShowAllNotes', function() {
beforeEach(async (done) => {
testApp = new TestApp();
await testApp.start(['--no-welcome']);
done();
});
afterEach(async (done) => {
if (testApp !== null) await testApp.destroy();
testApp = null;
done();
});
it('should show all notes', asyncTest(async () => {
// setup
let folders = await createNTestFolders(3);
Folder.moveToFolder(id(folders[2]), id(folders[1])); // subfolder
await time.msleep(100);
let notes0 = await createNTestNotes(3, folders[0]);
let notes1 = await createNTestNotes(3, folders[1]);
let notes2 = await createNTestNotes(3, folders[2]);
// TEST ACTION: View all-notes
testApp.dispatch({ type: 'SMART_FILTER_SELECT', id: ALL_NOTES_FILTER_ID });
await time.msleep(100);
// check: all the notes are shown
let state = testApp.store().getState();
expect(state.notesParentType).toEqual('SmartFilter');
expect(state.selectedSmartFilterId).toEqual(ALL_NOTES_FILTER_ID);
expect(sortedIds(state.notes)).toEqual(sortedIds(notes0.concat(notes1).concat(notes2)));
}));
it('should show retain note selection when going from a folder to all-notes', asyncTest(async () => {
// setup
let folders = await createNTestFolders(2);
let notes0 = await createNTestNotes(3, folders[0]);
let notes1 = await createNTestNotes(3, folders[1]);
testApp.dispatch({ type: 'FOLDER_SELECT', id: id(folders[1]) });
await time.msleep(100);
testApp.dispatch({ type: 'NOTE_SELECT', id: id(notes1[1]) });
await time.msleep(100);
// check the state is set up as expected
let state = testApp.store().getState();
expect(state.notesParentType).toEqual('Folder');
expect(state.selectedFolderId).toEqual(id(folders[1]));
expect(sortedIds(state.notes)).toEqual(sortedIds(notes1));
expect(state.selectedNoteIds).toEqual(ids([notes1[1]]));
// TEST ACTION: View all-notes
testApp.dispatch({ type: 'SMART_FILTER_SELECT', id: ALL_NOTES_FILTER_ID });
await time.msleep(100);
// check: all the notes are shown
state = testApp.store().getState();
expect(state.notesParentType).toEqual('SmartFilter');
expect(state.selectedSmartFilterId).toEqual(ALL_NOTES_FILTER_ID);
expect(sortedIds(state.notes)).toEqual(sortedIds(notes0.concat(notes1)));
expect(state.selectedNoteIds).toEqual(ids([notes1[1]]));
}));
});

View File

@@ -1,117 +0,0 @@
/* eslint-disable no-unused-vars */
require('app-module-path').addPath(__dirname);
const { setupDatabaseAndSynchronizer, switchClient, asyncTest, TestApp } = require('test-utils.js');
const Setting = require('lib/models/Setting.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const { time } = require('lib/time-utils.js');
const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids.js');
//
// The integration tests are to test the integration of the core system, comprising the
// base application with middleware, reducer and models in response to dispatched events.
//
// The general strategy for each integration test is:
// - create a starting application state,
// - inject the event to be tested
// - check the resulting application state
//
// In particular, this file contains integration tests for smart filter features.
//
async function createNTestFolders(n) {
let folders = [];
for (let i = 0; i < n; i++) {
let folder = await Folder.save({ title: 'folder' });
folders.push(folder);
}
return folders;
}
async function createNTestNotes(n, folder) {
let notes = [];
for (let i = 0; i < n; i++) {
let note = await Note.save({ title: 'note', parent_id: folder.id, is_conflict: 0 });
notes.push(note);
}
return notes;
}
async function createNTestTags(n) {
let tags = [];
for (let i = 0; i < n; i++) {
let tag = await Tag.save({ title: 'tag' });
tags.push(tag);
}
return tags;
}
// use this until Javascript arr.flat() function works in Travis
function flatten(arr) {
return (arr.reduce((acc, val) => acc.concat(val), []));
}
let testApp = null;
describe('integration_SmartFilters', function() {
beforeEach(async (done) => {
testApp = new TestApp();
await testApp.start(['--no-welcome']);
done();
});
afterEach(async (done) => {
if (testApp !== null) await testApp.destroy();
testApp = null;
done();
});
it('should show notes in a folder', asyncTest(async () => {
let folders = await createNTestFolders(2);
let notes = [];
for (let i = 0; i < folders.length; i++) {
notes.push(await createNTestNotes(3, folders[i]));
}
testApp.dispatch({
type: 'FOLDER_SELECT',
id: folders[1].id,
});
await time.msleep(100);
let state = testApp.store().getState();
expect(state.notesParentType).toEqual('Folder');
expect(state.selectedFolderId).toEqual(folders[1].id);
let expectedNoteIds = notes[1].map(n => n.id).sort();
let noteIds = state.notes.map(n => n.id).sort();
expect(noteIds).toEqual(expectedNoteIds);
}));
it('should show all notes', asyncTest(async () => {
let folders = await createNTestFolders(2);
let notes = [];
for (let i = 0; i < folders.length; i++) {
notes.push(await createNTestNotes(3, folders[i]));
}
testApp.dispatch({
type: 'SMART_FILTER_SELECT',
id: ALL_NOTES_FILTER_ID,
});
await time.msleep(100);
let state = testApp.store().getState();
expect(state.notesParentType).toEqual('SmartFilter');
expect(state.selectedSmartFilterId).toEqual(ALL_NOTES_FILTER_ID);
// let expectedNoteIds = notes.map(n => n.map(o => o.id)).flat().sort();
let expectedNoteIds = flatten(notes.map(n => n.map(o => o.id))).sort();
let noteIds = state.notes.map(n => n.id).sort();
expect(noteIds).toEqual(expectedNoteIds);
}));
});

View File

@@ -1,40 +1,13 @@
/* eslint-disable no-unused-vars */
require('app-module-path').addPath(__dirname);
const { setupDatabaseAndSynchronizer, switchClient, asyncTest } = require('test-utils.js');
const { setupDatabaseAndSynchronizer, switchClient, asyncTest, createNTestNotes, createNTestFolders, createNTestTags } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js');
const { reducer, defaultState, stateUtils } = require('lib/reducer.js');
async function createNTestFolders(n) {
let folders = [];
for (let i = 0; i < n; i++) {
let folder = await Folder.save({ title: 'folder' });
folders.push(folder);
}
return folders;
}
async function createNTestNotes(n, folder) {
let notes = [];
for (let i = 0; i < n; i++) {
let note = await Note.save({ title: 'note', parent_id: folder.id, is_conflict: 0 });
notes.push(note);
}
return notes;
}
async function createNTestTags(n) {
let tags = [];
for (let i = 0; i < n; i++) {
let tag = await Tag.save({ title: 'tag' });
tags.push(tag);
}
return tags;
}
function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tags=null, selectedTagIndex=null) {
function initTestState(folders, selectedFolderIndex, notes, selectedNoteIndexes, tags=null, selectedTagIndex=null) {
let state = defaultState;
if (selectedFolderIndex != null) {
@@ -46,10 +19,10 @@ function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tag
if (notes != null) {
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, noteSource: 'test' });
}
if (selectedIndexes != null) {
if (selectedNoteIndexes != null) {
let selectedIds = [];
for (let i = 0; i < selectedIndexes.length; i++) {
selectedIds.push(notes[selectedIndexes[i]].id);
for (let i = 0; i < selectedNoteIndexes.length; i++) {
selectedIds.push(notes[selectedNoteIndexes[i]].id);
}
state = reducer(state, { type: 'NOTE_SELECT', ids: selectedIds });
}

View File

@@ -153,6 +153,7 @@ async function switchClient(id) {
async function clearDatabase(id = null) {
if (id === null) id = currentClient_;
if (!databases_[id]) return;
await ItemChange.waitForAllSaved();
@@ -178,7 +179,6 @@ async function clearDatabase(id = null) {
queries.push(`DELETE FROM ${n}`);
queries.push(`DELETE FROM sqlite_sequence WHERE name="${n}"`); // Reset autoincremented IDs
}
await databases_[id].transactionExecBatch(queries);
}
@@ -413,6 +413,60 @@ async function allSyncTargetItemsEncrypted() {
return totalCount === encryptedCount;
}
function id(a) {
return a.id;
}
function ids(a) {
return a.map(n => n.id);
}
function sortedIds(a) {
return ids(a).sort();
}
function at(a, indexes) {
let out = [];
for (let i = 0; i < indexes.length; i++) {
out.push(a[indexes[i]]);
}
return out;
}
async function createNTestFolders(n) {
let folders = [];
for (let i = 0; i < n; i++) {
let folder = await Folder.save({ title: 'folder' });
folders.push(folder);
}
return folders;
}
async function createNTestNotes(n, folder, tagIds = null, title = 'note') {
let notes = [];
for (let i = 0; i < n; i++) {
let title_ = n > 1 ? `${title}${i}` : title;
let note = await Note.save({ title: title_, parent_id: folder.id, is_conflict: 0 });
notes.push(note);
}
if (tagIds) {
for (let i = 0; i < notes.length; i++) {
await Tag.setNoteTagsByIds(notes[i].id, tagIds);
}
}
return notes;
}
async function createNTestTags(n) {
let tags = [];
for (let i = 0; i < n; i++) {
let tag = await Tag.save({ title: 'tag' });
tags.push(tag);
}
return tags;
}
// Integration test application
class TestApp extends BaseApplication {
constructor() {
super();
@@ -420,10 +474,11 @@ class TestApp extends BaseApplication {
}
async start(argv) {
argv = await super.start(argv);
await clearDatabase(); // not sure why we need this as we use our own database
argv = argv.concat(['--profile', `tests-build/profile-${uuid.create()}`]);
argv = await super.start(['',''].concat(argv));
this.initRedux();
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
Setting.dispatchUpdateAll();
await time.msleep(100);
}
@@ -449,13 +504,12 @@ class TestApp extends BaseApplication {
}
async destroy() {
this.deinitRedux();
await this.waitForMiddleware_();
this.deinitRedux();
await super.destroy();
}
}
module.exports = { kvStore, resourceService, allSyncTargetItemsEncrypted, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, TestApp };
module.exports = { kvStore, resourceService, allSyncTargetItemsEncrypted, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };

View File

@@ -55,11 +55,20 @@ class BaseApplication {
}
async destroy() {
if (this.scheduleAutoAddResourcesIID_) {
clearTimeout(this.scheduleAutoAddResourcesIID_);
this.scheduleAutoAddResourcesIID_ = null;
}
await ResourceFetcher.instance().destroy();
await SearchEngine.instance().destroy();
await DecryptionWorker.instance().destroy();
await FoldersScreenUtils.cancelTimers();
await SearchEngine.instance().cancelTimers();
await DecryptionWorker.instance().cancelTimers();
await reg.cancelTimers();
this.eventEmitter_.removeAllListeners();
BaseModel.db_ = null;
reg.setDb(null);
this.logger_ = null;
this.dbLogger_ = null;
this.eventEmitter_ = null;

View File

@@ -56,7 +56,10 @@ class FoldersScreenUtils {
}
static async cancelTimers() {
if (this.scheduleRefreshFoldersIID_) clearTimeout(this.scheduleRefreshFoldersIID_);
if (this.scheduleRefreshFoldersIID_) {
clearTimeout(this.scheduleRefreshFoldersIID_);
this.scheduleRefreshFoldersIID_ = null;
}
return new Promise((resolve) => {
const iid = setInterval(() => {
if (!FoldersScreenUtils.refreshCalls_.length) {

View File

@@ -193,7 +193,10 @@ reg.db = () => {
};
reg.cancelTimers = async () => {
if (this.recurrentSyncId_) clearTimeout(this.recurrentSyncId_);
if (this.recurrentSyncId_) {
clearTimeout(this.recurrentSyncId_);
this.recurrentSyncId_ = null;
}
return new Promise((resolve) => {
const iid = setInterval(() => {
if (!reg.syncCalls_.length) {

View File

@@ -37,9 +37,9 @@ class DecryptionWorker {
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new DecryptionWorker();
return this.instance_;
if (DecryptionWorker.instance_) return DecryptionWorker.instance_;
DecryptionWorker.instance_ = new DecryptionWorker();
return DecryptionWorker.instance_;
}
setEncryptionService(v) {
@@ -250,8 +250,15 @@ class DecryptionWorker {
}
}
async cancelTimers() {
if (this.scheduleId_) clearTimeout(this.scheduleId_);
async destroy() {
this.eventEmitter_.removeAllListeners();
if (this.scheduleId_) {
clearTimeout(this.scheduleId_);
this.scheduleId_ = null;
}
this.eventEmitter_ = null;
DecryptionWorker.instance_ = null;
return new Promise((resolve) => {
const iid = setInterval(() => {
if (!this.startCalls_.length) {
@@ -263,4 +270,6 @@ class DecryptionWorker {
}
}
DecryptionWorker.instance_ = null;
module.exports = DecryptionWorker;

View File

@@ -24,9 +24,9 @@ class ResourceFetcher extends BaseService {
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new ResourceFetcher();
return this.instance_;
if (ResourceFetcher.instance_) return ResourceFetcher.instance_;
ResourceFetcher.instance_ = new ResourceFetcher();
return ResourceFetcher.instance_;
}
on(eventName, callback) {
@@ -248,6 +248,20 @@ class ResourceFetcher extends BaseService {
await Resource.resetStartedFetchStatus();
this.autoAddResources(null);
}
async destroy() {
this.eventEmitter_.removeAllListeners();
if (this.scheduleQueueProcessIID_) {
clearTimeout(this.scheduleQueueProcessIID_);
this.scheduleQueueProcessIID_ = null;
}
this.eventEmitter_ = null;
ResourceFetcher.instance_ = null;
return await this.waitForAllFinished();
}
}
ResourceFetcher.instance_ = null;
module.exports = ResourceFetcher;

View File

@@ -18,9 +18,9 @@ class SearchEngine {
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new SearchEngine();
return this.instance_;
if (SearchEngine.instance_) return SearchEngine.instance_;
SearchEngine.instance_ = new SearchEngine();
return SearchEngine.instance_;
}
setLogger(logger) {
@@ -413,8 +413,13 @@ class SearchEngine {
}
}
async cancelTimers() {
if (this.scheduleSyncTablesIID_) clearTimeout(this.scheduleSyncTablesIID_);
async destroy() {
if (this.scheduleSyncTablesIID_) {
clearTimeout(this.scheduleSyncTablesIID_);
this.scheduleSyncTablesIID_ = null;
}
SearchEngine.instance_ = null;
return new Promise((resolve) => {
const iid = setInterval(() => {
if (!this.syncCalls_.length) {
@@ -426,4 +431,6 @@ class SearchEngine {
}
}
SearchEngine.instance_ = null;
module.exports = SearchEngine;