Mailspring/spec/tasks/send-draft-task-spec.coffee

587 lines
24 KiB
CoffeeScript
Raw Normal View History

_ = require 'underscore'
fs = require 'fs'
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
{APIError,
Actions,
DatabaseStore,
DatabaseTransaction,
Message,
2016-01-27 11:12:46 +08:00
Contact,
feat(transactions): Explicit (and faster) database transactions Summary: Until now, we've been hiding transactions beneath the surface. When you call persistModel, you're implicitly creating a transaction. You could explicitly create them with `atomically`..., but there were several critical problems that are fixed in this diff: - Calling persistModel / unpersistModel within a transaction could cause the DatabaseStore to trigger. This could result in other parts of the app making queries /during/ the transaction, potentially before the COMMIT occurred and saved the changes. The new, explicit inTransaction syntax holds all changes until after COMMIT and then triggers. - Calling atomically and then calling persistModel inside that resulted in us having to check whether a transaction was present and was gross. - Many parts of the code ran extensive logic inside a promise chained within `atomically`: BAD: ``` DatabaseStore.atomically => DatabaseStore.persistModel(draft) => GoMakeANetworkRequestThatReturnsAPromise ``` OVERWHELMINGLY BETTER: ``` DatabaseStore.inTransaction (t) => t.persistModel(draft) .then => GoMakeANetworkRequestThatReturnsAPromise ``` Having explicit transactions also puts us on equal footing with Sequelize and other ORMs. Note that you /have/ to call DatabaseStore.inTransaction (t) =>. There is no other way to access the methods that let you alter the database. :-) Other changes: - This diff removes Message.labels and the Message-Labels table. We weren't using Message-level labels anywhere, and the table could grow very large. - This diff changes the page size during initial sync from 250 => 200 in an effort to make transactions a bit faster. Test Plan: Run tests! Reviewers: juan, evan Reviewed By: juan, evan Differential Revision: https://phab.nylas.com/D2353
2015-12-18 03:46:05 +08:00
Task,
TaskQueue,
SendDraftTask,
SyncbackDraftTask,
NylasAPI,
SoundRegistry} = require 'nylas-exports'
DBt = DatabaseTransaction.prototype
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
describe "SendDraftTask", ->
describe "isDependentOnTask", ->
it "is not dependent on any pending SyncbackDraftTasks", ->
draftA = new Message
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
version: '1'
clientId: 'localid-A'
serverId: '1233123AEDF1'
accountId: 'A12ADE'
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
subject: 'New Draft'
draft: true
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
to:
name: 'Dummy'
email: 'dummy@nylas.com'
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
saveA = new SyncbackDraftTask(draftA, [])
sendA = new SendDraftTask(draftA, [])
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
expect(sendA.isDependentOnTask(saveA)).toBe(false)
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
describe "performLocal", ->
it "rejects if we we don't pass a draft", ->
badTask = new SendDraftTask()
badTask.performLocal().then ->
throw new Error("Shouldn't succeed")
.catch (err) ->
expect(err.message).toBe "SendDraftTask - must be provided a draft."
it "rejects if we we don't pass uploads", ->
message = new Message(from: [new Contact(email: TEST_ACCOUNT_EMAIL)])
2016-01-28 16:48:45 +08:00
message.uploads = null
badTask = new SendDraftTask(message)
badTask.performLocal().then ->
throw new Error("Shouldn't succeed")
.catch (err) ->
expect(err.message).toBe "SendDraftTask - must be provided an array of uploads."
it "rejects if no from address is specified", ->
badTask = new SendDraftTask(new Message(from: [], uploads: []))
badTask.performLocal().then ->
throw new Error("Shouldn't succeed")
.catch (err) ->
expect(err.message).toBe "SendDraftTask - you must populate `from` before sending."
it "rejects if the from address does not map to any account", ->
badTask = new SendDraftTask(new Message(from: [new Contact(email: 'not-configured@nylas.com')], uploads: null))
badTask.performLocal().then ->
throw new Error("Shouldn't succeed")
.catch (err) ->
expect(err.message).toBe "SendDraftTask - you can only send drafts from a configured account."
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
describe "performRemote", ->
beforeEach ->
@response =
version: 2
2016-01-27 11:12:46 +08:00
id: '1233123AEDF1'
account_id: TEST_ACCOUNT_ID
from: [new Contact(email: TEST_ACCOUNT_EMAIL)]
subject: 'New Draft'
body: 'hello world'
to:
name: 'Dummy'
email: 'dummy@nylas.com'
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
options.success?(@response)
return Promise.resolve(@response)
spyOn(NylasAPI, 'incrementRemoteChangeLock')
spyOn(NylasAPI, 'decrementRemoteChangeLock')
spyOn(DBt, 'unpersistModel').andReturn Promise.resolve()
spyOn(DBt, 'persistModel').andReturn Promise.resolve()
2015-10-02 10:24:06 +08:00
spyOn(SoundRegistry, "playSound")
spyOn(Actions, "postNotification")
spyOn(Actions, "sendDraftSuccess")
spyOn(Actions, "attachmentUploaded")
spyOn(fs, 'createReadStream').andReturn "stub"
# The tests below are invoked twice, once with a new @draft and one with a
# persisted @draft.
2016-01-27 11:12:46 +08:00
sharedTests = =>
it "should return Task.Status.Success", ->
waitsForPromise =>
@task.performLocal()
@task.performRemote().then (status) ->
expect(status).toBe Task.Status.Success
it "makes a send request with the correct data", ->
2016-01-27 11:12:46 +08:00
waitsForPromise => @task._sendAndCreateMessage().then =>
expect(NylasAPI.makeRequest).toHaveBeenCalled()
expect(NylasAPI.makeRequest.callCount).toBe(1)
options = NylasAPI.makeRequest.mostRecentCall.args[0]
expect(options.path).toBe("/send")
expect(options.method).toBe('POST')
expect(options.accountId).toBe TEST_ACCOUNT_ID
expect(options.body).toEqual @draft.toJSON()
it "should pass returnsModel:false", ->
2016-01-27 11:12:46 +08:00
waitsForPromise => @task._sendAndCreateMessage().then ->
expect(NylasAPI.makeRequest.calls.length).toBe(1)
options = NylasAPI.makeRequest.mostRecentCall.args[0]
expect(options.returnsModel).toBe(false)
it "should always send the draft body in the request body (joined attribute check)", ->
waitsForPromise =>
2016-01-27 11:12:46 +08:00
@task._sendAndCreateMessage().then =>
expect(NylasAPI.makeRequest.calls.length).toBe(1)
options = NylasAPI.makeRequest.mostRecentCall.args[0]
expect(options.body.body).toBe('hello world')
describe "saving the sent message", ->
it "should preserve the draft client id", ->
waitsForPromise =>
@task._sendAndCreateMessage().then =>
expect(DBt.persistModel).toHaveBeenCalled()
model = DBt.persistModel.mostRecentCall.args[0]
expect(model.clientId).toEqual(@draft.clientId)
expect(model.serverId).toEqual(@response.id)
expect(model.draft).toEqual(false)
it "should preserve metadata, but not version numbers", ->
waitsForPromise =>
@task._sendAndCreateMessage().then =>
expect(DBt.persistModel).toHaveBeenCalled()
model = DBt.persistModel.mostRecentCall.args[0]
fix(drafts): Various improvements and fixes to drafts, draft state management Summary: This diff contains a few major changes: 1. Scribe is no longer used for the text editor. It's just a plain contenteditable region. The toolbar items (bold, italic, underline) still work. Scribe was causing React inconcistency issues in the following scenario: - View thread with draft, edit draft - Move to another thread - Move back to thread with draft - Move to another thread. Notice that one or more messages from thread with draft are still there. There may be a way to fix this, but I tried for hours and there are Github Issues open on it's repository asking for React compatibility, so it may be fixed soon. For now contenteditable is working great. 2. Action.saveDraft() is no longer debounced in the DraftStore. Instead, firing that action causes the save to happen immediately, and the DraftStoreProxy has a new "DraftChangeSet" class which is responsbile for batching saves as the user interacts with the ComposerView. There are a couple big wins here: - In the future, we may want to be able to call Action.saveDraft() in other situations and it should behave like a normal action. We may also want to expose the DraftStoreProxy as an easy way of backing interactive draft UI. - Previously, when you added a contact to To/CC/BCC, this happened: <input> -> Action.saveDraft -> (delay!!) -> Database -> DraftStore -> DraftStoreProxy -> View Updates Increasing the delay to something reasonable like 200msec meant there was 200msec of lag before you saw the new view state. To fix this, I created a new class called DraftChangeSet which is responsible for accumulating changes as they're made and firing Action.saveDraft. "Adding" a change to the change set also causes the Draft provided by the DraftStoreProxy to change immediately (the changes are a temporary layer on top of the database object). This means no delay while changes are being applied. There's a better explanation in the source! This diff includes a few minor fixes as well: 1. Draft.state is gone—use Message.object = draft instead 2. String model attributes should never be null 3. Pre-send checks that can cancel draft send 4. Put the entire curl history and task queue into feedback reports 5. Cache localIds for extra speed 6. Move us up to latest React Test Plan: No new tests - once we lock down this new design I'll write tests for the DraftChangeSet Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1125
2015-02-04 08:24:31 +08:00
expect(model.pluginMetadata.length).toEqual(@draft.pluginMetadata.length)
for {pluginId, value, version} in @draft.pluginMetadata
updated = model.metadataObjectForPluginId(pluginId)
expect(updated.value).toEqual(value)
expect(updated.version).toEqual(0)
it "should notify the draft was sent", ->
waitsForPromise =>
@task.performRemote().then =>
args = Actions.sendDraftSuccess.calls[0].args[0]
expect(args.message instanceof Message).toBe(true)
expect(args.messageClientId).toBe(@draft.clientId)
it "should queue tasks to sync back the metadata on the new message", ->
waitsForPromise =>
spyOn(Actions, 'queueTask')
@task.performRemote().then =>
metadataTasks = Actions.queueTask.calls.map (call) -> call.args[0]
expect(metadataTasks.length).toEqual(@draft.pluginMetadata.length)
for pluginMetadatum, idx in @draft.pluginMetadata
expect(metadataTasks[idx].clientId).toEqual(@draft.clientId)
expect(metadataTasks[idx].modelClassName).toEqual('Message')
expect(metadataTasks[idx].pluginId).toEqual(pluginMetadatum.pluginId)
it "should play a sound", ->
spyOn(NylasEnv.config, "get").andReturn true
waitsForPromise => @task.performRemote().then ->
expect(NylasEnv.config.get).toHaveBeenCalledWith("core.sending.sounds")
expect(SoundRegistry.playSound).toHaveBeenCalledWith("send")
it "shouldn't play a sound if the config is disabled", ->
spyOn(NylasEnv.config, "get").andReturn false
waitsForPromise => @task.performRemote().then ->
expect(NylasEnv.config.get).toHaveBeenCalledWith("core.sending.sounds")
expect(SoundRegistry.playSound).not.toHaveBeenCalled()
describe "when there are errors", ->
beforeEach ->
2016-01-27 11:12:46 +08:00
spyOn(Actions, 'draftSendingFailed')
jasmine.unspy(NylasAPI, "makeRequest")
it "notifies of a permanent error of misc error types", ->
## DB error
thrownError = null
spyOn(NylasEnv, "reportError")
jasmine.unspy(DBt, "persistModel")
spyOn(DBt, "persistModel").andCallFake =>
thrownError = new Error('db error')
throw thrownError
waitsForPromise =>
@task.performRemote().then (status) =>
expect(status[0]).toBe Task.Status.Failed
expect(status[1]).toBe thrownError
2016-01-27 11:12:46 +08:00
expect(Actions.draftSendingFailed).toHaveBeenCalled()
feat(error): improve error reporting. Now `NylasEnv.reportError` Summary: The goal is to let us see what plugins are throwing errors on Sentry. We are using a Sentry `tag` to identify and group plugins and their errors. Along the way, I cleaned up the error catching and reporting system. There was a lot of duplicate error logic (that wasn't always right) and some legacy Atom error handling. Now, if you catch an error that we should report (like when handling extensions), call `NylasEnv.reportError`. This used to be called `emitError` but I changed it to `reportError` to be consistent with the ErrorReporter and be a bit more indicative of what it does. In the production version, the `ErrorLogger` will forward the request to the `nylas-private-error-reporter` which will report to Sentry. The `reportError` function also now inspects the stack to determine which plugin(s) it came from. These are passed along to Sentry. I also cleaned up the `console.log` and `console.error` code. We were logging errors multiple times making the console confusing to read. Worse is that we were logging the `error` object, which would print not the stack of the actual error, but rather the stack of where the console.error was logged from. Printing `error.stack` instead shows much more accurate stack traces. See changes in the Edgehill repo here: https://github.com/nylas/edgehill/commit/8c4a86eb7ee1a06249a9ae35397e2084a09ad1dc Test Plan: Manual Reviewers: juan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2509
2016-02-04 07:06:52 +08:00
expect(NylasEnv.reportError).toHaveBeenCalled()
2016-01-27 11:12:46 +08:00
it "retries the task if 'Invalid message public id'", ->
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
if options.body.reply_to_message_id
err = new APIError(body: "Invalid message public id")
Promise.reject(err)
else
options.success?(@response)
Promise.resolve(@response)
@draft.replyToMessageId = "reply-123"
@draft.threadId = "thread-123"
waitsForPromise => @task._sendAndCreateMessage(@draft).then =>
expect(NylasAPI.makeRequest).toHaveBeenCalled()
expect(NylasAPI.makeRequest.callCount).toEqual 2
req1 = NylasAPI.makeRequest.calls[0].args[0]
req2 = NylasAPI.makeRequest.calls[1].args[0]
expect(req1.body.reply_to_message_id).toBe "reply-123"
expect(req1.body.thread_id).toBe "thread-123"
expect(req2.body.reply_to_message_id).toBe null
expect(req2.body.thread_id).toBe "thread-123"
it "retries the task if 'Invalid message public id'", ->
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
if options.body.reply_to_message_id
err = new APIError(body: "Invalid thread")
Promise.reject(err)
else
options.success?(@response)
Promise.resolve(@response)
@draft.replyToMessageId = "reply-123"
@draft.threadId = "thread-123"
waitsForPromise => @task._sendAndCreateMessage(@draft).then =>
expect(NylasAPI.makeRequest).toHaveBeenCalled()
expect(NylasAPI.makeRequest.callCount).toEqual 2
req1 = NylasAPI.makeRequest.calls[0].args[0]
req2 = NylasAPI.makeRequest.calls[1].args[0]
expect(req1.body.reply_to_message_id).toBe "reply-123"
expect(req1.body.thread_id).toBe "thread-123"
expect(req2.body.reply_to_message_id).toBe null
expect(req2.body.thread_id).toBe null
it "notifies of a permanent error on 500 errors", ->
thrownError = new APIError(statusCode: 500, body: "err")
spyOn(NylasEnv, "reportError")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise => @task.performRemote().then (status) =>
expect(status[0]).toBe Task.Status.Failed
expect(status[1]).toBe thrownError
2016-01-27 11:12:46 +08:00
expect(Actions.draftSendingFailed).toHaveBeenCalled()
it "notifies us and users of a permanent error on 400 errors", ->
thrownError = new APIError(statusCode: 400, body: "err")
spyOn(NylasEnv, "reportError")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise => @task.performRemote().then (status) =>
expect(status[0]).toBe Task.Status.Failed
expect(status[1]).toBe thrownError
2016-01-27 11:12:46 +08:00
expect(Actions.draftSendingFailed).toHaveBeenCalled()
it "presents helpful error messages for 402 errors (security blocked)", ->
thrownError = new APIError(statusCode: 402, body: {
"message": "Message content rejected for security reasons",
"server_error": "552 : 5.7.0 This message was blocked because its content presents a potential\n5.7.0 security issue. Please visit\n5.7.0 https://support.google.com/mail/answer/6590 to review our message\n5.7.0 content and attachment content guidelines. fk9sm21147314pad.9 - gsmtp",
"type": "api_error"
})
expectedMessage =
"""
Sorry, this message could not be sent because it was rejected by your mail provider. (Message content rejected for security reasons)
552 : 5.7.0 This message was blocked because its content presents a potential
5.7.0 security issue. Please visit
5.7.0 https://support.google.com/mail/answer/6590 to review our message
5.7.0 content and attachment content guidelines. fk9sm21147314pad.9 - gsmtp
"""
spyOn(NylasEnv, "reportError")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise => @task.performRemote().then (status) =>
expect(status[0]).toBe Task.Status.Failed
expect(status[1]).toBe thrownError
expect(Actions.draftSendingFailed).toHaveBeenCalled()
expect(Actions.draftSendingFailed.calls[0].args[0].errorMessage).toEqual(expectedMessage)
it "presents helpful error messages for 402 errors (recipient failed)", ->
thrownError = new APIError(statusCode: 402, body: {
"message": "Sending to at least one recipient failed.",
"server_error": "<<Don't know what this looks like >>",
"type": "api_error"
})
expectedMessage = "This message could not be delivered to at least one recipient. (Note: other recipients may have received this message - you should check Sent Mail before re-sending this message.)"
spyOn(NylasEnv, "reportError")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise => @task.performRemote().then (status) =>
expect(status[0]).toBe Task.Status.Failed
expect(status[1]).toBe thrownError
expect(Actions.draftSendingFailed).toHaveBeenCalled()
expect(Actions.draftSendingFailed.calls[0].args[0].errorMessage).toEqual(expectedMessage)
it "retries on timeouts", ->
thrownError = new APIError(statusCode: NylasAPI.TimeoutErrorCode, body: "err")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise => @task.performRemote().then (status) =>
expect(status).toBe Task.Status.Retry
describe "checking the promise chain halts on errors", ->
beforeEach ->
spyOn(NylasEnv, 'reportError')
2016-01-27 11:12:46 +08:00
spyOn(@task,"_sendAndCreateMessage").andCallThrough()
spyOn(@task,"_deleteRemoteDraft").andCallThrough()
2016-01-27 11:12:46 +08:00
spyOn(@task,"_onSuccess").andCallThrough()
spyOn(@task,"_onError").andCallThrough()
@expectBlockedChain = =>
2016-01-27 11:12:46 +08:00
expect(@task._sendAndCreateMessage).toHaveBeenCalled()
expect(@task._deleteRemoteDraft).not.toHaveBeenCalled()
2016-01-27 11:12:46 +08:00
expect(@task._onSuccess).not.toHaveBeenCalled()
expect(@task._onError).toHaveBeenCalled()
it "halts on 500s", ->
thrownError = new APIError(statusCode: 500, body: "err")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise =>
@task.performRemote().then (status) =>
@expectBlockedChain()
it "halts on 400s", ->
thrownError = new APIError(statusCode: 400, body: "err")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise =>
@task.performRemote().then (status) =>
@expectBlockedChain()
it "halts and retries on not permanent error codes", ->
thrownError = new APIError(statusCode: 409, body: "err")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise =>
@task.performRemote().then (status) =>
@expectBlockedChain()
it "halts on other errors", ->
thrownError = new Error("oh no")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(thrownError)
waitsForPromise =>
@task.performRemote().then (status) =>
@expectBlockedChain()
it "doesn't halt on success", ->
# Don't spy reportError to make sure to fail the test on unexpected
# errors
jasmine.unspy(NylasEnv, 'reportError')
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
options.success?(@response)
Promise.resolve(@response)
waitsForPromise =>
@task.performRemote().then (status) =>
expect(status).toBe Task.Status.Success
expect(@task._sendAndCreateMessage).toHaveBeenCalled()
expect(@task._deleteRemoteDraft).toHaveBeenCalled()
expect(@task._onSuccess).toHaveBeenCalled()
expect(@task._onError).not.toHaveBeenCalled()
describe "with a new draft", ->
beforeEach ->
@draft = new Message
version: 1
2016-01-27 11:12:46 +08:00
clientId: 'client-id'
accountId: TEST_ACCOUNT_ID
from: [new Contact(email: TEST_ACCOUNT_EMAIL)]
subject: 'New Draft'
draft: true
body: 'hello world'
2016-01-28 16:48:45 +08:00
uploads: []
@draft.applyPluginMetadata('pluginIdA', {tracked: true})
@draft.applyPluginMetadata('pluginIdB', {a: true, b: 2})
@draft.metadataObjectForPluginId('pluginIdA').version = 2
2016-01-28 16:48:45 +08:00
@task = new SendDraftTask(@draft)
@calledBody = "ERROR: The body wasn't included!"
spyOn(DatabaseStore, "findBy").andCallFake =>
Promise.resolve(@draft)
2016-01-27 11:12:46 +08:00
sharedTests()
it "should locally convert the draft to a message on send", ->
waitsForPromise =>
@task.performRemote().then =>
expect(DBt.persistModel).toHaveBeenCalled()
model = DBt.persistModel.calls[0].args[0]
expect(model.clientId).toBe @draft.clientId
expect(model.serverId).toBe @response.id
expect(model.draft).toBe false
describe "deleteRemoteDraft", ->
it "should not make an API request", ->
waitsForPromise =>
@task._deleteRemoteDraft(@draft).then =>
expect(NylasAPI.makeRequest).not.toHaveBeenCalled()
describe "with an existing persisted draft", ->
beforeEach ->
@draft = new Message
version: 1
2016-01-27 11:12:46 +08:00
clientId: 'client-id'
serverId: 'server-123'
accountId: TEST_ACCOUNT_ID
from: [new Contact(email: TEST_ACCOUNT_EMAIL)]
subject: 'New Draft'
draft: true
body: 'hello world'
to:
name: 'Dummy'
email: 'dummy@nylas.com'
2016-01-28 16:48:45 +08:00
uploads: []
@draft.applyPluginMetadata('pluginIdA', {tracked: true})
@draft.applyPluginMetadata('pluginIdB', {a: true, b: 2})
@draft.metadataObjectForPluginId('pluginIdA').version = 2
2016-01-28 16:48:45 +08:00
@task = new SendDraftTask(@draft)
@calledBody = "ERROR: The body wasn't included!"
spyOn(DatabaseStore, "findBy").andCallFake =>
Promise.resolve(@draft)
2016-01-27 11:12:46 +08:00
sharedTests()
it "should locally convert the existing draft to a message on send", ->
2016-01-27 11:12:46 +08:00
expect(@draft.clientId).toBe @draft.clientId
expect(@draft.serverId).toBe "server-123"
2016-01-27 11:12:46 +08:00
@task.performLocal()
waitsForPromise => @task.performRemote().then =>
expect(DBt.persistModel).toHaveBeenCalled()
model = DBt.persistModel.calls[0].args[0]
2016-01-27 11:12:46 +08:00
expect(model.clientId).toBe @draft.clientId
expect(model.serverId).toBe @response.id
expect(model.draft).toBe false
describe "deleteRemoteDraft", ->
it "should make an API request to delete the draft", ->
@task.performLocal()
waitsForPromise =>
@task._deleteRemoteDraft(@draft).then =>
expect(NylasAPI.makeRequest).toHaveBeenCalled()
expect(NylasAPI.makeRequest.callCount).toBe 1
req = NylasAPI.makeRequest.calls[0].args[0]
expect(req.path).toBe "/drafts/#{@draft.serverId}"
expect(req.accountId).toBe TEST_ACCOUNT_ID
expect(req.method).toBe "DELETE"
expect(req.returnsModel).toBe false
it "should increment the change tracker, preventing any further deltas about the draft", ->
@task.performLocal()
waitsForPromise =>
@task._deleteRemoteDraft(@draft).then =>
expect(NylasAPI.incrementRemoteChangeLock).toHaveBeenCalledWith(Message, @draft.serverId)
it "should continue if the request fails", ->
jasmine.unspy(NylasAPI, "makeRequest")
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
Promise.reject(new APIError(body: "Boo", statusCode: 500))
@task.performLocal()
waitsForPromise =>
@task._deleteRemoteDraft(@draft)
.then =>
expect(NylasAPI.makeRequest).toHaveBeenCalled()
expect(NylasAPI.makeRequest.callCount).toBe 1
.catch =>
throw new Error("Shouldn't fail the promise")
describe "with uploads", ->
beforeEach ->
@uploads = [
{targetPath: '/test-file-1.png', size: 100},
{targetPath: '/test-file-2.png', size: 100}
]
@draft = new Message
version: 1
clientId: 'client-id'
accountId: TEST_ACCOUNT_ID
from: [new Contact(email: TEST_ACCOUNT_EMAIL)]
subject: 'New Draft'
draft: true
body: 'hello world'
uploads: [].concat(@uploads)
@task = new SendDraftTask(@draft)
jasmine.unspy(NylasAPI, 'makeRequest')
@resolves = []
@resolveAll = =>
resolve() for resolve in @resolves
@resolves = []
advanceClock()
spyOn(NylasAPI, 'makeRequest').andCallFake (options) =>
response = @response
if options.path is '/files'
response = JSON.stringify([{
id: '1234'
account_id: TEST_ACCOUNT_ID
filename: options.formData.file.options.filename
}])
new Promise (resolve, reject) =>
@resolves.push =>
options.success?(response)
resolve(response)
spyOn(DatabaseStore, 'findBy').andCallFake =>
Promise.resolve(@draft)
it "should begin file uploads and not hit /send until they complete", ->
@task.performRemote()
advanceClock()
# uploads should be queued, but not the send
expect(NylasAPI.makeRequest.callCount).toEqual(2)
expect(NylasAPI.makeRequest.calls[0].args[0].formData).toEqual({ file : { value : 'stub', options : { filename : 'test-file-1.png' } } })
expect(NylasAPI.makeRequest.calls[1].args[0].formData).toEqual({ file : { value : 'stub', options : { filename : 'test-file-2.png' } } })
# finish all uploads
@resolveAll()
# send should now be queued
expect(NylasAPI.makeRequest.callCount).toEqual(3)
expect(NylasAPI.makeRequest.calls[2].args[0].path).toEqual('/send')
it "should convert the uploads to files", ->
@task.performRemote()
advanceClock()
expect(@task.draft.files.length).toEqual(0)
expect(@task.draft.uploads.length).toEqual(2)
@resolves[0]()
advanceClock()
expect(@task.draft.files.length).toEqual(1)
expect(@task.draft.uploads.length).toEqual(1)
{filename, accountId, id} = @task.draft.files[0]
expect({filename, accountId, id}).toEqual({
filename: 'test-file-1.png',
accountId: TEST_ACCOUNT_ID,
id: '1234'
})
describe "cancel, during attachment upload", ->
it "should make the task resolve early, before making the /send call", ->
exitStatus = null
@task.performRemote().then (status) => exitStatus = status
advanceClock()
@task.cancel()
NylasAPI.makeRequest.reset()
@resolveAll()
advanceClock()
expect(NylasAPI.makeRequest).not.toHaveBeenCalled()
expect(exitStatus).toEqual(Task.Status.Continue)
describe "after the message sends", ->
it "should notify the attachments were uploaded (so they can be deleted)", ->
@task.performRemote()
advanceClock()
@resolveAll() # uploads
@resolveAll() # send
expect(Actions.attachmentUploaded).toHaveBeenCalled()
expect(Actions.attachmentUploaded.callCount).toEqual(@uploads.length)
for upload, idx in @uploads
expect(Actions.attachmentUploaded.calls[idx].args[0]).toBe(upload)