mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-08 05:34:23 +08:00
refactor(draft): clean up draft store session proxy logic
Summary: This is an effort to make the logic around the process of sending a draft cleaner and easier to understand. Fixes T1253 Test Plan: edgehill --test Reviewers: bengotow Reviewed By: bengotow Maniphest Tasks: T1253 Differential Revision: https://phab.nylas.com/D1518
This commit is contained in:
parent
684a8ef4c0
commit
ae41c94e42
16 changed files with 236 additions and 264 deletions
|
@ -52,7 +52,7 @@ class ComposerView extends React.Component
|
|||
showbcc: false
|
||||
showsubject: false
|
||||
showQuotedText: false
|
||||
isSending: DraftStore.sendingState(@props.localId)
|
||||
isSending: DraftStore.isSendingDraft(@props.localId)
|
||||
|
||||
componentWillMount: =>
|
||||
@_prepareForDraft(@props.localId)
|
||||
|
@ -418,7 +418,7 @@ class ComposerView extends React.Component
|
|||
@focus "textFieldCc"
|
||||
|
||||
_onSendingStateChanged: =>
|
||||
@setState isSending: DraftStore.sendingState(@props.localId)
|
||||
@setState isSending: DraftStore.isSendingDraft(@props.localId)
|
||||
|
||||
|
||||
undo: (event) =>
|
||||
|
|
|
@ -181,12 +181,12 @@ describe "populated composer", ->
|
|||
|
||||
describe "When sending a message", ->
|
||||
beforeEach ->
|
||||
spyOn(atom, "isMainWindow").andReturn true
|
||||
remote = require('remote')
|
||||
@dialog = remote.require('dialog')
|
||||
spyOn(remote, "getCurrentWindow")
|
||||
spyOn(@dialog, "showMessageBox")
|
||||
spyOn(Actions, "sendDraft")
|
||||
DraftStore._sendingState = {}
|
||||
|
||||
it "shows a warning if there are no recipients", ->
|
||||
useDraft.call @, subject: "no recipients"
|
||||
|
@ -291,7 +291,7 @@ describe "populated composer", ->
|
|||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
simulateDraftStore = ->
|
||||
DraftStore._sendingState[DRAFT_LOCAL_ID] = true
|
||||
spyOn(DraftStore, "isSendingDraft").andReturn true
|
||||
DraftStore.trigger()
|
||||
|
||||
it "doesn't send twice if you double click", ->
|
||||
|
@ -314,14 +314,20 @@ describe "populated composer", ->
|
|||
expect(@composer.state.isSending).toBe true
|
||||
|
||||
it "re-enables the composer if sending threw an error", ->
|
||||
sending = null
|
||||
spyOn(DraftStore, "isSendingDraft").andCallFake => return sending
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
simulateDraftStore()
|
||||
expect(@composer.state.isSending).toBe true
|
||||
Actions.sendDraftError("oh no")
|
||||
DraftStore._sendingState[DRAFT_LOCAL_ID] = false
|
||||
|
||||
sending = true
|
||||
DraftStore.trigger()
|
||||
|
||||
expect(@composer.state.isSending).toBe true
|
||||
|
||||
sending = false
|
||||
DraftStore.trigger()
|
||||
|
||||
expect(@composer.state.isSending).toBe false
|
||||
|
||||
describe "when sending a message with keyboard inputs", ->
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"clear-cut": "0.4.0",
|
||||
"coffee-react": "^2.0.0",
|
||||
"coffee-script": "1.9.0",
|
||||
"coffeestack": "0.8.0",
|
||||
"coffeestack": "^1.1",
|
||||
"classnames": "1.2.1",
|
||||
"color": "^0.7.3",
|
||||
"delegato": "^1",
|
||||
|
|
|
@ -5,6 +5,7 @@ ModelQuery = require '../../src/flux/models/query'
|
|||
NamespaceStore = require '../../src/flux/stores/namespace-store'
|
||||
DatabaseStore = require '../../src/flux/stores/database-store'
|
||||
DraftStore = require '../../src/flux/stores/draft-store'
|
||||
TaskQueue = require '../../src/flux/stores/task-queue'
|
||||
SendDraftTask = require '../../src/flux/tasks/send-draft'
|
||||
DestroyDraftTask = require '../../src/flux/tasks/destroy-draft'
|
||||
Actions = require '../../src/flux/actions'
|
||||
|
@ -307,28 +308,28 @@ describe "DraftStore", ->
|
|||
, (thread, message) ->
|
||||
expect(message).toEqual(fakeMessage1)
|
||||
{}
|
||||
|
||||
|
||||
describe "onDestroyDraft", ->
|
||||
beforeEach ->
|
||||
@draftReset = jasmine.createSpy('draft reset')
|
||||
spyOn(Actions, 'queueTask')
|
||||
DraftStore._draftSessions = {"abc":{
|
||||
@session =
|
||||
draft: ->
|
||||
pristine: false
|
||||
changes:
|
||||
commit: -> Promise.resolve()
|
||||
reset: @draftReset
|
||||
cleanup: ->
|
||||
}}
|
||||
DraftStore._draftSessions = {"abc": @session}
|
||||
spyOn(Actions, 'queueTask')
|
||||
|
||||
it "should reset the draft session, ensuring no more saves are made", ->
|
||||
DraftStore._onDestroyDraft('abc')
|
||||
expect(@draftReset).toHaveBeenCalled()
|
||||
|
||||
it "should not do anything if the draft session is not in the window", ->
|
||||
it "should throw if the draft session is not in the window", ->
|
||||
expect ->
|
||||
DraftStore._onDestroyDraft('other')
|
||||
.not.toThrow()
|
||||
.toThrow()
|
||||
expect(@draftReset).not.toHaveBeenCalled()
|
||||
|
||||
it "should queue a destroy draft task", ->
|
||||
|
@ -337,9 +338,21 @@ describe "DraftStore", ->
|
|||
expect(Actions.queueTask.mostRecentCall.args[0] instanceof DestroyDraftTask).toBe(true)
|
||||
|
||||
it "should clean up the draft session", ->
|
||||
spyOn(DraftStore, 'cleanupSessionForLocalId')
|
||||
spyOn(DraftStore, '_doneWithSession')
|
||||
DraftStore._onDestroyDraft('abc')
|
||||
expect(DraftStore.cleanupSessionForLocalId).toHaveBeenCalledWith('abc')
|
||||
expect(DraftStore._doneWithSession).toHaveBeenCalledWith(@session)
|
||||
|
||||
it "should close the window if it's a popout", ->
|
||||
spyOn(atom, "close")
|
||||
spyOn(DraftStore, "_isPopout").andReturn true
|
||||
DraftStore._onDestroyDraft('abc')
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
|
||||
it "should NOT close the window if isn't a popout", ->
|
||||
spyOn(atom, "close")
|
||||
spyOn(DraftStore, "_isPopout").andReturn false
|
||||
DraftStore._onDestroyDraft('abc')
|
||||
expect(atom.close).not.toHaveBeenCalled()
|
||||
|
||||
describe "before unloading", ->
|
||||
it "should destroy pristine drafts", ->
|
||||
|
@ -389,7 +402,6 @@ describe "DraftStore", ->
|
|||
describe "sending a draft", ->
|
||||
draftLocalId = "local-123"
|
||||
beforeEach ->
|
||||
DraftStore._sendingState = {}
|
||||
DraftStore._draftSessions = {}
|
||||
proxy =
|
||||
prepare: -> Promise.resolve(proxy)
|
||||
|
@ -398,32 +410,25 @@ describe "DraftStore", ->
|
|||
changes:
|
||||
commit: -> Promise.resolve()
|
||||
DraftStore._draftSessions[draftLocalId] = proxy
|
||||
spyOn(DraftStore, "_doneWithSession").andCallThrough()
|
||||
spyOn(DraftStore, "trigger")
|
||||
TaskQueue._queue = []
|
||||
|
||||
it "sets the sending state when sending", ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
expect(DraftStore.sendingState(draftLocalId)).toBe true
|
||||
expect(DraftStore.trigger).toHaveBeenCalled()
|
||||
spyOn(atom, "isMainWindow").andReturn true
|
||||
spyOn(TaskQueue, "_update")
|
||||
spyOn(Actions, "queueTask").andCallThrough()
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
waitsFor ->
|
||||
Actions.queueTask.calls.length > 0
|
||||
runs ->
|
||||
expect(DraftStore.isSendingDraft(draftLocalId)).toBe true
|
||||
expect(DraftStore.trigger).toHaveBeenCalled()
|
||||
|
||||
it "returns false if the draft hasn't been seen", ->
|
||||
expect(DraftStore.sendingState(draftLocalId)).toBe false
|
||||
|
||||
it "resets the sending state on success", ->
|
||||
waitsForPromise ->
|
||||
DraftStore._onSendDraft(draftLocalId).then ->
|
||||
expect(DraftStore.sendingState(draftLocalId)).toBe true
|
||||
|
||||
DraftStore._onSendDraftSuccess({draftLocalId})
|
||||
expect(DraftStore.sendingState(draftLocalId)).toBe false
|
||||
expect(DraftStore.trigger).toHaveBeenCalled()
|
||||
|
||||
it "resets the sending state on error", ->
|
||||
waitsForPromise ->
|
||||
DraftStore._onSendDraft(draftLocalId).then ->
|
||||
expect(DraftStore.sendingState(draftLocalId)).toBe true
|
||||
DraftStore._onSendDraftError(draftLocalId)
|
||||
expect(DraftStore.sendingState(draftLocalId)).toBe false
|
||||
expect(DraftStore.trigger).toHaveBeenCalled()
|
||||
spyOn(atom, "isMainWindow").andReturn true
|
||||
expect(DraftStore.isSendingDraft(draftLocalId)).toBe false
|
||||
|
||||
it "closes the window if it's a popout", ->
|
||||
spyOn(atom, "getWindowType").andReturn "composer"
|
||||
|
@ -432,91 +437,63 @@ describe "DraftStore", ->
|
|||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
waitsFor "Atom to close", ->
|
||||
advanceClock(1000)
|
||||
atom.close.calls.length > 0
|
||||
|
||||
it "doesn't close the window if it's inline", ->
|
||||
spyOn(atom, "getWindowType").andReturn "other"
|
||||
spyOn(atom, "isMainWindow").andReturn false
|
||||
spyOn(atom, "close")
|
||||
waitsForPromise ->
|
||||
DraftStore._onSendDraft(draftLocalId).then ->
|
||||
expect(atom.close).not.toHaveBeenCalled()
|
||||
spyOn(DraftStore, "_isPopout").andCallThrough()
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
waitsFor ->
|
||||
DraftStore._isPopout.calls.length > 0
|
||||
runs ->
|
||||
expect(atom.close).not.toHaveBeenCalled()
|
||||
|
||||
it "queues a SendDraftTask", ->
|
||||
spyOn(Actions, "queueTask")
|
||||
waitsForPromise ->
|
||||
DraftStore._onSendDraft(draftLocalId).then ->
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task instanceof SendDraftTask).toBe true
|
||||
expect(task.draftLocalId).toBe draftLocalId
|
||||
expect(task.fromPopout).toBe false
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
waitsFor ->
|
||||
DraftStore._doneWithSession.calls.length > 0
|
||||
runs ->
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task instanceof SendDraftTask).toBe true
|
||||
expect(task.draftLocalId).toBe draftLocalId
|
||||
expect(task.fromPopout).toBe false
|
||||
|
||||
it "queues a SendDraftTask with popout info", ->
|
||||
spyOn(atom, "getWindowType").andReturn "composer"
|
||||
spyOn(atom, "isMainWindow").andReturn false
|
||||
spyOn(atom, "close")
|
||||
spyOn(Actions, "queueTask")
|
||||
waitsForPromise ->
|
||||
DraftStore._onSendDraft(draftLocalId).then ->
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task.fromPopout).toBe true
|
||||
runs ->
|
||||
DraftStore._onSendDraft(draftLocalId)
|
||||
waitsFor ->
|
||||
DraftStore._doneWithSession.calls.length > 0
|
||||
runs ->
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
task = Actions.queueTask.calls[0].args[0]
|
||||
expect(task.fromPopout).toBe true
|
||||
|
||||
describe "cleanupSessionForLocalId", ->
|
||||
it "should destroy the draft if it is pristine", ->
|
||||
DraftStore._draftSessions = {"abc":{
|
||||
describe "session cleanup", ->
|
||||
beforeEach ->
|
||||
@draftCleanup = jasmine.createSpy('draft cleanup')
|
||||
@session =
|
||||
draftLocalId: "abc"
|
||||
draft: ->
|
||||
pristine: true
|
||||
cleanup: ->
|
||||
}}
|
||||
spyOn(Actions, 'queueTask')
|
||||
DraftStore.cleanupSessionForLocalId('abc')
|
||||
expect(Actions.queueTask).toHaveBeenCalled()
|
||||
expect(Actions.queueTask.mostRecentCall.args[0] instanceof DestroyDraftTask).toBe(true)
|
||||
pristine: false
|
||||
changes:
|
||||
commit: -> Promise.resolve()
|
||||
reset: ->
|
||||
cleanup: @draftCleanup
|
||||
DraftStore._draftSessions = {"abc": @session}
|
||||
DraftStore._doneWithSession(@session)
|
||||
|
||||
it "should not do anything bad if the session does not exist", ->
|
||||
expect ->
|
||||
DraftStore.cleanupSessionForLocalId('dne')
|
||||
.not.toThrow()
|
||||
it "removes from the list of draftSessions", ->
|
||||
expect(DraftStore._draftSessions["abc"]).toBeUndefined()
|
||||
|
||||
describe "when in the popout composer", ->
|
||||
beforeEach ->
|
||||
spyOn(atom, "getWindowType").andReturn "composer"
|
||||
spyOn(atom, "isMainWindow").andReturn false
|
||||
DraftStore._draftSessions = {"abc":{
|
||||
draft: ->
|
||||
pristine: false
|
||||
cleanup: ->
|
||||
}}
|
||||
|
||||
it "should close the composer window", ->
|
||||
spyOn(atom, 'close')
|
||||
DraftStore.cleanupSessionForLocalId('abc')
|
||||
advanceClock(1000)
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
|
||||
it "should not close the composer window if the draft session is not in the window", ->
|
||||
spyOn(atom, 'close')
|
||||
DraftStore.cleanupSessionForLocalId('other-random-draft-id')
|
||||
advanceClock(1000)
|
||||
expect(atom.close).not.toHaveBeenCalled()
|
||||
|
||||
describe "when it is in a main window", ->
|
||||
beforeEach ->
|
||||
@cleanup = jasmine.createSpy('cleanup')
|
||||
spyOn(atom, "isMainWindow").andReturn true
|
||||
DraftStore._draftSessions = {"abc":{
|
||||
draft: ->
|
||||
pristine: false
|
||||
cleanup: @cleanup
|
||||
}}
|
||||
|
||||
it "should call proxy.cleanup() to unlink listeners", ->
|
||||
DraftStore.cleanupSessionForLocalId('abc')
|
||||
expect(@cleanup).toHaveBeenCalled()
|
||||
|
||||
it "should remove the proxy from the sessions list", ->
|
||||
DraftStore.cleanupSessionForLocalId('abc')
|
||||
expect(DraftStore._draftSessions).toEqual({})
|
||||
it "Calls cleanup on the session", ->
|
||||
expect(@draftCleanup).toHaveBeenCalled
|
||||
|
|
|
@ -15,6 +15,7 @@ class TaskSubclassB extends Task
|
|||
constructor: (val) -> @bProp = val; super
|
||||
|
||||
describe "TaskQueue", ->
|
||||
|
||||
makeUnstartedTask = (task) ->
|
||||
TaskQueue._initializeTask(task)
|
||||
return task
|
||||
|
@ -50,7 +51,6 @@ describe "TaskQueue", ->
|
|||
return task
|
||||
|
||||
beforeEach ->
|
||||
TaskQueue._onlineStatus = true
|
||||
@task = new Task()
|
||||
@unstartedTask = makeUnstartedTask(new Task())
|
||||
@localStarted = makeLocalStarted(new Task())
|
||||
|
@ -83,9 +83,11 @@ describe "TaskQueue", ->
|
|||
taks.queueState.performedRemote = false
|
||||
taks.queueState.notifiedOffline = false
|
||||
|
||||
afterEach ->
|
||||
TaskQueue._queue = []
|
||||
TaskQueue._completed = []
|
||||
localSpy = (task) ->
|
||||
spyOn(task, "performLocal").andCallFake -> Promise.resolve()
|
||||
|
||||
remoteSpy = (task) ->
|
||||
spyOn(task, "performRemote").andCallFake -> Promise.resolve()
|
||||
|
||||
describe "enqueue", ->
|
||||
it "makes sure you've queued a real task", ->
|
||||
|
@ -169,8 +171,8 @@ describe "TaskQueue", ->
|
|||
|
||||
describe "process Task", ->
|
||||
it "doesn't process processing tasks", ->
|
||||
spyOn(@remoteStarted, "performLocal")
|
||||
spyOn(@remoteStarted, "performRemote")
|
||||
localSpy(@remoteStarted)
|
||||
remoteSpy(@remoteStarted)
|
||||
TaskQueue._processTask(@remoteStarted)
|
||||
expect(@remoteStarted.performLocal).not.toHaveBeenCalled()
|
||||
expect(@remoteStarted.performRemote).not.toHaveBeenCalled()
|
||||
|
@ -181,8 +183,8 @@ describe "TaskQueue", ->
|
|||
shouldWaitForTask: (other) -> other instanceof TaskSubclassA
|
||||
|
||||
blockedByTask = new BlockedByTaskA()
|
||||
spyOn(blockedByTask, "performLocal")
|
||||
spyOn(blockedByTask, "performRemote")
|
||||
localSpy(blockedByTask)
|
||||
remoteSpy(blockedByTask)
|
||||
|
||||
blockingTask = makeRemoteFailed(new TaskSubclassA())
|
||||
|
||||
|
@ -199,8 +201,8 @@ describe "TaskQueue", ->
|
|||
shouldWaitForTask: (other) -> other instanceof BlockingTask
|
||||
|
||||
blockedByTask = new BlockingTask()
|
||||
spyOn(blockedByTask, "performLocal")
|
||||
spyOn(blockedByTask, "performRemote")
|
||||
localSpy(blockedByTask)
|
||||
remoteSpy(blockedByTask)
|
||||
|
||||
blockingTask = makeRemoteFailed(new BlockingTask())
|
||||
|
||||
|
@ -212,19 +214,21 @@ describe "TaskQueue", ->
|
|||
expect(blockedByTask.performRemote).not.toHaveBeenCalled()
|
||||
|
||||
it "sets the processing bit", ->
|
||||
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
|
||||
localSpy(@unstartedTask)
|
||||
TaskQueue._queue = [@unstartedTask]
|
||||
TaskQueue._processTask(@unstartedTask)
|
||||
expect(@unstartedTask.queueState.isProcessing).toBe true
|
||||
|
||||
it "performs local if it's a fresh task", ->
|
||||
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
|
||||
localSpy(@unstartedTask)
|
||||
TaskQueue._queue = [@unstartedTask]
|
||||
TaskQueue._processTask(@unstartedTask)
|
||||
expect(@unstartedTask.performLocal).toHaveBeenCalled()
|
||||
|
||||
describe "performLocal", ->
|
||||
it "on success it marks it as complete with the timestamp", ->
|
||||
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
|
||||
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
|
||||
localSpy(@unstartedTask)
|
||||
remoteSpy(@unstartedTask)
|
||||
runs ->
|
||||
TaskQueue.enqueue(@unstartedTask)
|
||||
waitsFor =>
|
||||
|
@ -234,7 +238,7 @@ describe "TaskQueue", ->
|
|||
|
||||
it "throws an error if it fails", ->
|
||||
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.reject("boo")
|
||||
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
|
||||
remoteSpy(@unstartedTask)
|
||||
runs ->
|
||||
TaskQueue.enqueue(@unstartedTask)
|
||||
waitsFor =>
|
||||
|
@ -246,7 +250,7 @@ describe "TaskQueue", ->
|
|||
|
||||
it "dequeues the task if it fails locally", ->
|
||||
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.reject("boo")
|
||||
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
|
||||
remoteSpy(@unstartedTask)
|
||||
runs ->
|
||||
TaskQueue.enqueue(@unstartedTask)
|
||||
waitsFor =>
|
||||
|
@ -257,10 +261,10 @@ describe "TaskQueue", ->
|
|||
|
||||
describe "performRemote", ->
|
||||
beforeEach ->
|
||||
spyOn(@unstartedTask, "performLocal").andCallFake -> Promise.resolve()
|
||||
localSpy(@unstartedTask)
|
||||
|
||||
it "performs remote properly", ->
|
||||
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
|
||||
remoteSpy(@unstartedTask)
|
||||
runs ->
|
||||
TaskQueue.enqueue(@unstartedTask)
|
||||
waitsFor =>
|
||||
|
@ -270,7 +274,7 @@ describe "TaskQueue", ->
|
|||
expect(@unstartedTask.performRemote).toHaveBeenCalled()
|
||||
|
||||
it "dequeues on success", ->
|
||||
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
|
||||
remoteSpy(@unstartedTask)
|
||||
runs ->
|
||||
TaskQueue.enqueue(@unstartedTask)
|
||||
waitsFor =>
|
||||
|
@ -282,7 +286,7 @@ describe "TaskQueue", ->
|
|||
|
||||
it "notifies we're offline the first time", ->
|
||||
spyOn(TaskQueue, "_isOnline").andReturn false
|
||||
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
|
||||
remoteSpy(@unstartedTask)
|
||||
spyOn(@unstartedTask, "onError")
|
||||
runs ->
|
||||
TaskQueue.enqueue(@unstartedTask)
|
||||
|
@ -297,8 +301,8 @@ describe "TaskQueue", ->
|
|||
|
||||
it "doesn't notify we're offline the second+ time", ->
|
||||
spyOn(TaskQueue, "_isOnline").andReturn false
|
||||
spyOn(@remoteFailed, "performLocal").andCallFake -> Promise.resolve()
|
||||
spyOn(@remoteFailed, "performRemote").andCallFake -> Promise.resolve()
|
||||
localSpy(@remoteFailed)
|
||||
remoteSpy(@remoteFailed)
|
||||
spyOn(@remoteFailed, "onError")
|
||||
@remoteFailed.queueState.notifiedOffline = true
|
||||
TaskQueue._queue = [@remoteFailed]
|
||||
|
@ -312,7 +316,7 @@ describe "TaskQueue", ->
|
|||
expect(@remoteFailed.onError).not.toHaveBeenCalled()
|
||||
|
||||
it "marks performedRemote on success", ->
|
||||
spyOn(@unstartedTask, "performRemote").andCallFake -> Promise.resolve()
|
||||
remoteSpy(@unstartedTask)
|
||||
runs ->
|
||||
TaskQueue.enqueue(@unstartedTask)
|
||||
waitsFor =>
|
||||
|
@ -362,8 +366,8 @@ describe "TaskQueue", ->
|
|||
@remoteFailed]
|
||||
it "when all tasks pass it processes all items", ->
|
||||
for task in TaskQueue._queue
|
||||
spyOn(task, "performLocal").andCallFake -> Promise.resolve()
|
||||
spyOn(task, "performRemote").andCallFake -> Promise.resolve()
|
||||
localSpy(task)
|
||||
remoteSpy(task)
|
||||
runs ->
|
||||
TaskQueue.enqueue(new Task)
|
||||
waitsFor ->
|
||||
|
|
|
@ -45,12 +45,14 @@ uploadData =
|
|||
|
||||
describe "FileUploadTask", ->
|
||||
it "rejects if not initialized with a path name", (done) ->
|
||||
waitsForPromise shouldReject: true, ->
|
||||
(new FileUploadTask).performLocal()
|
||||
waitsForPromise ->
|
||||
(new FileUploadTask).performLocal().catch (err) ->
|
||||
expect(err instanceof Error).toBe true
|
||||
|
||||
it "rejects if not initialized with a messageLocalId", ->
|
||||
waitsForPromise shouldReject: true, ->
|
||||
(new FileUploadTask(test_file_paths[0])).performLocal()
|
||||
waitsForPromise ->
|
||||
(new FileUploadTask(test_file_paths[0])).performLocal().catch (err) ->
|
||||
expect(err instanceof Error).toBe true
|
||||
|
||||
beforeEach ->
|
||||
@task = new FileUploadTask(test_file_paths[0], localId)
|
||||
|
|
|
@ -201,7 +201,6 @@ describe "SendDraftTask", ->
|
|||
email: 'dummy@nylas.com'
|
||||
@task = new SendDraftTask(@draft.id)
|
||||
spyOn(Actions, "dequeueTask")
|
||||
spyOn(Actions, "sendDraftError")
|
||||
|
||||
it "throws an error if the draft can't be found", ->
|
||||
spyOn(DatabaseStore, 'findByLocalId').andCallFake (klass, localId) ->
|
||||
|
@ -224,25 +223,15 @@ describe "SendDraftTask", ->
|
|||
@task.performRemote().catch (error) ->
|
||||
expect(error).toBe "DB error"
|
||||
|
||||
checkError = ->
|
||||
expect(Actions.sendDraftError).toHaveBeenCalled()
|
||||
args = Actions.sendDraftError.calls[0].args
|
||||
expect(args[0]).toBe @draft.id
|
||||
expect(args[1].length).toBeGreaterThan 0
|
||||
|
||||
it "onAPIError notifies of the error", ->
|
||||
@task.onAPIError(message: "oh no")
|
||||
checkError.call(@)
|
||||
|
||||
it "onOtherError notifies of the error", ->
|
||||
@task.onOtherError()
|
||||
checkError.call(@)
|
||||
|
||||
it "onTimeoutError notifies of the error", ->
|
||||
@task.onTimeoutError()
|
||||
checkError.call(@)
|
||||
|
||||
it "onOfflineError notifies of the error and dequeues", ->
|
||||
@task.onOfflineError()
|
||||
checkError.call(@)
|
||||
expect(Actions.dequeueTask).toHaveBeenCalledWith(@task)
|
||||
|
|
|
@ -20,7 +20,7 @@ clipboard = require 'clipboard'
|
|||
|
||||
NamespaceStore = require "../src/flux/stores/namespace-store"
|
||||
Contact = require '../src/flux/models/contact'
|
||||
{ComponentRegistry} = require "nylas-exports"
|
||||
{TaskQueue, ComponentRegistry} = require "nylas-exports"
|
||||
|
||||
atom.themes.loadBaseStylesheets()
|
||||
atom.themes.requireStylesheet '../static/jasmine'
|
||||
|
@ -101,6 +101,10 @@ beforeEach ->
|
|||
Grim.clearDeprecations() if isCoreSpec
|
||||
ComponentRegistry._clear()
|
||||
|
||||
TaskQueue._queue = []
|
||||
TaskQueue._completed = []
|
||||
TaskQueue._onlineStatus = true
|
||||
|
||||
$.fx.off = true
|
||||
documentTitle = null
|
||||
atom.packages.serviceHub = new ServiceHub
|
||||
|
|
|
@ -246,7 +246,7 @@ class Atom extends Model
|
|||
# to prevent the developer tools from being shown
|
||||
@emitter.emit('will-throw-error', eventObject)
|
||||
|
||||
if openDevTools
|
||||
if openDevTools and @inDevMode()
|
||||
@openDevTools()
|
||||
@executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()')
|
||||
|
||||
|
@ -257,21 +257,17 @@ class Atom extends Model
|
|||
# Since Bluebird is the promise library, we can properly report
|
||||
# unhandled errors from business logic inside promises.
|
||||
Promise.longStackTraces() unless @inSpecMode()
|
||||
Promise.onPossiblyUnhandledRejection (error) =>
|
||||
# In many cases, a promise will return a legitimate error which the receiver
|
||||
# doesn't care to handle. The ones we want to surface are core javascript errors:
|
||||
# Syntax problems, type errors, etc. If we didn't catch them here, these issues
|
||||
# (usually inside then() blocks) would be hard to track down.
|
||||
return unless (error instanceof TypeError or
|
||||
error instanceof SyntaxError or
|
||||
error instanceof RangeError or
|
||||
error instanceof ReferenceError)
|
||||
|
||||
Promise.onPossiblyUnhandledRejection (error) =>
|
||||
error.stack = convertStackTrace(error.stack, sourceMapCache)
|
||||
eventObject = {message: error.message, originalError: error}
|
||||
|
||||
if @inSpecMode()
|
||||
console.warn(error.stack)
|
||||
console.error(error.stack)
|
||||
else if @inDevMode()
|
||||
console.error(error.message, error.stack, error)
|
||||
@openDevTools()
|
||||
@executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()')
|
||||
else
|
||||
console.warn(error)
|
||||
console.warn(error.stack)
|
||||
|
|
|
@ -95,7 +95,6 @@ class Actions
|
|||
@fileUploaded: ActionScopeGlobal
|
||||
@attachFileComplete: ActionScopeGlobal
|
||||
@multiWindowNotification: ActionScopeGlobal
|
||||
@sendDraftError: ActionScopeGlobal
|
||||
@sendDraftSuccess: ActionScopeGlobal
|
||||
@sendToAllWindows: ActionScopeGlobal
|
||||
|
||||
|
|
|
@ -64,7 +64,6 @@ AnalyticsStore = Reflux.createStore
|
|||
logout: -> {}
|
||||
fileAborted: (uploadData={}) -> {fileSize: uploadData.fileSize}
|
||||
fileUploaded: (uploadData={}) -> {fileSize: uploadData.fileSize}
|
||||
sendDraftError: (dId, msg) -> {drafLocalId: dId, error: msg}
|
||||
sendDraftSuccess: ({draftLocalId}) -> {draftLocalId: draftLocalId}
|
||||
|
||||
track: (action, data={}) ->
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
Message = require '../models/message'
|
||||
Actions = require '../actions'
|
||||
EventEmitter = require('events').EventEmitter
|
||||
|
||||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
||||
CoffeeHelpers = require '../coffee-helpers'
|
||||
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
###
|
||||
|
@ -64,21 +67,23 @@ that display Draft objects or allow for interactive editing of Drafts.
|
|||
Section: Drafts
|
||||
###
|
||||
class DraftStoreProxy
|
||||
@include: CoffeeHelpers.includeModule
|
||||
|
||||
@include Publisher
|
||||
@include Listener
|
||||
|
||||
constructor: (@draftLocalId) ->
|
||||
DraftStore = require './draft-store'
|
||||
|
||||
@unlisteners = []
|
||||
@unlisteners.push DraftStore.listen(@_onDraftChanged, @)
|
||||
@unlisteners.push Actions.didSwapModel.listen(@_onDraftSwapped, @)
|
||||
@listenTo DraftStore, @_onDraftChanged
|
||||
@listenTo Actions.didSwapModel, @_onDraftSwapped
|
||||
|
||||
@_emitter = new EventEmitter()
|
||||
@_draft = false
|
||||
@_draftPromise = null
|
||||
@changes = new DraftChangeSet @draftLocalId, =>
|
||||
if !@_draft
|
||||
throw new Error("DraftChangeSet was modified before the draft was prepared.")
|
||||
@_emitter.emit('trigger')
|
||||
@trigger()
|
||||
|
||||
@prepare().catch (error) ->
|
||||
console.error(error)
|
||||
|
@ -101,26 +106,14 @@ class DraftStoreProxy
|
|||
.catch(reject)
|
||||
@_draftPromise
|
||||
|
||||
listen: (callback, bindContext) ->
|
||||
eventHandler = (args) ->
|
||||
callback.apply(bindContext, args)
|
||||
@_emitter.addListener('trigger', eventHandler)
|
||||
return =>
|
||||
@_emitter.removeListener('trigger', eventHandler)
|
||||
if @_emitter.listeners('trigger').length is 0
|
||||
DraftStore = require './draft-store'
|
||||
DraftStore.cleanupSessionForLocalId(@draftLocalId)
|
||||
|
||||
cleanup: ->
|
||||
# Unlink ourselves from the stores/actions we were listening to
|
||||
# so that we can be garbage collected
|
||||
unlisten() for unlisten in @unlisteners
|
||||
@stopListeningToAll()
|
||||
|
||||
_setDraft: (draft) ->
|
||||
if !draft.body?
|
||||
throw new Error("DraftStoreProxy._setDraft - new draft has no body!")
|
||||
@_draft = draft
|
||||
@_emitter.emit('trigger')
|
||||
@trigger()
|
||||
|
||||
_onDraftChanged: (change) ->
|
||||
return if not change?
|
||||
|
|
|
@ -16,6 +16,8 @@ Message = require '../models/message'
|
|||
MessageUtils = require '../models/message-utils'
|
||||
Actions = require '../actions'
|
||||
|
||||
TaskQueue = require './task-queue'
|
||||
|
||||
{subjectWithPrefix} = require '../models/utils'
|
||||
|
||||
{Listener, Publisher} = require '../modules/reflux-coffee'
|
||||
|
@ -50,19 +52,18 @@ class DraftStore
|
|||
atom.commands.add 'body',
|
||||
'application:new-message': => @_onPopoutBlankDraft()
|
||||
|
||||
# Remember that these two actions only fire in the current window and
|
||||
# are picked up by the instance of the DraftStore in the current
|
||||
# window.
|
||||
@listenTo Actions.sendDraft, @_onSendDraft
|
||||
@listenTo Actions.destroyDraft, @_onDestroyDraft
|
||||
|
||||
@listenTo Actions.removeFile, @_onRemoveFile
|
||||
@listenTo Actions.attachFileComplete, @_onAttachFileComplete
|
||||
|
||||
@listenTo Actions.sendDraftError, @_onSendDraftError
|
||||
@listenTo Actions.sendDraftSuccess, @_onSendDraftSuccess
|
||||
|
||||
atom.onBeforeUnload @_onBeforeUnload
|
||||
|
||||
@_draftSessions = {}
|
||||
@_sendingState = {}
|
||||
@_extensions = []
|
||||
|
||||
ipc.on 'mailto', @_onHandleMailtoLink
|
||||
|
@ -86,7 +87,7 @@ class DraftStore
|
|||
# - `localId` The {String} local ID of the draft.
|
||||
#
|
||||
# Returns a {Promise} that resolves to an {DraftStoreProxy} for the
|
||||
# draft:
|
||||
# draft once it has been prepared:
|
||||
sessionForLocalId: (localId) =>
|
||||
if not localId
|
||||
console.log((new Error).stack)
|
||||
|
@ -95,7 +96,15 @@ class DraftStore
|
|||
@_draftSessions[localId].prepare()
|
||||
|
||||
# Public: Look up the sending state of the given draft Id.
|
||||
sendingState: (draftLocalId) -> @_sendingState[draftLocalId] ? false
|
||||
# In popout windows the existance of the window is the sending state.
|
||||
isSendingDraft: (draftLocalId) ->
|
||||
if atom.isMainWindow()
|
||||
task = TaskQueue.findTask
|
||||
object: "SendDraftTask"
|
||||
matchKey: "draftLocalId"
|
||||
matchValue: draftLocalId
|
||||
return task?
|
||||
else return false
|
||||
|
||||
###
|
||||
Composer Extensions
|
||||
|
@ -123,29 +132,9 @@ class DraftStore
|
|||
|
||||
########### PRIVATE ####################################################
|
||||
|
||||
cleanupSessionForLocalId: (localId) =>
|
||||
session = @_draftSessions[localId]
|
||||
return unless session
|
||||
|
||||
draft = session.draft()
|
||||
Actions.queueTask(new DestroyDraftTask(localId)) if draft.pristine
|
||||
|
||||
if atom.getWindowType() is "composer"
|
||||
# Sometimes we swap out one ID for another. In that case we don't
|
||||
# want to close while it's swapping. We are using a defer here to
|
||||
# give the swap code time to put the new ID in the @_draftSessions.
|
||||
#
|
||||
# This defer hack prevents us from having to pass around a lock or a
|
||||
# parameter through functions who may do this in other parts of the
|
||||
# application.
|
||||
_.defer =>
|
||||
if Object.keys(@_draftSessions).length is 0
|
||||
atom.close()
|
||||
|
||||
if atom.isMainWindow()
|
||||
session.cleanup()
|
||||
|
||||
delete @_draftSessions[localId]
|
||||
_doneWithSession: (session) ->
|
||||
session.cleanup()
|
||||
delete @_draftSessions[session.draftLocalId]
|
||||
|
||||
_onBeforeUnload: =>
|
||||
promises = []
|
||||
|
@ -357,46 +346,49 @@ class DraftStore
|
|||
DatabaseStore.localIdForModel(draft).then(@_onPopoutDraftLocalId)
|
||||
|
||||
_onDestroyDraft: (draftLocalId) =>
|
||||
session = @_draftSessions[draftLocalId]
|
||||
|
||||
if not session
|
||||
throw new Error("Couldn't find the draft session in the current window")
|
||||
|
||||
# Immediately reset any pending changes so no saves occur
|
||||
@_draftSessions[draftLocalId]?.changes.reset()
|
||||
session.changes.reset()
|
||||
|
||||
# Queue the task to destroy the draft
|
||||
Actions.queueTask(new DestroyDraftTask(draftLocalId))
|
||||
|
||||
# Clean up the draft session
|
||||
@cleanupSessionForLocalId(draftLocalId)
|
||||
@_doneWithSession(session)
|
||||
|
||||
atom.close() if @_isPopout()
|
||||
|
||||
# The user request to send the draft
|
||||
_onSendDraft: (draftLocalId) =>
|
||||
new Promise (resolve, reject) =>
|
||||
@_sendingState[draftLocalId] = true
|
||||
@trigger()
|
||||
@sessionForLocalId(draftLocalId).then (session) =>
|
||||
@_runExtensionsBeforeSend(session)
|
||||
|
||||
@sessionForLocalId(draftLocalId).then (session) =>
|
||||
# Give third-party plugins an opportunity to sanitize draft data
|
||||
for extension in @_extensions
|
||||
continue unless extension.finalizeSessionBeforeSending
|
||||
extension.finalizeSessionBeforeSending(session)
|
||||
# Immediately save any pending changes so we don't save after sending
|
||||
session.changes.commit().then =>
|
||||
|
||||
# Immediately save any pending changes so we don't save after sending
|
||||
session.changes.commit().then =>
|
||||
# Queue the task to send the draft
|
||||
fromPopout = atom.getWindowType() is "composer"
|
||||
Actions.queueTask(new SendDraftTask(draftLocalId, fromPopout: fromPopout))
|
||||
task = new SendDraftTask draftLocalId, {fromPopout: @_isPopout()}
|
||||
Actions.queueTask(task)
|
||||
|
||||
# Clean up session, close window
|
||||
@cleanupSessionForLocalId(draftLocalId)
|
||||
# As far as this window is concerned, we're not making any more
|
||||
# edits and are destroying the session. If there are errors down
|
||||
# the line, we'll make a new session and handle them later
|
||||
@_doneWithSession(session)
|
||||
|
||||
resolve()
|
||||
atom.close() if @_isPopout()
|
||||
|
||||
_onSendDraftError: (draftLocalId, errorMessage) ->
|
||||
@_sendingState[draftLocalId] = false
|
||||
if atom.getWindowType() is "composer"
|
||||
@_onPopoutDraftLocalId(draftLocalId, {errorMessage})
|
||||
@trigger()
|
||||
|
||||
_onSendDraftSuccess: ({draftLocalId}) =>
|
||||
@_sendingState[draftLocalId] = false
|
||||
@trigger()
|
||||
_isPopout: ->
|
||||
atom.getWindowType() is "composer"
|
||||
|
||||
# Give third-party plugins an opportunity to sanitize draft data
|
||||
_runExtensionsBeforeSend: (session) ->
|
||||
for extension in @_extensions
|
||||
continue unless extension.finalizeSessionBeforeSending
|
||||
extension.finalizeSessionBeforeSending(session)
|
||||
|
||||
_onAttachFileComplete: ({file, messageLocalId}) =>
|
||||
@sessionForLocalId(messageLocalId).then (session) ->
|
||||
|
|
|
@ -100,6 +100,12 @@ class TaskQueue
|
|||
performedRemote: false
|
||||
notifiedOffline: false
|
||||
|
||||
findTask: ({object, matchKey, matchValue}) ->
|
||||
for other in @_queue by -1
|
||||
if object is object and other[matchKey] is matchValue
|
||||
return other
|
||||
return null
|
||||
|
||||
enqueue: (task, {silent}={}) =>
|
||||
if not (task instanceof Task)
|
||||
throw new Error("You must queue a `Task` object")
|
||||
|
@ -111,6 +117,8 @@ class TaskQueue
|
|||
|
||||
dequeue: (taskOrId={}, {silent}={}) =>
|
||||
task = @_parseArgs(taskOrId)
|
||||
if not task
|
||||
throw new Error("Couldn't find task in queue to dequeue")
|
||||
|
||||
task.queueState.isProcessing = false
|
||||
task.cleanup()
|
||||
|
@ -125,14 +133,12 @@ class TaskQueue
|
|||
@_update()
|
||||
|
||||
dequeueMatching: (task) =>
|
||||
identifier = task.matchKey
|
||||
propValue = task.matchValue
|
||||
toDequeue = @findTask(task)
|
||||
|
||||
for other in @_queue by -1
|
||||
if task.object == task.object
|
||||
if other[identifier] == propValue
|
||||
@dequeue(other, silent: true)
|
||||
if not toDequeue
|
||||
console.warn("Could not find task: #{task?.object}", task)
|
||||
|
||||
@dequeue(toDequeue, silent: true)
|
||||
@_update()
|
||||
|
||||
clearCompleted: =>
|
||||
|
@ -213,8 +219,6 @@ class TaskQueue
|
|||
task = _.find @_queue, (task) -> task is taskOrId
|
||||
else
|
||||
task = _.findWhere(@_queue, id: taskOrId)
|
||||
if not task?
|
||||
throw new Error("Can't find task #{taskOrId}")
|
||||
return task
|
||||
|
||||
_moveToCompleted: (task) =>
|
||||
|
@ -237,7 +241,6 @@ class TaskQueue
|
|||
if not atom.inSpecMode()
|
||||
console.log("Queue deserialization failed with error: #{e.toString()}")
|
||||
|
||||
|
||||
# It's very important that we debounce saving here. When the user bulk-archives
|
||||
# items, they can easily process 1000 tasks at the same moment. We can't try to
|
||||
# save 1000 times! (Do not remove debounce without a plan!)
|
||||
|
|
|
@ -37,30 +37,34 @@ class SendDraftTask extends Task
|
|||
# The draft may have been deleted by another task. Nothing we can do.
|
||||
return reject(new Error("We couldn't find the saved draft.")) unless draft
|
||||
|
||||
if draft.isSaved()
|
||||
body =
|
||||
draft_id: draft.id
|
||||
version: draft.version
|
||||
else
|
||||
# Pass joined:true so the draft body is included
|
||||
body = draft.toJSON(joined: true)
|
||||
|
||||
NylasAPI.makeRequest
|
||||
path: "/n/#{draft.namespaceId}/send"
|
||||
method: 'POST'
|
||||
body: body
|
||||
body: @_prepareBody(draft)
|
||||
returnsModel: true
|
||||
success: (newMessage) =>
|
||||
newMessage = (new Message).fromJSON(newMessage)
|
||||
atom.playSound('mail_sent.ogg')
|
||||
Actions.postNotification({message: "Sent!", type: 'success'})
|
||||
Actions.sendDraftSuccess
|
||||
draftLocalId: @draftLocalId
|
||||
newMessage: newMessage
|
||||
DatabaseStore.unpersistModel(draft).then(resolve)
|
||||
success: @_onSendDraftSuccess(draft, resolve, reject)
|
||||
error: reject
|
||||
.catch(reject)
|
||||
|
||||
_prepareBody: (draft) ->
|
||||
if draft.isSaved()
|
||||
body =
|
||||
draft_id: draft.id
|
||||
version: draft.version
|
||||
else
|
||||
# Pass joined:true so the draft body is included
|
||||
body = draft.toJSON(joined: true)
|
||||
return body
|
||||
|
||||
_onSendDraftSuccess: (draft, resolve, reject) => (newMessage) =>
|
||||
newMessage = (new Message).fromJSON(newMessage)
|
||||
atom.playSound('mail_sent.ogg')
|
||||
Actions.postNotification({message: "Sent!", type: 'success'})
|
||||
Actions.sendDraftSuccess
|
||||
draftLocalId: @draftLocalId
|
||||
newMessage: newMessage
|
||||
DatabaseStore.unpersistModel(draft).then(resolve).catch(reject)
|
||||
|
||||
onAPIError: (apiError) ->
|
||||
msg = apiError.message ? "Our server is having problems. Your message has not been sent."
|
||||
@_notifyError(msg)
|
||||
|
@ -80,5 +84,6 @@ class SendDraftTask extends Task
|
|||
Actions.dequeueTask(@)
|
||||
|
||||
_notifyError: (msg) ->
|
||||
Actions.sendDraftError(@draftLocalId, msg)
|
||||
@notifyErrorMessage(msg)
|
||||
if @fromPopout
|
||||
Actions.composePopoutDraft(@draftLocalId, {errorMessage: msg})
|
||||
|
|
|
@ -84,6 +84,9 @@ class Task
|
|||
else if error instanceof OfflineError
|
||||
@onOfflineError(error)
|
||||
else
|
||||
if error instanceof Error
|
||||
console.error "Task #{@constructor.name} threw an unknown error: #{error.message}"
|
||||
console.error error.stack
|
||||
@onOtherError(error)
|
||||
|
||||
notifyErrorMessage: (msg) ->
|
||||
|
|
Loading…
Add table
Reference in a new issue