Mailspring/app/spec/stores/draft-editing-session-spec.es6
2018-01-23 17:35:09 -08:00

292 lines
11 KiB
JavaScript

/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const Message = require('../../src/flux/models/message').default;
const Actions = require('../../src/flux/actions').default;
const DatabaseStore = require('../../src/flux/stores/database-store').default;
const DraftEditingSession = require('../../src/flux/stores/draft-editing-session').default;
const { DraftChangeSet } = DraftEditingSession;
const _ = require('underscore');
xdescribe("DraftEditingSession Specs", function() {
describe("DraftChangeSet", function() {
beforeEach(function() {
this.onDidAddChanges = jasmine.createSpy('onDidAddChanges');
this.onWillAddChanges = jasmine.createSpy('onWillAddChanges');
this.commitResolve = null;
this.commitResolves = [];
this.onCommit = jasmine.createSpy('commit').andCallFake(() => {
new Promise((resolve, reject) => {
this.commitResolves.push(resolve);
this.commitResolve = resolve;
});
});
this.changeSet = new DraftChangeSet({
onDidAddChanges: this.onDidAddChanges,
onWillAddChanges: this.onWillAddChanges,
onCommit: this.onCommit,
});
this.changeSet._pending =
{subject: 'Change to subject line'};
});
describe("teardown", () =>
it("should remove all of the pending and saving changes", function() {
this.changeSet.teardown();
expect(this.changeSet._saving).toEqual({});
expect(this.changeSet._pending).toEqual({});
})
);
describe("add", function() {
it("should mark that the draft is not pristine", function() {
this.changeSet.add({body: 'Hello World!'});
expect(this.changeSet._pending.pristine).toEqual(false);
});
it("should add the changes to the _pending set", function() {
this.changeSet.add({body: 'Hello World!'});
expect(this.changeSet._pending.body).toEqual('Hello World!');
});
describe("otherwise", () =>
it("should commit after thirty seconds", function() {
spyOn(this.changeSet, 'commit');
this.changeSet.add({body: 'Hello World!'});
expect(this.changeSet.commit).not.toHaveBeenCalled();
advanceClock(31000);
expect(this.changeSet.commit).toHaveBeenCalled();
})
);
});
describe("commit", function() {
it("should resolve immediately if the pending set is empty", function() {
this.changeSet._pending = {};
waitsForPromise(() => {
return this.changeSet.commit().then(() => {
expect(this.onCommit).not.toHaveBeenCalled();
});
});
});
it("should move changes to the saving set", function() {
const pendingBefore = Object.assign({}, this.changeSet._pending);
expect(this.changeSet._saving).toEqual({});
this.changeSet.commit();
advanceClock();
expect(this.changeSet._pending).toEqual({});
expect(this.changeSet._saving).toEqual(pendingBefore);
});
it("should call the commit handler and then clear the saving set", function() {
this.changeSet.commit();
advanceClock();
expect(this.changeSet._saving).not.toEqual({});
this.commitResolve();
advanceClock();
expect(this.changeSet._saving).toEqual({});
});
describe("concurrency", () =>
it("the commit function should always run serially", function() {
let firstFulfilled = false;
let secondFulfilled = false;
this.changeSet._pending = {subject: 'A'};
this.changeSet.commit().then(() => {
this.changeSet._pending = {subject: 'B'};
firstFulfilled = true;
});
this.changeSet.commit().then(() => {
secondFulfilled = true;
});
advanceClock();
expect(firstFulfilled).toBe(false);
expect(secondFulfilled).toBe(false);
this.commitResolves[0]();
advanceClock();
expect(firstFulfilled).toBe(true);
expect(secondFulfilled).toBe(false);
this.commitResolves[1]();
advanceClock();
expect(firstFulfilled).toBe(true);
expect(secondFulfilled).toBe(true);
})
);
});
});
describe("DraftEditingSession", function() {
describe("constructor", function() {
it("should make a query to fetch the draft", function() {
spyOn(DatabaseStore, 'run').andCallFake(() => {
return new Promise((resolve, reject) => {});
});
const session = new DraftEditingSession('client-id');
expect(DatabaseStore.run).toHaveBeenCalled();
});
describe("when given a draft object", function() {
beforeEach(function() {
spyOn(DatabaseStore, 'run');
this.draft = new Message({draft: true, body: '123'});
this.session = new DraftEditingSession('client-id', this.draft);
});
it("should not make a query for the draft", () => expect(DatabaseStore.run).not.toHaveBeenCalled());
it("prepare should resolve without querying for the draft", function() {
waitsForPromise(() => this.session.prepare().then(() => {
expect(this.session.draft()).toBeDefined();
expect(DatabaseStore.run).not.toHaveBeenCalled();
})
);
});
});
});
describe("teardown", () =>
it("should mark the session as destroyed", function() {
spyOn(DraftEditingSession.prototype, "prepare");
const session = new DraftEditingSession('client-id');
session.teardown();
expect(session._destroyed).toEqual(true);
})
);
describe("prepare", function() {
beforeEach(function() {
this.draft = new Message({draft: true, body: '123', id: 'client-id'});
spyOn(DraftEditingSession.prototype, "prepare");
this.session = new DraftEditingSession('client-id');
spyOn(this.session, '_setDraft').andCallThrough();
spyOn(DatabaseStore, 'run').andCallFake(modelQuery => {
return Promise.resolve(this.draft);
});
jasmine.unspy(DraftEditingSession.prototype, "prepare");
});
it("should call setDraft with the retrieved draft", function() {
waitsForPromise(() => {
return this.session.prepare().then(() => {
expect(this.session._setDraft).toHaveBeenCalledWith(this.draft);
});
});
});
it("should resolve with the DraftEditingSession", function() {
waitsForPromise(() => {
return this.session.prepare().then(val => {
expect(val).toBe(this.session);
});
});
});
describe("error handling", function() {
it("should reject if the draft session has already been destroyed", function() {
this.session._destroyed = true;
waitsForPromise(() => {
return this.session.prepare().then(() => {
expect(false).toBe(true);
}).catch(val => {
expect(val instanceof Error).toBe(true);
});
});
});
it("should reject if the draft cannot be found", function() {
this.draft = null;
waitsForPromise(() => {
return this.session.prepare().then(() => {
expect(false).toBe(true);
}).catch(val => {
expect(val instanceof Error).toBe(true);
});
});
});
});
});
describe("when a draft changes", function() {
beforeEach(function() {
this.draft = new Message({draft: true, id: 'client-id', body: 'A', subject: 'initial'});
this.session = new DraftEditingSession('client-id', this.draft);
advanceClock();
spyOn(Actions, "queueTask").andReturn(Promise.resolve());
});
it("should ignore the update unless it applies to the current draft", function() {
spyOn(this.session, 'trigger');
this.session._onDraftChanged({objectClass: 'message', objects: [new Message()]});
expect(this.session.trigger).not.toHaveBeenCalled();
this.session._onDraftChanged({objectClass: 'message', objects: [this.draft]});
expect(this.session.trigger).toHaveBeenCalled();
});
it("should apply the update to the current draft", function() {
const updatedDraft = this.draft.clone();
updatedDraft.subject = 'This is the new subject';
spyOn(this.session, '_setDraft');
this.session._onDraftChanged({objectClass: 'message', objects: [updatedDraft]});
expect(this.session._setDraft).toHaveBeenCalled();
const draft = this.session._setDraft.calls[0].args[0];
expect(draft.subject).toEqual(updatedDraft.subject);
});
it("atomically commits changes", function() {
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(this.draft));
spyOn(DatabaseStore, 'inTransaction').andCallThrough();
this.session.changes.add({body: "123"});
waitsForPromise(() => {
return this.session.changes.commit().then(() => {
expect(DatabaseStore.inTransaction).toHaveBeenCalled();
expect(DatabaseStore.inTransaction.calls.length).toBe(1);
});
});
});
it("persist the applied changes", function() {
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(this.draft));
this.session.changes.add({body: "123"});
waitsForPromise(() => {
return this.session.changes.commit().then(() => {
expect(updated.body).toBe("123");
});
});
});
describe("when findBy does not return a draft", () =>
it("continues and persists it's local draft reference, so it is resaved and draft editing can continue", function() {
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(null));
this.session.changes.add({body: "123"});
waitsForPromise(() => {
return this.session.changes.commit().then(() => {
expect(updated.body).toBe("123");
});
});
})
);
it("does nothing if the draft is marked as destroyed", function() {
spyOn(DatabaseStore, "run").andReturn(Promise.resolve(this.draft));
spyOn(DatabaseStore, 'inTransaction').andCallThrough();
return waitsForPromise(() => {
this.session._destroyed = true;
this.session.changes.add({body: "123"});
return this.session.changes.commit().then(() => {
expect(DatabaseStore.inTransaction).not.toHaveBeenCalled();
});
});
});
});
});
});